diff --git a/Package.swift b/Package.swift index 0432357..e4617ea 100644 --- a/Package.swift +++ b/Package.swift @@ -29,6 +29,9 @@ let package = Package( exclude: [ "../../Images", "../../Performance Profiler", + ], + swiftSettings: [ + .define("ENABLE_TESTABILITY", .when(configuration: .debug)) ] ), .testTarget( diff --git a/Sources/Boutique/SecurelyStoredValue.swift b/Sources/Boutique/SecurelyStoredValue.swift index a3a8824..2dd5200 100644 --- a/Sources/Boutique/SecurelyStoredValue.swift +++ b/Sources/Boutique/SecurelyStoredValue.swift @@ -128,6 +128,8 @@ public struct SecurelyStoredValue { public func remove() throws { if self.wrappedValue != nil { try self.removeItem() + } else if self.wrappedValue == nil && Self.keychainValueExists(group: self.group, service: self.keychainService, account: self.key) { + try self.removeItem() } } @@ -234,6 +236,22 @@ private extension SecurelyStoredValue { } } + static func keychainValueExists(group: String?, service: String, account: String) -> Bool { + let keychainQuery = [ + kSecClass: kSecClassGenericPassword, + kSecAttrService: service, + kSecAttrAccount: account, + kSecReturnData: true + ] + .withGroup(group) + .mapToStringDictionary() + + var extractedData: AnyObject? + let status = SecItemCopyMatching(keychainQuery as CFDictionary, &extractedData) + + return status != errSecItemNotFound + } + var keychainService: String { self.service ?? Self.defaultService } diff --git a/Sources/Boutique/Store.swift b/Sources/Boutique/Store.swift index 1b12e03..d6e9325 100644 --- a/Sources/Boutique/Store.swift +++ b/Sources/Boutique/Store.swift @@ -385,14 +385,16 @@ public extension Store { // Internal versions of the `insert`, `remove`, and `removeAll` function code paths so we can avoid duplicating code. internal extension Store { func performInsert(_ item: Item, firstRemovingExistingItems existingItemsStrategy: ItemRemovalStrategy? = nil) async throws { + var currentItems = await self.items + if let strategy = existingItemsStrategy { - // Remove items from disk and memory based on the cache invalidation strategy - var removedItems: [Item] = [item] - try await self.removeItems(&removedItems, withStrategy: strategy) + var removedItems = [item] + try await self.removeItemsFromStorageEngine(&removedItems, withStrategy: strategy) + // If we remove this one it will error + self.removeItemsFromMemory(¤tItems, withStrategy: strategy, identifier: cacheIdentifier) } // Take the current items array and turn it into an OrderedDictionary. - let currentItems = await self.items let identifier = item[keyPath: self.cacheIdentifier] let currentItemsKeys = currentItems.map({ $0[keyPath: self.cacheIdentifier] }) var currentValuesDictionary = OrderedDictionary(uniqueKeys: currentItemsKeys, values: currentItems) @@ -407,11 +409,16 @@ internal extension Store { } func performInsert(_ items: [Item], firstRemovingExistingItems existingItemsStrategy: ItemRemovalStrategy? = nil) async throws { + var currentItems = await self.items if let strategy = existingItemsStrategy { // Remove items from disk and memory based on the cache invalidation strategy var removedItems = items - try await self.removeItems(&removedItems, withStrategy: strategy) + try await self.removeItemsFromStorageEngine(&removedItems, withStrategy: strategy) + // This one is fine to remove... but why? + // Is it the way we construct the items in the ordered dictionary? + // If so should the two just use the same approach — perhaps sharing all the same code except for the last call to `persistItem` vs. `persistItems`? + self.removeItemsFromMemory(¤tItems, withStrategy: strategy, identifier: cacheIdentifier) } var insertedItemsDictionary = OrderedDictionary() @@ -424,7 +431,6 @@ internal extension Store { } // Take the current items array and turn it into an OrderedDictionary. - let currentItems = await self.items let currentItemsKeys = currentItems.map({ $0[keyPath: self.cacheIdentifier] }) var currentValuesDictionary = OrderedDictionary(uniqueKeys: currentItemsKeys, values: currentItems) @@ -477,7 +483,6 @@ internal extension Store { } private extension Store { - func persistItem(_ item: Item) async throws { let cacheKey = CacheKey(item[keyPath: self.cacheIdentifier]) @@ -503,18 +508,12 @@ private extension Store { try await self.storageEngine.remove(keys: itemKeys) } - func removeItems(_ items: inout [Item], withStrategy strategy: ItemRemovalStrategy) async throws { + func removeItemsFromStorageEngine(_ items: inout [Item], withStrategy strategy: ItemRemovalStrategy) async throws { let itemsToRemove = strategy.removedItems(items) // If we're using the `.removeNone` strategy then there are no items to invalidate and we can return early guard itemsToRemove.count != 0 else { return } - items = items.filter { item in - !itemsToRemove.contains(where: { - $0[keyPath: cacheIdentifier] == item[keyPath: cacheIdentifier] - } - )} - let itemKeys = itemsToRemove.map({ CacheKey(verbatim: $0[keyPath: self.cacheIdentifier]) }) if itemKeys.count == 1 { @@ -523,4 +522,14 @@ private extension Store { try await self.storageEngine.remove(keys: itemKeys) } } + + func removeItemsFromMemory(_ items: inout [Item], withStrategy strategy: ItemRemovalStrategy, identifier: KeyPath) { + let itemsToRemove = strategy.removedItems(items) + + items = items.filter { item in + !itemsToRemove.contains(where: { + $0[keyPath: identifier] == item[keyPath: identifier] + } + )} + } } diff --git a/Tests/BoutiqueTests/AsyncStoreTests.swift b/Tests/BoutiqueTests/AsyncStoreTests.swift index 95595e4..5060156 100644 --- a/Tests/BoutiqueTests/AsyncStoreTests.swift +++ b/Tests/BoutiqueTests/AsyncStoreTests.swift @@ -3,31 +3,30 @@ import Combine import XCTest final class AsyncStoreTests: XCTestCase { - private var asyncStore: Store! private var cancellables: Set = [] - + override func setUp() async throws { asyncStore = try await Store( storage: SQLiteStorageEngine.default(appendingPath: "Tests"), cacheIdentifier: \.merchantID) try await asyncStore.removeAll() } - + override func tearDown() { cancellables.removeAll() } - + @MainActor func testInsertingItem() async throws { try await asyncStore.insert(.coat) XCTAssertTrue(asyncStore.items.contains(.coat)) - + try await asyncStore.insert(.belt) XCTAssertTrue(asyncStore.items.contains(.belt)) XCTAssertEqual(asyncStore.items.count, 2) } - + @MainActor func testInsertingItems() async throws { try await asyncStore.insert([.coat, .sweater, .sweater, .purse]) @@ -35,127 +34,198 @@ final class AsyncStoreTests: XCTestCase { XCTAssertTrue(asyncStore.items.contains(.sweater)) XCTAssertTrue(asyncStore.items.contains(.purse)) } - + @MainActor func testInsertingDuplicateItems() async throws { XCTAssertTrue(asyncStore.items.isEmpty) try await asyncStore.insert(.allItems) XCTAssertEqual(asyncStore.items.count, 4) } - + @MainActor func testReadingItems() async throws { try await asyncStore.insert(.allItems) - + XCTAssertEqual(asyncStore.items[0], .coat) XCTAssertEqual(asyncStore.items[1], .sweater) XCTAssertEqual(asyncStore.items[2], .purse) XCTAssertEqual(asyncStore.items[3], .belt) - + XCTAssertEqual(asyncStore.items.count, 4) } - + @MainActor func testReadingPersistedItems() async throws { try await asyncStore.insert(.allItems) - + // The new store has to fetch items from disk. let newStore = try await Store( storage: SQLiteStorageEngine.default(appendingPath: "Tests"), cacheIdentifier: \.merchantID) - + XCTAssertEqual(newStore.items[0], .coat) XCTAssertEqual(newStore.items[1], .sweater) XCTAssertEqual(newStore.items[2], .purse) XCTAssertEqual(newStore.items[3], .belt) - + XCTAssertEqual(newStore.items.count, 4) } - + @MainActor func testRemovingItems() async throws { try await asyncStore.insert(.allItems) try await asyncStore.remove(.coat) - + XCTAssertFalse(asyncStore.items.contains(.coat)) - + XCTAssertTrue(asyncStore.items.contains(.sweater)) XCTAssertTrue(asyncStore.items.contains(.purse)) - + try await asyncStore.remove([.sweater, .purse]) XCTAssertFalse(asyncStore.items.contains(.sweater)) XCTAssertFalse(asyncStore.items.contains(.purse)) } - + @MainActor func testRemoveAll() async throws { try await asyncStore.insert(.coat) XCTAssertEqual(asyncStore.items.count, 1) try await asyncStore.removeAll() - + try await asyncStore.insert(.uniqueItems) XCTAssertEqual(asyncStore.items.count, 4) try await asyncStore.removeAll() XCTAssertTrue(asyncStore.items.isEmpty) } - + @MainActor func testChainingInsertOperations() async throws { try await asyncStore.insert(.uniqueItems) - + try await asyncStore .remove(.coat) .insert(.belt) .insert(.belt) .run() - + XCTAssertEqual(asyncStore.items.count, 3) XCTAssertTrue(asyncStore.items.contains(.sweater)) XCTAssertTrue(asyncStore.items.contains(.purse)) XCTAssertTrue(asyncStore.items.contains(.belt)) XCTAssertFalse(asyncStore.items.contains(.coat)) - + try await asyncStore.removeAll() - + try await asyncStore .insert(.belt) .insert(.coat) .remove([.belt]) .insert(.sweater) .run() - + XCTAssertEqual(asyncStore.items.count, 2) XCTAssertTrue(asyncStore.items.contains(.coat)) XCTAssertTrue(asyncStore.items.contains(.sweater)) XCTAssertFalse(asyncStore.items.contains(.belt)) - + + try await asyncStore.removeAll() + try await asyncStore .insert(.belt) .insert(.coat) .insert(.purse) .remove([.belt, .coat]) + .insert([.sweater]) + .run() + + XCTAssertEqual(asyncStore.items.count, 2) + XCTAssertTrue(asyncStore.items.contains(.sweater)) + XCTAssertTrue(asyncStore.items.contains(.purse)) + XCTAssertFalse(asyncStore.items.contains(.coat)) + XCTAssertFalse(asyncStore.items.contains(.belt)) + + try await asyncStore.removeAll() + + try await asyncStore + .insert(.belt) + .insert(.coat) + .insert(.purse) + .remove(.belt) + .remove(.coat) .insert(.sweater) .run() - + XCTAssertEqual(asyncStore.items.count, 2) XCTAssertTrue(asyncStore.items.contains(.sweater)) XCTAssertTrue(asyncStore.items.contains(.purse)) XCTAssertFalse(asyncStore.items.contains(.coat)) XCTAssertFalse(asyncStore.items.contains(.belt)) - + try await asyncStore.removeAll() - + try await asyncStore .insert(.coat) .insert([.purse, .belt]) .run() - + XCTAssertEqual(asyncStore.items.count, 3) XCTAssertTrue(asyncStore.items.contains(.purse)) XCTAssertTrue(asyncStore.items.contains(.belt)) XCTAssertTrue(asyncStore.items.contains(.coat)) + + try await asyncStore.removeAll() + + try await asyncStore + .insert(.coat) + .insert([.purse, .belt]) + .remove(.purse) + .run() + + XCTAssertEqual(asyncStore.items.count, 2) + XCTAssertFalse(asyncStore.items.contains(.purse)) + XCTAssertTrue(asyncStore.items.contains(.belt)) + XCTAssertTrue(asyncStore.items.contains(.coat)) + + try await asyncStore.removeAll() + + try await asyncStore + .insert([.coat]) + .remove(.coat) + .insert([.purse, .belt]) + .remove(.purse) + .run() + + XCTAssertEqual(asyncStore.items.count, 1) + XCTAssertFalse(asyncStore.items.contains(.purse)) + XCTAssertTrue(asyncStore.items.contains(.belt)) + XCTAssertFalse(asyncStore.items.contains(.coat)) + + try await asyncStore.removeAll() + + try await asyncStore + .insert([.coat]) + .remove(.coat) + .insert([.purse, .belt]) + .removeAll() + .run() + + XCTAssertEqual(asyncStore.items.count, 0) + XCTAssertFalse(asyncStore.items.contains(.purse)) + XCTAssertFalse(asyncStore.items.contains(.belt)) + XCTAssertFalse(asyncStore.items.contains(.coat)) + + try await asyncStore + .insert([.coat]) + .removeAll() + .insert([.purse, .belt]) + .run() + + XCTAssertEqual(asyncStore.items.count, 2) + XCTAssertTrue(asyncStore.items.contains(.purse)) + XCTAssertTrue(asyncStore.items.contains(.belt)) + XCTAssertFalse(asyncStore.items.contains(.coat)) } - + @MainActor func testChainingRemoveOperations() async throws { try await asyncStore @@ -163,61 +233,61 @@ final class AsyncStoreTests: XCTestCase { .remove(.belt) .remove(.purse) .run() - + XCTAssertEqual(asyncStore.items.count, 2) XCTAssertTrue(asyncStore.items.contains(.sweater)) XCTAssertTrue(asyncStore.items.contains(.coat)) - + try await asyncStore.insert(.uniqueItems) XCTAssertEqual(asyncStore.items.count, 4) - + try await asyncStore .remove([.sweater, .coat]) .remove(.belt) .run() - + XCTAssertEqual(asyncStore.items.count, 1) XCTAssertTrue(asyncStore.items.contains(.purse)) - + try await asyncStore .removeAll() .insert(.belt) .run() - + XCTAssertEqual(asyncStore.items.count, 1) XCTAssertTrue(asyncStore.items.contains(.belt)) - + try await asyncStore .removeAll() .remove(.belt) .insert(.belt) .run() - + XCTAssertEqual(asyncStore.items.count, 1) XCTAssertTrue(asyncStore.items.contains(.belt)) } - + @MainActor func testChainingOperationsDontExecuteUnlessRun() async throws { let operation = try await asyncStore .insert(.coat) .insert([.purse, .belt]) - + XCTAssertEqual(asyncStore.items.count, 0) XCTAssertFalse(asyncStore.items.contains(.purse)) XCTAssertFalse(asyncStore.items.contains(.belt)) XCTAssertFalse(asyncStore.items.contains(.coat)) - + // Adding this line to get rid of the error about // `operation` being unused, given that's the point of the test. _ = operation } - + @MainActor func testPublishedItemsSubscription() async throws { - let uniqueItems = [BoutiqueItem].uniqueItems + let uniqueItems: [BoutiqueItem] = .uniqueItems let expectation = XCTestExpectation(description: "uniqueItems is published and read") - + asyncStore.$items .dropFirst() .sink(receiveValue: { items in @@ -225,12 +295,11 @@ final class AsyncStoreTests: XCTestCase { expectation.fulfill() }) .store(in: &cancellables) - + XCTAssertTrue(asyncStore.items.isEmpty) - + // Sets items under the hood try await asyncStore.insert(uniqueItems) wait(for: [expectation], timeout: 1) } - } diff --git a/Tests/BoutiqueTests/BoutiqueItem.swift b/Tests/BoutiqueTests/BoutiqueItem.swift index 4299030..02c2b5d 100644 --- a/Tests/BoutiqueTests/BoutiqueItem.swift +++ b/Tests/BoutiqueTests/BoutiqueItem.swift @@ -37,18 +37,18 @@ extension BoutiqueItem { } extension [BoutiqueItem] { - static let allItems = [ - BoutiqueItem.coat, - BoutiqueItem.sweater, - BoutiqueItem.purse, - BoutiqueItem.belt, - BoutiqueItem.duplicateBelt + static let allItems: [BoutiqueItem] = [ + .coat, + .sweater, + .purse, + .belt, + .duplicateBelt ] - static let uniqueItems = [ - BoutiqueItem.coat, - BoutiqueItem.sweater, - BoutiqueItem.purse, - BoutiqueItem.belt, + static let uniqueItems: [BoutiqueItem] = [ + .coat, + .sweater, + .purse, + .belt, ] } diff --git a/Tests/BoutiqueTests/SecurelyStoredValueTests.swift b/Tests/BoutiqueTests/SecurelyStoredValueTests.swift index 8ae0992..7ca2fb5 100644 --- a/Tests/BoutiqueTests/SecurelyStoredValueTests.swift +++ b/Tests/BoutiqueTests/SecurelyStoredValueTests.swift @@ -123,11 +123,11 @@ final class SecurelyStoredValueTests: XCTestCase { func testStoredDictionary() async throws { XCTAssertEqual(self.storedDictionary, nil) - try await self.$storedDictionary.update(key: BoutiqueItem.sweater.merchantID, value: BoutiqueItem.sweater) - XCTAssertEqual(self.storedDictionary, [BoutiqueItem.sweater.merchantID : BoutiqueItem.sweater]) + try await self.$storedDictionary.update(key: BoutiqueItem.sweater.merchantID, value: .sweater) + XCTAssertEqual(self.storedDictionary, [BoutiqueItem.sweater.merchantID : .sweater]) try await self.$storedDictionary.update(key: BoutiqueItem.belt.merchantID, value: nil) - XCTAssertEqual(self.storedDictionary, [BoutiqueItem.sweater.merchantID : BoutiqueItem.sweater]) + XCTAssertEqual(self.storedDictionary, [BoutiqueItem.sweater.merchantID : .sweater]) try await self.$storedDictionary.update(key: BoutiqueItem.sweater.merchantID, value: nil) XCTAssertEqual(self.storedDictionary, [:]) diff --git a/Tests/BoutiqueTests/StoreTests.swift b/Tests/BoutiqueTests/StoreTests.swift index 6a4069f..b708082 100644 --- a/Tests/BoutiqueTests/StoreTests.swift +++ b/Tests/BoutiqueTests/StoreTests.swift @@ -15,11 +15,11 @@ final class StoreTests: XCTestCase { storage: SQLiteStorageEngine.default(appendingPath: "Tests"), cacheIdentifier: \.merchantID) } - + store = makeNonAsyncStore() try await store.removeAll() } - + override func tearDown() { cancellables.removeAll() } @@ -64,12 +64,12 @@ final class StoreTests: XCTestCase { @MainActor func testReadingPersistedItems() async throws { try await store.insert(.allItems) - + // The new store has to fetch items from disk. let newStore = try await Store( storage: SQLiteStorageEngine.default(appendingPath: "Tests"), cacheIdentifier: \.merchantID) - + XCTAssertEqual(newStore.items[0], .coat) XCTAssertEqual(newStore.items[1], .sweater) XCTAssertEqual(newStore.items[2], .purse) @@ -135,11 +135,30 @@ final class StoreTests: XCTestCase { XCTAssertTrue(store.items.contains(.sweater)) XCTAssertFalse(store.items.contains(.belt)) + try await store.removeAll() + try await store .insert(.belt) .insert(.coat) .insert(.purse) .remove([.belt, .coat]) + .insert([.sweater]) + .run() + + XCTAssertEqual(store.items.count, 2) + XCTAssertTrue(store.items.contains(.sweater)) + XCTAssertTrue(store.items.contains(.purse)) + XCTAssertFalse(store.items.contains(.coat)) + XCTAssertFalse(store.items.contains(.belt)) + + try await store.removeAll() + + try await store + .insert(.belt) + .insert(.coat) + .insert(.purse) + .remove(.belt) + .remove(.coat) .insert(.sweater) .run() @@ -160,6 +179,58 @@ final class StoreTests: XCTestCase { XCTAssertTrue(store.items.contains(.purse)) XCTAssertTrue(store.items.contains(.belt)) XCTAssertTrue(store.items.contains(.coat)) + + try await store.removeAll() + + try await store + .insert(.coat) + .insert([.purse, .belt]) + .remove(.purse) + .run() + + XCTAssertEqual(store.items.count, 2) + XCTAssertFalse(store.items.contains(.purse)) + XCTAssertTrue(store.items.contains(.belt)) + XCTAssertTrue(store.items.contains(.coat)) + + try await store.removeAll() + + try await store + .insert([.coat]) + .remove(.coat) + .insert([.purse, .belt]) + .remove(.purse) + .run() + + XCTAssertEqual(store.items.count, 1) + XCTAssertFalse(store.items.contains(.purse)) + XCTAssertTrue(store.items.contains(.belt)) + XCTAssertFalse(store.items.contains(.coat)) + + try await store.removeAll() + + try await store + .insert([.coat]) + .remove(.coat) + .insert([.purse, .belt]) + .removeAll() + .run() + + XCTAssertEqual(store.items.count, 0) + XCTAssertFalse(store.items.contains(.purse)) + XCTAssertFalse(store.items.contains(.belt)) + XCTAssertFalse(store.items.contains(.coat)) + + try await store + .insert([.coat]) + .removeAll() + .insert([.purse, .belt]) + .run() + + XCTAssertEqual(store.items.count, 2) + XCTAssertTrue(store.items.contains(.purse)) + XCTAssertTrue(store.items.contains(.belt)) + XCTAssertFalse(store.items.contains(.coat)) } @MainActor @@ -221,7 +292,7 @@ final class StoreTests: XCTestCase { @MainActor func testPublishedItemsSubscription() async throws { - let uniqueItems = [BoutiqueItem].uniqueItems + let uniqueItems: [BoutiqueItem] = .uniqueItems let expectation = XCTestExpectation(description: "uniqueItems is published and read") store.$items @@ -239,3 +310,4 @@ final class StoreTests: XCTestCase { wait(for: [expectation], timeout: 1) } } + diff --git a/Tests/BoutiqueTests/StoredTests.swift b/Tests/BoutiqueTests/StoredTests.swift index a7019ca..0626fb2 100644 --- a/Tests/BoutiqueTests/StoredTests.swift +++ b/Tests/BoutiqueTests/StoredTests.swift @@ -133,11 +133,30 @@ final class StoredTests: XCTestCase { XCTAssertTrue(items.contains(.sweater)) XCTAssertFalse(items.contains(.belt)) + try await $items.removeAll() + try await $items .insert(.belt) .insert(.coat) .insert(.purse) .remove([.belt, .coat]) + .insert([.sweater]) + .run() + + XCTAssertEqual(items.count, 2) + XCTAssertTrue(items.contains(.sweater)) + XCTAssertTrue(items.contains(.purse)) + XCTAssertFalse(items.contains(.coat)) + XCTAssertFalse(items.contains(.belt)) + + try await $items.removeAll() + + try await $items + .insert(.belt) + .insert(.coat) + .insert(.purse) + .remove(.belt) + .remove(.coat) .insert(.sweater) .run() @@ -158,6 +177,58 @@ final class StoredTests: XCTestCase { XCTAssertTrue(items.contains(.purse)) XCTAssertTrue(items.contains(.belt)) XCTAssertTrue(items.contains(.coat)) + + try await $items.removeAll() + + try await $items + .insert(.coat) + .insert([.purse, .belt]) + .remove(.purse) + .run() + + XCTAssertEqual(items.count, 2) + XCTAssertFalse(items.contains(.purse)) + XCTAssertTrue(items.contains(.belt)) + XCTAssertTrue(items.contains(.coat)) + + try await $items.removeAll() + + try await $items + .insert([.coat]) + .remove(.coat) + .insert([.purse, .belt]) + .remove(.purse) + .run() + + XCTAssertEqual(items.count, 1) + XCTAssertFalse(items.contains(.purse)) + XCTAssertTrue(items.contains(.belt)) + XCTAssertFalse(items.contains(.coat)) + + try await $items.removeAll() + + try await $items + .insert([.coat]) + .remove(.coat) + .insert([.purse, .belt]) + .removeAll() + .run() + + XCTAssertEqual(items.count, 0) + XCTAssertFalse(items.contains(.purse)) + XCTAssertFalse(items.contains(.belt)) + XCTAssertFalse(items.contains(.coat)) + + try await $items + .insert([.coat]) + .removeAll() + .insert([.purse, .belt]) + .run() + + XCTAssertEqual(items.count, 2) + XCTAssertTrue(items.contains(.purse)) + XCTAssertTrue(items.contains(.belt)) + XCTAssertFalse(items.contains(.coat)) } @MainActor @@ -237,4 +308,3 @@ final class StoredTests: XCTestCase { wait(for: [expectation], timeout: 1) } } - diff --git a/Tests/BoutiqueTests/StoredValueTests.swift b/Tests/BoutiqueTests/StoredValueTests.swift index 3fc8360..ecfc280 100644 --- a/Tests/BoutiqueTests/StoredValueTests.swift +++ b/Tests/BoutiqueTests/StoredValueTests.swift @@ -7,7 +7,7 @@ final class StoredValueTests: XCTestCase { private var cancellables: Set = [] @StoredValue(key: "storedItem") - private var storedItem = BoutiqueItem.coat + private var storedItem = .coat @StoredValue(key: "storedNilValue") private var storedNilValue = nil @@ -90,11 +90,11 @@ final class StoredValueTests: XCTestCase { func testStoredDictionaryValueUpdate() async throws { XCTAssertEqual(self.storedDictionaryValue, [:]) - await self.$storedDictionaryValue.update(key: BoutiqueItem.sweater.merchantID, value: BoutiqueItem.sweater) - XCTAssertEqual(self.storedDictionaryValue, [BoutiqueItem.sweater.merchantID : BoutiqueItem.sweater]) + await self.$storedDictionaryValue.update(key: BoutiqueItem.sweater.merchantID, value: .sweater) + XCTAssertEqual(self.storedDictionaryValue, [BoutiqueItem.sweater.merchantID : .sweater]) await self.$storedDictionaryValue.update(key: BoutiqueItem.belt.merchantID, value: nil) - XCTAssertEqual(self.storedDictionaryValue, [BoutiqueItem.sweater.merchantID : BoutiqueItem.sweater]) + XCTAssertEqual(self.storedDictionaryValue, [BoutiqueItem.sweater.merchantID : .sweater]) await self.$storedDictionaryValue.update(key: BoutiqueItem.sweater.merchantID, value: nil) XCTAssertEqual(self.storedDictionaryValue, [:])