diff --git a/Sources/YMFF/FeatureFlagResolver/FeatureFlagResolver+Error.swift b/Sources/YMFF/FeatureFlagResolver/FeatureFlagResolver+Error.swift index fa2ac8e..03ac538 100644 --- a/Sources/YMFF/FeatureFlagResolver/FeatureFlagResolver+Error.swift +++ b/Sources/YMFF/FeatureFlagResolver/FeatureFlagResolver+Error.swift @@ -20,14 +20,14 @@ extension FeatureFlagResolver { /// No feature-flag store contains a value for the given key. case valueNotFoundInStores(key: String) + /// The feature-flag store has thrown an error. + case storeError(any Swift.Error) + /// Currently, optional values are not supported by the `FeatureFlagResolver`. /// /// - Note: Support for optional values will be added in [#130](https://github.com/yakovmanshin/YMFF/issues/130). case optionalValuesNotAllowed - /// The types of the old and new values don’t match. - case typeMismatch - } } diff --git a/Sources/YMFF/FeatureFlagResolver/FeatureFlagResolver.swift b/Sources/YMFF/FeatureFlagResolver/FeatureFlagResolver.swift index 3bab614..b812bd0 100644 --- a/Sources/YMFF/FeatureFlagResolver/FeatureFlagResolver.swift +++ b/Sources/YMFF/FeatureFlagResolver/FeatureFlagResolver.swift @@ -42,7 +42,7 @@ final public class FeatureFlagResolver { let mutableStores = getMutableStores() Task { [mutableStores] in for store in mutableStores { - await store.saveChanges() + try? await store.saveChanges() } } } @@ -68,7 +68,11 @@ extension FeatureFlagResolver: FeatureFlagResolverProtocol { try await validateOverrideValue(newValue, forKey: key) - await mutableStores[0].setValue(newValue, forKey: key) + do { + try await mutableStores[0].setValue(newValue, forKey: key) + } catch { + throw Error.storeError(error) + } } public func removeValueFromMutableStore(using key: FeatureFlagKey) async throws { @@ -77,7 +81,11 @@ extension FeatureFlagResolver: FeatureFlagResolverProtocol { throw Error.noStoreAvailable } - await mutableStores[0].removeValue(forKey: key) + do { + try await mutableStores[0].removeValue(forKey: key) + } catch { + throw Error.storeError(error) + } } } @@ -101,7 +109,11 @@ extension FeatureFlagResolver: SynchronousFeatureFlagResolverProtocol { try validateOverrideValueSync(newValue, forKey: key) - syncMutableStores[0].setValueSync(newValue, forKey: key) + do { + try syncMutableStores[0].setValueSync(newValue, forKey: key) + } catch { + throw Error.storeError(error) + } } public func removeValueFromMutableStoreSync(using key: FeatureFlagKey) throws { @@ -110,7 +122,11 @@ extension FeatureFlagResolver: SynchronousFeatureFlagResolverProtocol { throw Error.noStoreAvailable } - syncMutableStores[0].removeValueSync(forKey: key) + do { + try syncMutableStores[0].removeValueSync(forKey: key) + } catch { + throw Error.storeError(error) + } } } @@ -149,10 +165,12 @@ extension FeatureFlagResolver { for store in matchingStores { if await store.containsValue(forKey: key) { - guard let value: Value = await store.value(forKey: key) - else { throw Error.typeMismatch } - - return value + do { + let value: Value = try await store.value(forKey: key) + return value + } catch { + throw Error.storeError(error) + } } } @@ -167,10 +185,12 @@ extension FeatureFlagResolver { for store in matchingStores { if store.containsValueSync(forKey: key) { - guard let value: Value = store.valueSync(forKey: key) - else { throw Error.typeMismatch } - - return value + do { + let value: Value = try store.valueSync(forKey: key) + return value + } catch { + throw Error.storeError(error) + } } } diff --git a/Sources/YMFF/FeatureFlagResolver/Store/CommonFeatureFlagStoreError.swift b/Sources/YMFF/FeatureFlagResolver/Store/CommonFeatureFlagStoreError.swift new file mode 100644 index 0000000..e38f625 --- /dev/null +++ b/Sources/YMFF/FeatureFlagResolver/Store/CommonFeatureFlagStoreError.swift @@ -0,0 +1,12 @@ +// +// CommonFeatureFlagStoreError.swift +// YMFF +// +// Created by Yakov Manshin on 5/7/24. +// Copyright © 2024 Yakov Manshin. See the LICENSE file for license info. +// + +enum CommonFeatureFlagStoreError: Error { + case valueNotFound(key: String) + case typeMismatch +} diff --git a/Sources/YMFF/FeatureFlagResolver/Store/RuntimeOverridesStore.swift b/Sources/YMFF/FeatureFlagResolver/Store/RuntimeOverridesStore.swift index a1b9386..7d77847 100644 --- a/Sources/YMFF/FeatureFlagResolver/Store/RuntimeOverridesStore.swift +++ b/Sources/YMFF/FeatureFlagResolver/Store/RuntimeOverridesStore.swift @@ -31,8 +31,10 @@ extension RuntimeOverridesStore: SynchronousMutableFeatureFlagStore { store[key] != nil } - public func valueSync(forKey key: String) -> Value? { - store[key] as? Value + public func valueSync(forKey key: String) throws -> Value { + guard let anyValue = store[key] else { throw CommonFeatureFlagStoreError.valueNotFound(key: key) } + guard let value = anyValue as? Value else { throw CommonFeatureFlagStoreError.typeMismatch } + return value } public func setValueSync(_ value: Value, forKey key: String) { diff --git a/Sources/YMFF/FeatureFlagResolver/Store/TransparentFeatureFlagStore.swift b/Sources/YMFF/FeatureFlagResolver/Store/TransparentFeatureFlagStore.swift index 6e62bc1..a2b0cf9 100644 --- a/Sources/YMFF/FeatureFlagResolver/Store/TransparentFeatureFlagStore.swift +++ b/Sources/YMFF/FeatureFlagResolver/Store/TransparentFeatureFlagStore.swift @@ -23,8 +23,10 @@ extension TransparentFeatureFlagStore: SynchronousFeatureFlagStore, FeatureFlagS self[key] != nil } - public func valueSync(forKey key: String) -> V? { - self[key] as? V + public func valueSync(forKey key: String) throws -> V { + guard let anyValue = self[key] else { throw CommonFeatureFlagStoreError.valueNotFound(key: key) } + guard let value = anyValue as? V else { throw CommonFeatureFlagStoreError.typeMismatch } + return value } } diff --git a/Sources/YMFF/FeatureFlagResolver/Store/UserDefaultsStore.swift b/Sources/YMFF/FeatureFlagResolver/Store/UserDefaultsStore.swift index 241f47f..ff54ee9 100644 --- a/Sources/YMFF/FeatureFlagResolver/Store/UserDefaultsStore.swift +++ b/Sources/YMFF/FeatureFlagResolver/Store/UserDefaultsStore.swift @@ -38,8 +38,12 @@ extension UserDefaultsStore: SynchronousMutableFeatureFlagStore { userDefaults.object(forKey: key) != nil } - public func valueSync(forKey key: String) -> Value? { - userDefaults.object(forKey: key) as? Value + public func valueSync(forKey key: String) throws -> Value { + guard let anyValue = userDefaults.object(forKey: key) else { + throw CommonFeatureFlagStoreError.valueNotFound(key: key) + } + guard let value = anyValue as? Value else { throw CommonFeatureFlagStoreError.typeMismatch } + return value } public func setValueSync(_ value: Value, forKey key: String) { diff --git a/Sources/YMFFProtocols/FeatureFlagResolver/Store/FeatureFlagStore.swift b/Sources/YMFFProtocols/FeatureFlagResolver/Store/FeatureFlagStore.swift index df25ede..e24dc94 100644 --- a/Sources/YMFFProtocols/FeatureFlagResolver/Store/FeatureFlagStore.swift +++ b/Sources/YMFFProtocols/FeatureFlagResolver/Store/FeatureFlagStore.swift @@ -17,6 +17,6 @@ public protocol FeatureFlagStore { /// Retrieves a feature flag value by its key. /// /// - Parameter key: *Required.* The key that points to a feature flag value in the store. - func value(forKey key: String) async -> Value? + func value(forKey key: String) async throws -> Value } diff --git a/Sources/YMFFProtocols/FeatureFlagResolver/Store/MutableFeatureFlagStore.swift b/Sources/YMFFProtocols/FeatureFlagResolver/Store/MutableFeatureFlagStore.swift index 83107c6..3713b7b 100644 --- a/Sources/YMFFProtocols/FeatureFlagResolver/Store/MutableFeatureFlagStore.swift +++ b/Sources/YMFFProtocols/FeatureFlagResolver/Store/MutableFeatureFlagStore.swift @@ -14,17 +14,17 @@ public protocol MutableFeatureFlagStore: AnyObject, FeatureFlagStore { /// - Parameters: /// - value: *Required.* The value to record. /// - key: *Required.* The key used to address the value. - func setValue(_ value: Value, forKey key: String) async + func setValue(_ value: Value, forKey key: String) async throws /// Removes the value from the store. /// /// - Parameter key: *Required.* The key used to address the value. - func removeValue(forKey key: String) async + func removeValue(forKey key: String) async throws /// Immediately saves changed values so they’re not lost. /// /// + This method can be called when work with the feature flag store is finished. - func saveChanges() async + func saveChanges() async throws } @@ -33,6 +33,6 @@ public protocol MutableFeatureFlagStore: AnyObject, FeatureFlagStore { extension MutableFeatureFlagStore { // Not all kinds of feature flag stores need this method, so it’s optional to implement. - public func saveChanges() async { } + public func saveChanges() async throws { } } diff --git a/Sources/YMFFProtocols/FeatureFlagResolver/Store/SynchronousFeatureFlagStore.swift b/Sources/YMFFProtocols/FeatureFlagResolver/Store/SynchronousFeatureFlagStore.swift index a57cd1c..3fbd1e7 100644 --- a/Sources/YMFFProtocols/FeatureFlagResolver/Store/SynchronousFeatureFlagStore.swift +++ b/Sources/YMFFProtocols/FeatureFlagResolver/Store/SynchronousFeatureFlagStore.swift @@ -16,7 +16,7 @@ public protocol SynchronousFeatureFlagStore: FeatureFlagStore { /// Retrieves a feature flag value by its key. /// /// - Parameter key: *Required.* The key that points to a feature flag value in the store. - func valueSync(forKey key: String) -> Value? + func valueSync(forKey key: String) throws -> Value } @@ -28,8 +28,8 @@ extension SynchronousFeatureFlagStore { containsValueSync(forKey: key) } - public func value(forKey key: String) async -> Value? { - valueSync(forKey: key) + public func value(forKey key: String) async throws -> Value { + try valueSync(forKey: key) } } diff --git a/Sources/YMFFProtocols/FeatureFlagResolver/Store/SynchronousMutableFeatureFlagStore.swift b/Sources/YMFFProtocols/FeatureFlagResolver/Store/SynchronousMutableFeatureFlagStore.swift index 9380875..02619ce 100644 --- a/Sources/YMFFProtocols/FeatureFlagResolver/Store/SynchronousMutableFeatureFlagStore.swift +++ b/Sources/YMFFProtocols/FeatureFlagResolver/Store/SynchronousMutableFeatureFlagStore.swift @@ -13,17 +13,17 @@ public protocol SynchronousMutableFeatureFlagStore: SynchronousFeatureFlagStore, /// - Parameters: /// - value: *Required.* The value to record. /// - key: *Required.* The key used to address the value. - func setValueSync(_ value: Value, forKey key: String) + func setValueSync(_ value: Value, forKey key: String) throws /// Removes the value from the store. /// /// - Parameter key: *Required.* The key used to address the value. - func removeValueSync(forKey key: String) + func removeValueSync(forKey key: String) throws /// Immediately saves changed values so they’re not lost. /// /// + This method can be called when work with the feature flag store is finished. - func saveChangesSync() + func saveChangesSync() throws } @@ -31,16 +31,16 @@ public protocol SynchronousMutableFeatureFlagStore: SynchronousFeatureFlagStore, extension SynchronousMutableFeatureFlagStore { - public func setValue(_ value: Value, forKey key: String) async { - setValueSync(value, forKey: key) + public func setValue(_ value: Value, forKey key: String) async throws { + try setValueSync(value, forKey: key) } - public func removeValue(forKey key: String) async { - removeValueSync(forKey: key) + public func removeValue(forKey key: String) async throws { + try removeValueSync(forKey: key) } - public func saveChanges() async { - saveChangesSync() + public func saveChanges() async throws { + try saveChangesSync() } } @@ -49,6 +49,6 @@ extension SynchronousMutableFeatureFlagStore { extension SynchronousMutableFeatureFlagStore { - public func saveChangesSync() { } + public func saveChangesSync() throws { } } diff --git a/Tests/YMFFTests/Cases/FeatureFlagResolverTests.swift b/Tests/YMFFTests/Cases/FeatureFlagResolverTests.swift index 7927791..ab2f397 100644 --- a/Tests/YMFFTests/Cases/FeatureFlagResolverTests.swift +++ b/Tests/YMFFTests/Cases/FeatureFlagResolverTests.swift @@ -137,7 +137,7 @@ final class FeatureFlagResolverTests: XCTestCase { configuration.stores = [store1, store2] store1.containsValue_returnValue = false store2.containsValue_returnValue = true - store2.value_returnValue = "TEST_value2" + store2.value_result = .success("TEST_value2") do { let value: String = try await resolver.value(for: "TEST_key1") @@ -161,7 +161,7 @@ final class FeatureFlagResolverTests: XCTestCase { configuration.stores = [store1, store2] store1.containsValueSync_returnValue = false store2.containsValueSync_returnValue = true - store2.valueSync_returnValue = "TEST_value2" + store2.valueSync_result = .success("TEST_value2") do { let value: String = try resolver.valueSync(for: "TEST_key1") @@ -186,9 +186,9 @@ final class FeatureFlagResolverTests: XCTestCase { configuration.stores = [store1, store2, store3] store1.containsValue_returnValue = false store2.containsValue_returnValue = true - store2.value_returnValue = "TEST_value2" + store2.value_result = .success("TEST_value2") store3.containsValue_returnValue = true - store3.value_returnValue = "TEST_value3" + store3.value_result = .success("TEST_value3") do { let value: String = try await resolver.value(for: "TEST_key1") @@ -213,9 +213,9 @@ final class FeatureFlagResolverTests: XCTestCase { configuration.stores = [store1, store2, store3] store1.containsValueSync_returnValue = false store2.containsValueSync_returnValue = true - store2.valueSync_returnValue = "TEST_value2" + store2.valueSync_result = .success("TEST_value2") store3.containsValueSync_returnValue = true - store3.valueSync_returnValue = "TEST_value3" + store3.valueSync_result = .success("TEST_value3") do { let value: String = try resolver.valueSync(for: "TEST_key1") @@ -237,12 +237,13 @@ final class FeatureFlagResolverTests: XCTestCase { let store = FeatureFlagStoreMock() configuration.stores = [store] store.containsValue_returnValue = true - store.value_returnValue = 123 + store.value_result = .success(123) do { let _: String = try await resolver.value(for: "TEST_key1") XCTFail("Expected an error") - } catch FeatureFlagResolver.Error.typeMismatch { + } catch FeatureFlagResolver.Error.storeError(let error) { + XCTAssertEqual(error as? TestFeatureFlagStoreError, .typeMismatch) XCTAssertEqual(store.containsValue_invocationCount, 1) XCTAssertEqual(store.containsValue_keys, ["TEST_key1"]) XCTAssertEqual(store.value_invocationCount, 1) @@ -256,12 +257,13 @@ final class FeatureFlagResolverTests: XCTestCase { let store = SynchronousFeatureFlagStoreMock() configuration.stores = [store] store.containsValueSync_returnValue = true - store.valueSync_returnValue = 123 + store.valueSync_result = .success(123) do { let _: String = try resolver.valueSync(for: "TEST_key1") XCTFail("Expected an error") - } catch FeatureFlagResolver.Error.typeMismatch { + } catch FeatureFlagResolver.Error.storeError(let error) { + XCTAssertEqual(error as? TestFeatureFlagStoreError, .typeMismatch) XCTAssertEqual(store.containsValueSync_invocationCount, 1) XCTAssertEqual(store.containsValueSync_keys, ["TEST_key1"]) XCTAssertEqual(store.valueSync_invocationCount, 1) @@ -275,7 +277,7 @@ final class FeatureFlagResolverTests: XCTestCase { let store = FeatureFlagStoreMock() configuration.stores = [store] store.containsValue_returnValue = true - store.value_returnValue = 123 as Int? + store.value_result = .success((123 as Int?)!) do { let _: Int? = try await resolver.value(for: "TEST_key1") @@ -294,7 +296,7 @@ final class FeatureFlagResolverTests: XCTestCase { let store = SynchronousFeatureFlagStoreMock() configuration.stores = [store] store.containsValueSync_returnValue = true - store.valueSync_returnValue = 123 as Int? + store.valueSync_result = .success((123 as Int?)!) do { let _: Int? = try resolver.valueSync(for: "TEST_key1") @@ -405,7 +407,7 @@ final class FeatureFlagResolverTests: XCTestCase { let store = MutableFeatureFlagStoreMock() configuration.stores = [store] store.containsValue_returnValue = true - store.value_returnValue = "TEST_value1" + store.value_result = .success("TEST_value1") do { try await resolver.setValue("TEST_value2", toMutableStoreUsing: "TEST_key1") @@ -425,7 +427,7 @@ final class FeatureFlagResolverTests: XCTestCase { let store = SynchronousMutableFeatureFlagStoreMock() configuration.stores = [store] store.containsValueSync_returnValue = true - store.valueSync_returnValue = "TEST_value1" + store.valueSync_result = .success("TEST_value1") do { try resolver.setValueSync("TEST_value2", toMutableStoreUsing: "TEST_key1") @@ -445,7 +447,7 @@ final class FeatureFlagResolverTests: XCTestCase { let store = MutableFeatureFlagStoreMock() configuration.stores = [store] store.containsValue_returnValue = true - store.value_returnValue = "TEST_value1" + store.value_result = .success("TEST_value1") do { try await resolver.setValue(456 as Int?, toMutableStoreUsing: "TEST_key1") @@ -464,7 +466,7 @@ final class FeatureFlagResolverTests: XCTestCase { let store = SynchronousMutableFeatureFlagStoreMock() configuration.stores = [store] store.containsValueSync_returnValue = true - store.valueSync_returnValue = "TEST_value1" + store.valueSync_result = .success("TEST_value1") do { try resolver.setValueSync(456 as Int?, toMutableStoreUsing: "TEST_key1") @@ -483,12 +485,13 @@ final class FeatureFlagResolverTests: XCTestCase { let store = MutableFeatureFlagStoreMock() configuration.stores = [store] store.containsValue_returnValue = true - store.value_returnValue = "TEST_value1" + store.value_result = .success("TEST_value1") do { try await resolver.setValue(456, toMutableStoreUsing: "TEST_key1") XCTFail("Expected an error") - } catch FeatureFlagResolver.Error.typeMismatch { + } catch FeatureFlagResolver.Error.storeError(let error) { + XCTAssertEqual(error as? TestFeatureFlagStoreError, .typeMismatch) XCTAssertEqual(store.containsValue_invocationCount, 1) XCTAssertEqual(store.containsValue_keys, ["TEST_key1"]) XCTAssertEqual(store.setValue_invocationCount, 0) @@ -502,12 +505,13 @@ final class FeatureFlagResolverTests: XCTestCase { let store = SynchronousMutableFeatureFlagStoreMock() configuration.stores = [store] store.containsValueSync_returnValue = true - store.valueSync_returnValue = "TEST_value1" + store.valueSync_result = .success("TEST_value1") do { try resolver.setValueSync(456, toMutableStoreUsing: "TEST_key1") XCTFail("Expected an error") - } catch FeatureFlagResolver.Error.typeMismatch { + } catch FeatureFlagResolver.Error.storeError(let error) { + XCTAssertEqual(error as? TestFeatureFlagStoreError, .typeMismatch) XCTAssertEqual(store.containsValueSync_invocationCount, 1) XCTAssertEqual(store.containsValueSync_keys, ["TEST_key1"]) XCTAssertEqual(store.setValueSync_invocationCount, 0) @@ -525,9 +529,9 @@ final class FeatureFlagResolverTests: XCTestCase { store1.containsValue_returnValue = true store2.containsValue_returnValue = true store3.containsValueSync_returnValue = true - store1.value_returnValue = "TEST_value1" - store2.value_returnValue = "TEST_value2" - store3.valueSync_returnValue = "TEST_value3" + store1.value_result = .success("TEST_value1") + store2.value_result = .success("TEST_value2") + store3.valueSync_result = .success("TEST_value3") do { try await resolver.setValue("TEST_value4", toMutableStoreUsing: "TEST_key1") @@ -557,9 +561,9 @@ final class FeatureFlagResolverTests: XCTestCase { store1.containsValueSync_returnValue = true store2.containsValueSync_returnValue = true store3.containsValueSync_returnValue = true - store1.valueSync_returnValue = "TEST_value1" - store2.valueSync_returnValue = "TEST_value2" - store3.valueSync_returnValue = "TEST_value3" + store1.valueSync_result = .success("TEST_value1") + store2.valueSync_result = .success("TEST_value2") + store3.valueSync_result = .success("TEST_value3") do { try resolver.setValueSync("TEST_value4", toMutableStoreUsing: "TEST_key1") @@ -581,6 +585,68 @@ final class FeatureFlagResolverTests: XCTestCase { } } + func test_setValue_storeError() async { + let store1 = MutableFeatureFlagStoreMock() + let store2 = SynchronousMutableFeatureFlagStoreMock() + configuration.stores = [store1, store2] + store1.containsValue_returnValue = false + store1.setValue_result = .failure(TestFeatureFlagStoreError.failedToSetValue) + store2.containsValueSync_returnValue = true + store2.valueSync_result = .success("TEST_value1") + store2.setValueSync_result = .success(()) + + do { + try await resolver.setValue("TEST_value2", toMutableStoreUsing: "TEST_key1") + XCTFail("Expected an error") + } catch FeatureFlagResolver.Error.storeError(let error) { + XCTAssertEqual(error as? TestFeatureFlagStoreError, .failedToSetValue) + XCTAssertEqual(store1.containsValue_invocationCount, 1) + XCTAssertEqual(store1.containsValue_keys, ["TEST_key1"]) + XCTAssertEqual(store2.containsValueSync_invocationCount, 1) + XCTAssertEqual(store2.containsValueSync_keys, ["TEST_key1"]) + XCTAssertEqual(store1.value_invocationCount, 0) + XCTAssertEqual(store1.setValue_invocationCount, 1) + XCTAssertEqual(store1.setValue_keyValuePairs.count, 1) + XCTAssertEqual(store1.setValue_keyValuePairs[0].0, "TEST_key1") + XCTAssertEqual(store1.setValue_keyValuePairs[0].1 as? String, "TEST_value2") + XCTAssertEqual(store2.setValueSync_invocationCount, 0) + XCTAssertTrue(store2.setValueSync_keyValuePairs.isEmpty) + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + func test_setValueSync_storeError() { + let store1 = SynchronousMutableFeatureFlagStoreMock() + let store2 = SynchronousMutableFeatureFlagStoreMock() + configuration.stores = [store1, store2] + store1.containsValueSync_returnValue = false + store1.setValueSync_result = .failure(TestFeatureFlagStoreError.failedToSetValue) + store2.containsValueSync_returnValue = true + store2.valueSync_result = .success("TEST_value1") + store2.setValueSync_result = .success(()) + + do { + try resolver.setValueSync("TEST_value2", toMutableStoreUsing: "TEST_key1") + XCTFail("Expected an error") + } catch FeatureFlagResolver.Error.storeError(let error) { + XCTAssertEqual(error as? TestFeatureFlagStoreError, .failedToSetValue) + XCTAssertEqual(store1.containsValueSync_invocationCount, 1) + XCTAssertEqual(store1.containsValueSync_keys, ["TEST_key1"]) + XCTAssertEqual(store2.containsValueSync_invocationCount, 1) + XCTAssertEqual(store2.containsValueSync_keys, ["TEST_key1"]) + XCTAssertEqual(store1.valueSync_invocationCount, 0) + XCTAssertEqual(store1.setValueSync_invocationCount, 1) + XCTAssertEqual(store1.setValueSync_keyValuePairs.count, 1) + XCTAssertEqual(store1.setValueSync_keyValuePairs[0].0, "TEST_key1") + XCTAssertEqual(store1.setValueSync_keyValuePairs[0].1 as? String, "TEST_value2") + XCTAssertEqual(store2.setValueSync_invocationCount, 0) + XCTAssertTrue(store2.setValueSync_keyValuePairs.isEmpty) + } catch { + XCTFail("Unexpected error: \(error)") + } + } + func test_removeValueFromMutableStore_noStores() async { do { try await resolver.removeValueFromMutableStore(using: "TEST_key1") @@ -701,6 +767,48 @@ final class FeatureFlagResolverTests: XCTestCase { } } + func test_removeValueFromMutableStore_storeError() async { + let store1 = MutableFeatureFlagStoreMock() + let store2 = SynchronousMutableFeatureFlagStoreMock() + configuration.stores = [store1, store2] + store1.removeValue_result = .failure(.failedToRemoveValue) + store2.removeValueSync_result = .success(()) + + do { + try await resolver.removeValueFromMutableStore(using: "TEST_key1") + XCTFail("Expected an error") + } catch FeatureFlagResolver.Error.storeError(let error) { + XCTAssertEqual(error as? TestFeatureFlagStoreError, .failedToRemoveValue) + XCTAssertEqual(store1.removeValue_invocationCount, 1) + XCTAssertEqual(store1.removeValue_keys, ["TEST_key1"]) + XCTAssertEqual(store2.removeValueSync_invocationCount, 0) + XCTAssertTrue(store2.removeValueSync_keys.isEmpty) + } catch { + XCTFail("Unexpected error: \(error)") + } + } + + func test_removeValueFromMutableStoreSync_storeError() { + let store1 = SynchronousMutableFeatureFlagStoreMock() + let store2 = SynchronousMutableFeatureFlagStoreMock() + configuration.stores = [store1, store2] + store1.removeValueSync_result = .failure(.failedToRemoveValue) + store2.removeValueSync_result = .success(()) + + do { + try resolver.removeValueFromMutableStoreSync(using: "TEST_key1") + XCTFail("Expected an error") + } catch FeatureFlagResolver.Error.storeError(let error) { + XCTAssertEqual(error as? TestFeatureFlagStoreError, .failedToRemoveValue) + XCTAssertEqual(store1.removeValueSync_invocationCount, 1) + XCTAssertEqual(store1.removeValueSync_keys, ["TEST_key1"]) + XCTAssertEqual(store2.removeValueSync_invocationCount, 0) + XCTAssertTrue(store2.removeValueSync_keys.isEmpty) + } catch { + XCTFail("Unexpected error: \(error)") + } + } + func test_deinit() async throws { let store1 = MutableFeatureFlagStoreMock() let store2 = SynchronousMutableFeatureFlagStoreMock() diff --git a/Tests/YMFFTests/Cases/RuntimeOverridesStoreTests.swift b/Tests/YMFFTests/Cases/RuntimeOverridesStoreTests.swift index 3b98e3a..bfe0ba6 100644 --- a/Tests/YMFFTests/Cases/RuntimeOverridesStoreTests.swift +++ b/Tests/YMFFTests/Cases/RuntimeOverridesStoreTests.swift @@ -43,41 +43,63 @@ final class RuntimeOverridesStoreTests: XCTestCase { XCTAssertFalse(containsValue2) } - func test_value() async { + func test_value() async throws { store.store = [ "TEST_key1": "TEST_value1", "TEST_key2": "TEST_value2", ] - let value1: String? = await store.value(forKey: "TEST_key1") - let value2: Int? = await store.value(forKey: "TEST_key2") - let value3: Bool? = await store.value(forKey: "TEST_key3") - + let value1: String = try await store.value(forKey: "TEST_key1") XCTAssertEqual(value1, "TEST_value1") - XCTAssertNil(value2) - XCTAssertNil(value3) + + do { + let _: Int = try await store.value(forKey: "TEST_key2") + XCTFail("Expected an error") + } catch CommonFeatureFlagStoreError.typeMismatch { } catch { + XCTFail("Unexpected error: \(error)") + } + + do { + let _: Bool = try await store.value(forKey: "TEST_key3") + XCTFail("Expected an error") + } catch CommonFeatureFlagStoreError.valueNotFound(key: let key) { + XCTAssertEqual(key, "TEST_key3") + } catch { + XCTFail("Unexpected error: \(error)") + } } - func test_valueSync() { + func test_valueSync() throws { store.store = [ "TEST_key1": "TEST_value1", "TEST_key2": "TEST_value2", ] - let value1: String? = store.valueSync(forKey: "TEST_key1") - let value2: Int? = store.valueSync(forKey: "TEST_key2") - let value3: Bool? = store.valueSync(forKey: "TEST_key3") - + let value1: String = try store.valueSync(forKey: "TEST_key1") XCTAssertEqual(value1, "TEST_value1") - XCTAssertNil(value2) - XCTAssertNil(value3) + + do { + let _: Int = try store.valueSync(forKey: "TEST_key2") + XCTFail("Expected an error") + } catch CommonFeatureFlagStoreError.typeMismatch { } catch { + XCTFail("Unexpected error: \(error)") + } + + do { + let _: Bool = try store.valueSync(forKey: "TEST_key3") + XCTFail("Expected an error") + } catch CommonFeatureFlagStoreError.valueNotFound(key: let key) { + XCTAssertEqual(key, "TEST_key3") + } catch { + XCTFail("Unexpected error: \(error)") + } } - func test_setValue() async { + func test_setValue() async throws { store.store = ["TEST_key1": "TEST_value1"] - await store.setValue("TEST_newValue1", forKey: "TEST_key1") - await store.setValue("TEST_newValue2", forKey: "TEST_key2") + try await store.setValue("TEST_newValue1", forKey: "TEST_key1") + try await store.setValue("TEST_newValue2", forKey: "TEST_key2") XCTAssertEqual(store.store["TEST_key1"] as? String, "TEST_newValue1") XCTAssertEqual(store.store["TEST_key2"] as? String, "TEST_newValue2") @@ -93,14 +115,14 @@ final class RuntimeOverridesStoreTests: XCTestCase { XCTAssertEqual(store.store["TEST_key2"] as? String, "TEST_newValue2") } - func test_removeValue() async { + func test_removeValue() async throws { store.store = [ "TEST_key1": "TEST_value1", "TEST_key2": "TEST_value2", ] - await store.removeValue(forKey: "TEST_key1") - await store.removeValue(forKey: "TEST_key999") + try await store.removeValue(forKey: "TEST_key1") + try await store.removeValue(forKey: "TEST_key999") XCTAssertNil(store.store["TEST_key1"]) XCTAssertEqual(store.store["TEST_key2"] as? String, "TEST_value2") diff --git a/Tests/YMFFTests/Cases/TransparentFeatureFlagStoreTests.swift b/Tests/YMFFTests/Cases/TransparentFeatureFlagStoreTests.swift index df2db39..889912b 100644 --- a/Tests/YMFFTests/Cases/TransparentFeatureFlagStoreTests.swift +++ b/Tests/YMFFTests/Cases/TransparentFeatureFlagStoreTests.swift @@ -43,30 +43,52 @@ final class TransparentFeatureFlagStoreTests: XCTestCase { XCTAssertFalse(containsValue2) } - func test_value() async { + func test_value() async throws { store["TEST_key1"] = "TEST_value1" store["TEST_key2"] = "TEST_value2" - let value1: String? = await store.value(forKey: "TEST_key1") - let value2: Int? = await store.value(forKey: "TEST_key2") - let value3: Bool? = await store.value(forKey: "TEST_key3") - + let value1: String = try await store.value(forKey: "TEST_key1") XCTAssertEqual(value1, "TEST_value1") - XCTAssertNil(value2) - XCTAssertNil(value3) + + do { + let _: Int = try await store.value(forKey: "TEST_key2") + XCTFail("Expected an error") + } catch CommonFeatureFlagStoreError.typeMismatch { } catch { + XCTFail("Unexpected error: \(error.localizedDescription)") + } + + do { + let _: Bool = try await store.value(forKey: "TEST_key3") + XCTFail("Expected an error") + } catch CommonFeatureFlagStoreError.valueNotFound(key: let key) { + XCTAssertEqual(key, "TEST_key3") + } catch { + XCTFail("Unexpected error: \(error.localizedDescription)") + } } - func test_valueSync() { + func test_valueSync() throws { store["TEST_key1"] = "TEST_value1" store["TEST_key2"] = "TEST_value2" - let value1: String? = store.valueSync(forKey: "TEST_key1") - let value2: Int? = store.valueSync(forKey: "TEST_key2") - let value3: Bool? = store.valueSync(forKey: "TEST_key3") - + let value1: String = try store.valueSync(forKey: "TEST_key1") XCTAssertEqual(value1, "TEST_value1") - XCTAssertNil(value2) - XCTAssertNil(value3) + + do { + let _: Int = try store.valueSync(forKey: "TEST_key2") + XCTFail("Expected an error") + } catch CommonFeatureFlagStoreError.typeMismatch { } catch { + XCTFail("Unexpected error: \(error.localizedDescription)") + } + + do { + let _: Bool = try store.valueSync(forKey: "TEST_key3") + XCTFail("Expected an error") + } catch CommonFeatureFlagStoreError.valueNotFound(key: let key) { + XCTAssertEqual(key, "TEST_key3") + } catch { + XCTFail("Unexpected error: \(error.localizedDescription)") + } } } diff --git a/Tests/YMFFTests/Cases/UserDefaultsStoreTests.swift b/Tests/YMFFTests/Cases/UserDefaultsStoreTests.swift index 3898f7e..cd7ee7c 100644 --- a/Tests/YMFFTests/Cases/UserDefaultsStoreTests.swift +++ b/Tests/YMFFTests/Cases/UserDefaultsStoreTests.swift @@ -54,37 +54,59 @@ final class UserDefaultsStoreTests: XCTestCase { XCTAssertFalse(containsValue2) } - func test_value() async { + func test_value() async throws { userDefaults.set("TEST_value1", forKey: "TEST_key1") userDefaults.set("TEST_value2", forKey: "TEST_key2") - let value1: String? = await store.value(forKey: "TEST_key1") - let value2: Int? = await store.value(forKey: "TEST_key2") - let value3: Bool? = await store.value(forKey: "TEST_key3") - + let value1: String = try await store.value(forKey: "TEST_key1") XCTAssertEqual(value1, "TEST_value1") - XCTAssertNil(value2) - XCTAssertNil(value3) + + do { + let _: Int = try await store.value(forKey: "TEST_key2") + XCTFail("Expected an error") + } catch CommonFeatureFlagStoreError.typeMismatch { } catch { + XCTFail("Unexpected error: \(error.localizedDescription)") + } + + do { + let _: Bool = try await store.value(forKey: "TEST_key3") + XCTFail("Expected an error") + } catch CommonFeatureFlagStoreError.valueNotFound(key: let key) { + XCTAssertEqual(key, "TEST_key3") + } catch { + XCTFail("Unexpected error: \(error.localizedDescription)") + } } - func test_valueSync() { + func test_valueSync() throws { userDefaults.set("TEST_value1", forKey: "TEST_key1") userDefaults.set("TEST_value2", forKey: "TEST_key2") - let value1: String? = store.valueSync(forKey: "TEST_key1") - let value2: Int? = store.valueSync(forKey: "TEST_key2") - let value3: Bool? = store.valueSync(forKey: "TEST_key3") - + let value1: String = try store.valueSync(forKey: "TEST_key1") XCTAssertEqual(value1, "TEST_value1") - XCTAssertNil(value2) - XCTAssertNil(value3) + + do { + let _: Int = try store.valueSync(forKey: "TEST_key2") + XCTFail("Expected an error") + } catch CommonFeatureFlagStoreError.typeMismatch { } catch { + XCTFail("Unexpected error: \(error.localizedDescription)") + } + + do { + let _: Bool = try store.valueSync(forKey: "TEST_key3") + XCTFail("Expected an error") + } catch CommonFeatureFlagStoreError.valueNotFound(key: let key) { + XCTAssertEqual(key, "TEST_key3") + } catch { + XCTFail("Unexpected error: \(error.localizedDescription)") + } } - func test_setValue() async { + func test_setValue() async throws { userDefaults.set("TEST_value1", forKey: "TEST_key1") - await store.setValue("TEST_newValue1", forKey: "TEST_key1") - await store.setValue("TEST_newValue2", forKey: "TEST_key2") + try await store.setValue("TEST_newValue1", forKey: "TEST_key1") + try await store.setValue("TEST_newValue2", forKey: "TEST_key2") XCTAssertEqual(userDefaults.string(forKey: "TEST_key1"), "TEST_newValue1") XCTAssertEqual(userDefaults.string(forKey: "TEST_key2"), "TEST_newValue2") @@ -100,12 +122,12 @@ final class UserDefaultsStoreTests: XCTestCase { XCTAssertEqual(userDefaults.string(forKey: "TEST_key2"), "TEST_newValue2") } - func test_removeValue() async { + func test_removeValue() async throws { userDefaults.set("TEST_value1", forKey: "TEST_key1") userDefaults.set("TEST_value2", forKey: "TEST_key2") - await store.removeValue(forKey: "TEST_key1") - await store.removeValue(forKey: "TEST_key999") + try await store.removeValue(forKey: "TEST_key1") + try await store.removeValue(forKey: "TEST_key999") XCTAssertNil(userDefaults.string(forKey: "TEST_key1")) XCTAssertEqual(userDefaults.string(forKey: "TEST_key2"), "TEST_value2") diff --git a/Tests/YMFFTests/Utilities/FeatureFlagStoreMock.swift b/Tests/YMFFTests/Utilities/FeatureFlagStoreMock.swift index 07560ad..8d9293e 100644 --- a/Tests/YMFFTests/Utilities/FeatureFlagStoreMock.swift +++ b/Tests/YMFFTests/Utilities/FeatureFlagStoreMock.swift @@ -22,7 +22,7 @@ final class FeatureFlagStoreMock { var value_invocationCount = 0 var value_keys = [String]() - var value_returnValue: Any? + var value_result: Result! } @@ -36,13 +36,17 @@ extension FeatureFlagStoreMock: FeatureFlagStore { return containsValue_returnValue } - func value(forKey key: String) async -> Value? { + func value(forKey key: String) async throws -> Value { value_invocationCount += 1 value_keys.append(key) - if let value_returnValue { - return value_returnValue as? Value? ?? nil - } else { - return nil + switch value_result! { + case .success(let anyValue): + if let value = anyValue as? Value { + return value + } else { + throw TestFeatureFlagStoreError.typeMismatch + } + case .failure(let error): throw error } } diff --git a/Tests/YMFFTests/Utilities/MutableFeatureFlagStoreMock.swift b/Tests/YMFFTests/Utilities/MutableFeatureFlagStoreMock.swift index 11b23bb..3cd4f5f 100644 --- a/Tests/YMFFTests/Utilities/MutableFeatureFlagStoreMock.swift +++ b/Tests/YMFFTests/Utilities/MutableFeatureFlagStoreMock.swift @@ -22,15 +22,18 @@ final class MutableFeatureFlagStoreMock { var value_invocationCount = 0 var value_keys = [String]() - var value_returnValue: Any? + var value_result: Result! var setValue_invocationCount = 0 var setValue_keyValuePairs = [(String, Any)]() + var setValue_result: Result! var removeValue_invocationCount = 0 var removeValue_keys = [String]() + var removeValue_result: Result! var saveChanges_invocationCount = 0 + var saveChanges_result: Result! } @@ -44,28 +47,41 @@ extension MutableFeatureFlagStoreMock: MutableFeatureFlagStore { return containsValue_returnValue } - func value(forKey key: String) async -> Value? { + func value(forKey key: String) async throws -> Value { value_invocationCount += 1 value_keys.append(key) - if let value_returnValue { - return value_returnValue as? Value? ?? nil - } else { - return nil + switch value_result! { + case .success(let anyValue): + if let value = anyValue as? Value { + return value + } else { + throw TestFeatureFlagStoreError.typeMismatch + } + case .failure(let error): throw error } } - func setValue(_ value: Value, forKey key: String) async { + func setValue(_ value: Value, forKey key: String) async throws { setValue_invocationCount += 1 setValue_keyValuePairs.append((key, value)) + if case .failure(let error) = setValue_result { + throw error + } } - func removeValue(forKey key: String) async { + func removeValue(forKey key: String) async throws { removeValue_invocationCount += 1 removeValue_keys.append(key) + if case .failure(let error) = removeValue_result { + throw error + } } - func saveChanges() async { + func saveChanges() async throws { saveChanges_invocationCount += 1 + if case .failure(let error) = saveChanges_result { + throw error + } } } diff --git a/Tests/YMFFTests/Utilities/SynchronousFeatureFlagStoreMock.swift b/Tests/YMFFTests/Utilities/SynchronousFeatureFlagStoreMock.swift index 4901115..de0de58 100644 --- a/Tests/YMFFTests/Utilities/SynchronousFeatureFlagStoreMock.swift +++ b/Tests/YMFFTests/Utilities/SynchronousFeatureFlagStoreMock.swift @@ -22,7 +22,7 @@ final class SynchronousFeatureFlagStoreMock { var valueSync_invocationCount = 0 var valueSync_keys = [String]() - var valueSync_returnValue: Any? + var valueSync_result: Result! } @@ -36,13 +36,17 @@ extension SynchronousFeatureFlagStoreMock: SynchronousFeatureFlagStore { return containsValueSync_returnValue } - func valueSync(forKey key: String) -> Value? { + func valueSync(forKey key: String) throws -> Value { valueSync_invocationCount += 1 valueSync_keys.append(key) - if let valueSync_returnValue { - return valueSync_returnValue as? Value? ?? nil - } else { - return nil + switch valueSync_result! { + case .success(let anyValue): + if let value = anyValue as? Value { + return value + } else { + throw TestFeatureFlagStoreError.typeMismatch + } + case .failure(let error): throw error } } diff --git a/Tests/YMFFTests/Utilities/SynchronousMutableFeatureFlagStoreMock.swift b/Tests/YMFFTests/Utilities/SynchronousMutableFeatureFlagStoreMock.swift index 6bb42e9..acd95e3 100644 --- a/Tests/YMFFTests/Utilities/SynchronousMutableFeatureFlagStoreMock.swift +++ b/Tests/YMFFTests/Utilities/SynchronousMutableFeatureFlagStoreMock.swift @@ -22,15 +22,18 @@ final class SynchronousMutableFeatureFlagStoreMock { var valueSync_invocationCount = 0 var valueSync_keys = [String]() - var valueSync_returnValue: Any? + var valueSync_result: Result! var setValueSync_invocationCount = 0 var setValueSync_keyValuePairs = [(String, Any)]() + var setValueSync_result: Result! var removeValueSync_invocationCount = 0 var removeValueSync_keys = [String]() + var removeValueSync_result: Result! var saveChangesSync_invocationCount = 0 + var saveChangesSync_result: Result! } @@ -44,28 +47,41 @@ extension SynchronousMutableFeatureFlagStoreMock: SynchronousMutableFeatureFlagS return containsValueSync_returnValue } - func valueSync(forKey key: String) -> Value? { + func valueSync(forKey key: String) throws -> Value { valueSync_invocationCount += 1 valueSync_keys.append(key) - if let valueSync_returnValue { - return valueSync_returnValue as? Value? ?? nil - } else { - return nil + switch valueSync_result! { + case .success(let anyValue): + if let value = anyValue as? Value { + return value + } else { + throw TestFeatureFlagStoreError.typeMismatch + } + case .failure(let error): throw error } } - func setValueSync(_ value: Value, forKey key: String) { + func setValueSync(_ value: Value, forKey key: String) throws { setValueSync_invocationCount += 1 setValueSync_keyValuePairs.append((key, value)) + if case .failure(let error) = setValueSync_result { + throw error + } } - func removeValueSync(forKey key: String) { + func removeValueSync(forKey key: String) throws { removeValueSync_invocationCount += 1 removeValueSync_keys.append(key) + if case .failure(let error) = removeValueSync_result { + throw error + } } - func saveChangesSync() { + func saveChangesSync() throws { saveChangesSync_invocationCount += 1 + if case .failure(let error) = saveChangesSync_result { + throw error + } } } diff --git a/Tests/YMFFTests/Utilities/TestFeatureFlagStoreError.swift b/Tests/YMFFTests/Utilities/TestFeatureFlagStoreError.swift new file mode 100644 index 0000000..6cdf93d --- /dev/null +++ b/Tests/YMFFTests/Utilities/TestFeatureFlagStoreError.swift @@ -0,0 +1,13 @@ +// +// TestFeatureFlagStoreError.swift +// YMFFTests +// +// Created by Yakov Manshin on 5/10/24. +// Copyright © 2024 Yakov Manshin. See the LICENSE file for license info. +// + +enum TestFeatureFlagStoreError: Error, Equatable { + case typeMismatch + case failedToSetValue + case failedToRemoveValue +}