|
1 | 1 | import XCTest |
| 2 | +import OpenFeature |
2 | 3 | @testable import FirebaseRemoteConfigOpenFeatureProvider |
3 | 4 |
|
4 | | -final class FirebaseRemoteConfig_OpenFeature_Provider_SwiftTests: XCTestCase { |
5 | | - func testExample() throws { |
6 | | - // XCTest Documentation |
7 | | - // https://developer.apple.com/documentation/xctest |
| 5 | +final class ProviderSpecTests: XCTestCase { |
8 | 6 |
|
9 | | - // Defining Test Cases and Test Methods |
10 | | - // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods |
| 7 | + let provider: FeatureProvider = { |
| 8 | + let mock = MockRemoteConfig(lastFetchTime: Date()) { |
| 9 | + "string" |
| 10 | + } numberValueClosure: { |
| 11 | + 100 |
| 12 | + } boolValueClosure: { |
| 13 | + true |
| 14 | + } jsonValueClosure: { |
| 15 | + [ |
| 16 | + ProviderSpecTestCase.trueKey.rawValue: true, |
| 17 | + ProviderSpecTestCase.stringKey.rawValue: "string", |
| 18 | + ProviderSpecTestCase.integer100Key.rawValue: 100, |
| 19 | + ProviderSpecTestCase.piDoubleKey.rawValue: 3.1415, |
| 20 | + ProviderSpecTestCase.objectKey.rawValue: [ |
| 21 | + ProviderSpecTestCase.falseKey.rawValue: false |
| 22 | + ] |
| 23 | + ] |
| 24 | + } allKeysClosure: { |
| 25 | + ProviderSpecTestCase.allCases.map { $0.rawValue } |
| 26 | + } remoteConfigSourceClosure: { |
| 27 | + return .remote |
| 28 | + } |
| 29 | + |
| 30 | + return FirebaseRemoteConfigOpenFeatureProvider(remoteConfig: mock) |
| 31 | + }() |
| 32 | + |
| 33 | + static let defaultRegisterData: [String: Any] = [ |
| 34 | + ProviderSpecTestCase.trueKey.rawValue: true, |
| 35 | + ProviderSpecTestCase.stringKey.rawValue: "string", |
| 36 | + ProviderSpecTestCase.integer100Key.rawValue: 100, |
| 37 | + ProviderSpecTestCase.piDoubleKey.rawValue: 3.1415, |
| 38 | + ProviderSpecTestCase.objectKey.rawValue: [ |
| 39 | + ProviderSpecTestCase.trueKey.rawValue: true, |
| 40 | + ProviderSpecTestCase.stringKey.rawValue: "string", |
| 41 | + ProviderSpecTestCase.integer100Key.rawValue: 100, |
| 42 | + ProviderSpecTestCase.piDoubleKey.rawValue: 3.1415, |
| 43 | + ProviderSpecTestCase.objectKey.rawValue: [ |
| 44 | + ProviderSpecTestCase.falseKey.rawValue: false |
| 45 | + ] |
| 46 | + ] |
| 47 | + ] |
| 48 | + |
| 49 | + enum ProviderSpecTestCase: String, CaseIterable { |
| 50 | + case trueKey |
| 51 | + case falseKey |
| 52 | + case stringKey |
| 53 | + case integer100Key |
| 54 | + case piDoubleKey |
| 55 | + case objectKey |
| 56 | + } |
| 57 | + |
| 58 | + // MARK: - Feature Provider Interface |
| 59 | + |
| 60 | + /// Test for the provider metadata |
| 61 | + /// |
| 62 | + /// - Requirement 2.1.1: The provider interface MUST define a metadata member or accessor, containing a name field or accessor of type string, which identifies the provider implementation. |
| 63 | + /// - https://openfeature.dev/specification/sections/providers#requirement-211 |
| 64 | + func testGetNameOfProviderFromMetadata() { |
| 65 | + XCTAssertEqual("FirebaseRemoteConfigOpenFeatureProvider", provider.metadata.name) |
| 66 | + } |
| 67 | + |
| 68 | + // MARK: - Flag Value Resolution |
| 69 | + |
| 70 | + /// Test for the provider resolution result |
| 71 | + /// |
| 72 | + /// - Conditional Requirement 2.2.2.1: The feature provider interface MUST define methods for typed flag resolution, including boolean, numeric, string, and structure. |
| 73 | + /// https://openfeature.dev/specification/sections/providers#conditional-requirement-2221 |
| 74 | + /// - Requirement 2.2.3: In cases of normal execution, the provider MUST populate the resolution details structure's value field with the resolved flag value. |
| 75 | + /// https://openfeature.dev/specification/sections/providers#requirement-223 |
| 76 | + func testEachInterfacesOfResolutionHaveResolvedValue() throws { |
| 77 | + let boolResult = try provider.getBooleanEvaluation(key: ProviderSpecTestCase.trueKey.rawValue, defaultValue: false, context: MutableContext()) |
| 78 | + XCTAssertEqual(true, boolResult.value) |
| 79 | + |
| 80 | + let stringResult = try provider.getStringEvaluation(key: ProviderSpecTestCase.stringKey.rawValue, defaultValue: "", context: MutableContext()) |
| 81 | + XCTAssertEqual("string", stringResult.value) |
| 82 | + |
| 83 | + let intResult = try provider.getIntegerEvaluation(key: ProviderSpecTestCase.integer100Key.rawValue, defaultValue: 0, context: MutableContext()) |
| 84 | + XCTAssertEqual(100, intResult.value) |
| 85 | + |
| 86 | + let doubleResult = try provider.getDoubleEvaluation(key: ProviderSpecTestCase.piDoubleKey.rawValue, defaultValue: 0.1, context: MutableContext()) |
| 87 | + XCTAssertEqual(100.0, doubleResult.value) |
| 88 | + |
| 89 | + let objectResult = try provider.getObjectEvaluation(key: ProviderSpecTestCase.objectKey.rawValue, defaultValue: .null, context: MutableContext()) |
| 90 | + XCTAssertNotNil(objectResult.value) |
| 91 | + } |
| 92 | + |
| 93 | + /// Test for the provider resolution variant |
| 94 | + /// |
| 95 | + /// - Requirement 2.2.4: In cases of normal execution, the provider SHOULD populate the resolution details structure's variant field with a string identifier corresponding to the returned flag value. |
| 96 | + /// https://openfeature.dev/specification/sections/providers#requirement-224 |
| 97 | + func testEvaluationContainsVariant() throws { |
| 98 | + let boolResult = try provider.getBooleanEvaluation(key: ProviderSpecTestCase.trueKey.rawValue, defaultValue: false, context: MutableContext()) |
| 99 | + XCTAssertEqual(Value.boolean(true).description, boolResult.variant) |
| 100 | + |
| 101 | + let stringResult = try provider.getStringEvaluation(key: ProviderSpecTestCase.stringKey.rawValue, defaultValue: "", context: MutableContext()) |
| 102 | + XCTAssertEqual(Value.string("string").description, stringResult.variant) |
| 103 | + |
| 104 | + let intResult = try provider.getIntegerEvaluation(key: ProviderSpecTestCase.integer100Key.rawValue, defaultValue: 0, context: MutableContext()) |
| 105 | + XCTAssertEqual(Value.integer(100).description, intResult.variant) |
| 106 | + |
| 107 | + let doubleResult = try provider.getDoubleEvaluation(key: ProviderSpecTestCase.piDoubleKey.rawValue, defaultValue: 0.1, context: MutableContext()) |
| 108 | + XCTAssertEqual(Value.double(100.0).description, doubleResult.variant) |
| 109 | + |
| 110 | + let objectResult = try provider.getObjectEvaluation(key: ProviderSpecTestCase.objectKey.rawValue, defaultValue: .structure(["foo": Value.string("bar")]), context: MutableContext()) |
| 111 | + let expects = Self.defaultRegisterData[ProviderSpecTestCase.objectKey.rawValue] as! [String: Any] |
| 112 | + let variant = try XCTUnwrap(objectResult.variant) |
| 113 | + expects.keys.forEach { key in |
| 114 | + XCTAssertTrue(variant.contains(key)) |
| 115 | + } |
| 116 | + } |
| 117 | + |
| 118 | + /// Test for the provider resolution reason |
| 119 | + /// |
| 120 | + /// - Requirement 2.2.5: The provider SHOULD populate the resolution details structure's reason field with "STATIC", "DEFAULT", "TARGETING_MATCH", "SPLIT", "CACHED", "DISABLED", "UNKNOWN", "STALE", "ERROR" or some other string indicating the semantic reason for the returned flag value. |
| 121 | + /// https://openfeature.dev/specification/sections/providers#requirement-225 |
| 122 | + func testEvaluationContainsReason() throws { |
| 123 | + let boolResult = try provider.getBooleanEvaluation(key: ProviderSpecTestCase.trueKey.rawValue, defaultValue: false, context: MutableContext()) |
| 124 | + XCTAssertEqual(boolResult.reason, Reason.cached.rawValue) |
| 125 | + |
| 126 | + let stringResult = try provider.getStringEvaluation(key: ProviderSpecTestCase.stringKey.rawValue, defaultValue: "", context: MutableContext()) |
| 127 | + XCTAssertEqual(stringResult.reason, Reason.cached.rawValue) |
| 128 | + |
| 129 | + let intResult = try provider.getIntegerEvaluation(key: ProviderSpecTestCase.integer100Key.rawValue, defaultValue: 0, context: MutableContext()) |
| 130 | + XCTAssertEqual(intResult.reason, Reason.cached.rawValue) |
| 131 | + |
| 132 | + let doubleResult = try provider.getDoubleEvaluation(key: ProviderSpecTestCase.piDoubleKey.rawValue, defaultValue: 0.1, context: MutableContext()) |
| 133 | + XCTAssertEqual(doubleResult.reason, Reason.cached.rawValue) |
| 134 | + |
| 135 | + let objectResult = try provider.getObjectEvaluation(key: ProviderSpecTestCase.objectKey.rawValue, defaultValue: .null, context: MutableContext()) |
| 136 | + XCTAssertEqual(objectResult.reason, Reason.cached.rawValue) |
| 137 | + } |
| 138 | + |
| 139 | + /// Test for the provider resolution has no error code in case of normal |
| 140 | + /// |
| 141 | + /// - Requirement 2.2.6: In cases of normal execution, the provider MUST NOT populate the resolution details structure's error code field, or otherwise must populate it with a null or falsy value. |
| 142 | + /// https://openfeature.dev/specification/sections/providers#requirement-226 |
| 143 | + func testNoErrorCodeInCaseOfNormal() throws { |
| 144 | + let boolResult = try provider.getBooleanEvaluation(key: ProviderSpecTestCase.trueKey.rawValue, defaultValue: false, context: MutableContext()) |
| 145 | + XCTAssertNil(boolResult.errorCode) |
| 146 | + |
| 147 | + let stringResult = try provider.getStringEvaluation(key: ProviderSpecTestCase.stringKey.rawValue, defaultValue: "", context: MutableContext()) |
| 148 | + XCTAssertNil(stringResult.errorCode) |
| 149 | + |
| 150 | + let intResult = try provider.getIntegerEvaluation(key: ProviderSpecTestCase.integer100Key.rawValue, defaultValue: 0, context: MutableContext()) |
| 151 | + XCTAssertNil(intResult.errorCode) |
| 152 | + |
| 153 | + let doubleResult = try provider.getDoubleEvaluation(key: ProviderSpecTestCase.piDoubleKey.rawValue, defaultValue: 0.1, context: MutableContext()) |
| 154 | + XCTAssertNil(doubleResult.errorCode) |
| 155 | + |
| 156 | + let objectResult = try provider.getObjectEvaluation(key: ProviderSpecTestCase.objectKey.rawValue, defaultValue: .null, context: MutableContext()) |
| 157 | + XCTAssertNil(objectResult.errorCode) |
| 158 | + } |
| 159 | + |
| 160 | + /// Test for the exceptions about no key found |
| 161 | + /// |
| 162 | + /// - Requirement 2.2.7: In cases of abnormal execution, the provider MUST indicate an error using the idioms of the implementation language, with an associated error code and optional associated error message. |
| 163 | + /// https://openfeature.dev/specification/sections/providers#requirement-227 |
| 164 | + func testErrorCodeAndErrorMessageIfAbnormal() throws { |
| 165 | + let wrongKey = "wrongKey" |
| 166 | + |
| 167 | + XCTAssertThrowsError(try provider.getBooleanEvaluation(key: wrongKey, defaultValue: false, context: MutableContext())) |
| 168 | + |
| 169 | + XCTAssertThrowsError(try provider.getStringEvaluation(key: wrongKey, defaultValue: "", context: MutableContext())) |
| 170 | + |
| 171 | + XCTAssertThrowsError(try provider.getIntegerEvaluation(key: wrongKey, defaultValue: 0, context: MutableContext())) |
| 172 | + |
| 173 | + XCTAssertThrowsError(try provider.getDoubleEvaluation(key: wrongKey, defaultValue: 0.1, context: MutableContext())) |
| 174 | + |
| 175 | + XCTAssertThrowsError(try provider.getObjectEvaluation(key: wrongKey, defaultValue: .structure([wrongKey: .boolean(false)]), context: MutableContext())) |
| 176 | + } |
| 177 | + |
| 178 | + /// no implementation |
| 179 | + /// |
| 180 | + /// - Requirement 2.2.8: The resolution details structure SHOULD accept a generic argument (or use an equivalent language feature) which indicates the type of the wrapped value field. |
| 181 | + /// https://openfeature.dev/specification/sections/providers#condition-2281 |
| 182 | + func testGenerics() throws {} |
| 183 | + |
| 184 | + // MARK: Provider hooks (no implementation) |
| 185 | + |
| 186 | + // MARK: Initialization |
| 187 | + |
| 188 | + /// no implementation, this library does not support any context |
| 189 | + /// |
| 190 | + /// - Requirement 2.4.1: The provider MAY define an initialize function which accepts the global evaluation context as an argument and performs initialization logic relevant to the provider. |
| 191 | + /// https://openfeature.dev/specification/sections/providers#requirement-241 |
| 192 | + |
| 193 | + /// Test for provider status |
| 194 | + /// |
| 195 | + /// - Requirement 2.4.2: The provider MAY define a status field/accessor which indicates the readiness of the provider, with possible values NOT_READY, READY, STALE, or ERROR. |
| 196 | + /// https://openfeature.dev/specification/sections/providers#requirement-242 |
| 197 | + /// - Requirement 2.4.3: The provider MUST set its status field/accessor to READY if its initialize function terminates normally. |
| 198 | + /// https://openfeature.dev/specification/sections/providers#requirement-243 |
| 199 | + /// - Requirement 2.4.4: The provider MUST set its status field to ERROR if its initialize function terminates abnormally. |
| 200 | + /// https://openfeature.dev/specification/sections/providers#requirement-244 |
| 201 | + func testStatus() throws { |
| 202 | + var providerToTest = FirebaseRemoteConfigOpenFeatureProvider(remoteConfig: MockRemoteConfig()) |
| 203 | + XCTAssertEqual(providerToTest.status, .notReady) |
| 204 | + |
| 205 | + providerToTest = FirebaseRemoteConfigOpenFeatureProvider(remoteConfig: MockRemoteConfig(lastFetchStatus: .success)) |
| 206 | + XCTAssertEqual(providerToTest.status, .ready) |
| 207 | + |
| 208 | + providerToTest = FirebaseRemoteConfigOpenFeatureProvider(remoteConfig: MockRemoteConfig(lastFetchStatus: .throttled)) |
| 209 | + XCTAssertEqual(providerToTest.status, .ready) |
| 210 | + |
| 211 | + providerToTest = FirebaseRemoteConfigOpenFeatureProvider(remoteConfig: MockRemoteConfig(lastFetchStatus: .failure)) |
| 212 | + XCTAssertEqual(providerToTest.status, .error) |
| 213 | + } |
| 214 | + |
| 215 | + |
| 216 | + // MARK: Shutdown (no implementation) |
| 217 | + |
| 218 | + // MARK: Provider context reconciliation |
| 219 | + |
| 220 | + /// Test for context change event |
| 221 | + /// |
| 222 | + /// - Requirement 2.6.1: The provider MAY define an on context changed handler, which takes an argument for the previous context and the newly set context, in order to respond to an evaluation context change. |
| 223 | + /// https://openfeature.dev/specification/sections/providers#requirement-261 |
| 224 | + func testContextChangeEvent() throws { |
| 225 | + let providerToTest = FirebaseRemoteConfigOpenFeatureProvider(remoteConfig: MockRemoteConfig()) |
| 226 | + |
| 227 | + OpenFeatureAPI.shared.addHandler( |
| 228 | + observer: self, selector: #selector(configurationChangedEventEmitted(notification:)), event: .configurationChanged |
| 229 | + ) |
| 230 | + |
| 231 | + OpenFeatureAPI.shared.setProvider(provider: providerToTest) |
| 232 | + providerToTest.onContextSet(oldContext: MutableContext(), newContext: MutableContext()) |
| 233 | + |
| 234 | + wait(for: [configurationChangedExpectation], timeout: 5) |
| 235 | + } |
| 236 | + |
| 237 | + let configurationChangedExpectation = XCTestExpectation(description: "ConfigurationChanged") |
| 238 | + |
| 239 | + func configurationChangedEventEmitted(notification: NSNotification) { |
| 240 | + configurationChangedExpectation.fulfill() |
| 241 | + |
| 242 | + let maybeProvider = notification.userInfo?[providerEventDetailsKeyProvider] |
| 243 | + guard let eventProvider = maybeProvider as? FirebaseRemoteConfigOpenFeatureProvider else { |
| 244 | + XCTFail("Provider not passed in notification") |
| 245 | + return |
| 246 | + } |
| 247 | + XCTAssertEqual(eventProvider.metadata.name, provider.metadata.name) |
| 248 | + } |
| 249 | + |
| 250 | + func testReadyEvent() { |
| 251 | + let providerToTest = FirebaseRemoteConfigOpenFeatureProvider(remoteConfig: MockRemoteConfig(lastFetchStatus: .success)) |
| 252 | + |
| 253 | + OpenFeatureAPI.shared.addHandler( |
| 254 | + observer: self, selector: #selector(readyEventEmitted(notification:)), event: .ready |
| 255 | + ) |
| 256 | + |
| 257 | + OpenFeatureAPI.shared.setProvider(provider: providerToTest) |
| 258 | + wait(for: [readyExpectation], timeout: 5) |
| 259 | + |
| 260 | + providerToTest.initialize(initialContext: MutableContext()) |
| 261 | + } |
| 262 | + |
| 263 | + let readyExpectation = XCTestExpectation(description: "Ready") |
| 264 | + |
| 265 | + func readyEventEmitted(notification: NSNotification) { |
| 266 | + readyExpectation.fulfill() |
| 267 | + |
| 268 | + let maybeProvider = notification.userInfo?[providerEventDetailsKeyProvider] |
| 269 | + guard let eventProvider = maybeProvider as? FirebaseRemoteConfigOpenFeatureProvider else { |
| 270 | + XCTFail("Provider not passed in notification") |
| 271 | + return |
| 272 | + } |
| 273 | + XCTAssertEqual(eventProvider.metadata.name, provider.metadata.name) |
| 274 | + } |
| 275 | + |
| 276 | + func testErrorEvent() { |
| 277 | + let providerToTest = FirebaseRemoteConfigOpenFeatureProvider(remoteConfig: MockRemoteConfig(lastFetchStatus: .failure)) |
| 278 | + |
| 279 | + OpenFeatureAPI.shared.addHandler( |
| 280 | + observer: self, selector: #selector(errorEventEmitted(notification:)), event: .error |
| 281 | + ) |
| 282 | + |
| 283 | + OpenFeatureAPI.shared.setProvider(provider: providerToTest) |
| 284 | + wait(for: [errorExpectation], timeout: 5) |
| 285 | + |
| 286 | + providerToTest.initialize(initialContext: MutableContext()) |
| 287 | + } |
| 288 | + |
| 289 | + let errorExpectation = XCTestExpectation(description: "Error") |
| 290 | + |
| 291 | + func errorEventEmitted(notification: NSNotification) { |
| 292 | + errorExpectation.fulfill() |
| 293 | + |
| 294 | + let maybeProvider = notification.userInfo?[providerEventDetailsKeyProvider] |
| 295 | + guard let eventProvider = maybeProvider as? FirebaseRemoteConfigOpenFeatureProvider else { |
| 296 | + XCTFail("Provider not passed in notification") |
| 297 | + return |
| 298 | + } |
| 299 | + XCTAssertEqual(eventProvider.metadata.name, provider.metadata.name) |
11 | 300 | } |
12 | 301 | } |
0 commit comments