Skip to content

Commit

Permalink
[#147] Crash Fix for Optionals in UserDefaultsStore (#151)
Browse files Browse the repository at this point in the history
* `UserDefaults` does not accept `nil` as the value in a record
* Upon finding a `nil`, `UserDefaults` raises an exception which crashes the client app
* This fix does not add complete support for optionals but addresses the crash by checking the type against the `ExpressibleByNilLiteral` protocol and throwing the `optionalValuesAreNotSupported` error
* Complete support for optionals in `UserDefaultsStore` is planned for #150
  • Loading branch information
yakovmanshin authored May 13, 2024
1 parent 90460f3 commit 6121eff
Show file tree
Hide file tree
Showing 2 changed files with 118 additions and 4 deletions.
20 changes: 19 additions & 1 deletion Sources/YMFF/FeatureFlagResolver/Store/UserDefaultsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,20 @@ final public class UserDefaultsStore {
extension UserDefaultsStore: SynchronousMutableFeatureFlagStore {

public func valueSync<Value>(for key: FeatureFlagKey) -> Result<Value, FeatureFlagStoreError> {
guard !(Value.self is ExpressibleByNilLiteral.Type) else {
return .failure(.otherError(Error.optionalValuesAreNotSupported))
}

guard let anyValue = userDefaults.object(forKey: key) else { return .failure(.valueNotFound) }
guard let value = anyValue as? Value else { return .failure(.typeMismatch) }
return .success(value)
}

public func setValueSync<Value>(_ value: Value, for key: FeatureFlagKey) {
public func setValueSync<Value>(_ value: Value, for key: FeatureFlagKey) throws {
guard !(value is ExpressibleByNilLiteral) else {
throw FeatureFlagStoreError.otherError(Error.optionalValuesAreNotSupported)
}

userDefaults.set(value, forKey: key)
}

Expand All @@ -54,4 +62,14 @@ extension UserDefaultsStore: SynchronousMutableFeatureFlagStore {

}

// MARK: - Error

extension UserDefaultsStore {

enum Error: Swift.Error {
case optionalValuesAreNotSupported
}

}

#endif
102 changes: 99 additions & 3 deletions Tests/YMFFTests/Cases/UserDefaultsStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,52 @@ final class UserDefaultsStoreTests: XCTestCase {
}
}

func test_value_optionals() async {
userDefaults.set("TEST_value1", forKey: "TEST_key1")
// No record for TEST_key2

do {
let _: String? = try await store.value(for: "TEST_key1").get()
XCTFail("Expected an error")
} catch FeatureFlagStoreError.otherError(UserDefaultsStore.Error.optionalValuesAreNotSupported) {
XCTAssertEqual(userDefaults.string(forKey: "TEST_key1"), "TEST_value1")
} catch {
XCTFail("Unexpected error: \(error)")
}

do {
let _: String? = try await store.value(for: "TEST_key2").get()
XCTFail("Expected an error")
} catch FeatureFlagStoreError.otherError(UserDefaultsStore.Error.optionalValuesAreNotSupported) {
XCTAssertNil(userDefaults.string(forKey: "TEST_key2"))
} catch {
XCTFail("Unexpected error: \(error)")
}
}

func test_valueSync_optionals() {
userDefaults.set("TEST_value1", forKey: "TEST_key1")
// No record for TEST_key2

do {
let _: String? = try store.valueSync(for: "TEST_key1").get()
XCTFail("Expected an error")
} catch FeatureFlagStoreError.otherError(UserDefaultsStore.Error.optionalValuesAreNotSupported) {
XCTAssertEqual(userDefaults.string(forKey: "TEST_key1"), "TEST_value1")
} catch {
XCTFail("Unexpected error: \(error)")
}

do {
let _: String? = try store.valueSync(for: "TEST_key2").get()
XCTFail("Expected an error")
} catch FeatureFlagStoreError.otherError(UserDefaultsStore.Error.optionalValuesAreNotSupported) {
XCTAssertNil(userDefaults.string(forKey: "TEST_key2"))
} catch {
XCTFail("Unexpected error: \(error)")
}
}

func test_setValue() async throws {
userDefaults.set("TEST_value1", forKey: "TEST_key1")

Expand All @@ -91,16 +137,66 @@ final class UserDefaultsStoreTests: XCTestCase {
XCTAssertEqual(userDefaults.string(forKey: "TEST_key2"), "TEST_newValue2")
}

func test_setValueSync() {
func test_setValueSync() throws {
userDefaults.set("TEST_value1", forKey: "TEST_key1")

store.setValueSync("TEST_newValue1", for: "TEST_key1")
store.setValueSync("TEST_newValue2", for: "TEST_key2")
try store.setValueSync("TEST_newValue1", for: "TEST_key1")
try store.setValueSync("TEST_newValue2", for: "TEST_key2")

XCTAssertEqual(userDefaults.string(forKey: "TEST_key1"), "TEST_newValue1")
XCTAssertEqual(userDefaults.string(forKey: "TEST_key2"), "TEST_newValue2")
}

func test_setValue_optionals() async {
userDefaults.set("TEST_value1", forKey: "TEST_key1")
userDefaults.set("TEST_value2", forKey: "TEST_key2")

do {
let optionalValue1: String? = "TEST_newValue1"
try await store.setValue(optionalValue1, for: "TEST_key1")
XCTFail("Expected an error")
} catch FeatureFlagStoreError.otherError(UserDefaultsStore.Error.optionalValuesAreNotSupported) {
XCTAssertEqual(userDefaults.string(forKey: "TEST_key1"), "TEST_value1")
} catch {
XCTFail("Unexpected error: \(error)")
}

do {
let optionalValue2: String? = nil
try await store.setValue(optionalValue2, for: "TEST_key2")
XCTFail("Expected an error")
} catch FeatureFlagStoreError.otherError(UserDefaultsStore.Error.optionalValuesAreNotSupported) {
XCTAssertEqual(userDefaults.string(forKey: "TEST_key2"), "TEST_value2")
} catch {
XCTFail("Unexpected error: \(error)")
}
}

func test_setValueSync_optionals() {
userDefaults.set("TEST_value1", forKey: "TEST_key1")
userDefaults.set("TEST_value2", forKey: "TEST_key2")

do {
let optionalValue1: String? = "TEST_newValue1"
try store.setValueSync(optionalValue1, for: "TEST_key1")
XCTFail("Expected an error")
} catch FeatureFlagStoreError.otherError(UserDefaultsStore.Error.optionalValuesAreNotSupported) {
XCTAssertEqual(userDefaults.string(forKey: "TEST_key1"), "TEST_value1")
} catch {
XCTFail("Unexpected error: \(error)")
}

do {
let optionalValue2: String? = nil
try store.setValueSync(optionalValue2, for: "TEST_key2")
XCTFail("Expected an error")
} catch FeatureFlagStoreError.otherError(UserDefaultsStore.Error.optionalValuesAreNotSupported) {
XCTAssertEqual(userDefaults.string(forKey: "TEST_key2"), "TEST_value2")
} catch {
XCTFail("Unexpected error: \(error)")
}
}

func test_removeValue() async throws {
userDefaults.set("TEST_value1", forKey: "TEST_key1")
userDefaults.set("TEST_value2", forKey: "TEST_key2")
Expand Down

0 comments on commit 6121eff

Please sign in to comment.