Skip to content

Commit 2abdfc3

Browse files
committed
add tests
1 parent 689c8dc commit 2abdfc3

File tree

5 files changed

+390
-8
lines changed

5 files changed

+390
-8
lines changed

Sources/FirebaseRemoteConfigOpenFeatureProvider/Extensions/Dictionary+Extensions.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ extension Dictionary<String, Any> {
2525
return .string(typedValue)
2626

2727
case .integer:
28-
guard let typedValue = value as? Int64 else {
28+
guard let typedValue = value as? Int else {
2929
throw OpenFeatureError.parseError(message: "Cannot parse \(value) as Int64")
3030
}
31-
return .integer(typedValue)
31+
return .integer(Int64(typedValue))
3232

3333
case .double:
3434
guard let typedValue = value as? Double else {

Sources/FirebaseRemoteConfigOpenFeatureProvider/FirebaseRemoteConfigOpenFeatureProvider.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public final class FirebaseRemoteConfigOpenFeatureProvider: FeatureProvider {
4040

4141
public init(remoteConfig: RemoteConfigCompatible) {
4242
self.remoteConfig = remoteConfig
43+
updateStatus(for: remoteConfig)
4344
}
4445

4546
public func initialize(initialContext: EvaluationContext?) {
Lines changed: 295 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,301 @@
11
import XCTest
2+
import OpenFeature
23
@testable import FirebaseRemoteConfigOpenFeatureProvider
34

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 {
86

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)
11300
}
12301
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//
2+
// MockRemoteConfig.swift
3+
//
4+
//
5+
// Created by Fumito Ito on 2024/02/01.
6+
//
7+
8+
import Foundation
9+
import FirebaseRemoteConfig
10+
import FirebaseRemoteConfigOpenFeatureProvider
11+
12+
struct MockRemoteConfig: RemoteConfigCompatible {
13+
var lastFetchTime: Date?
14+
15+
var lastFetchStatus: RemoteConfigFetchStatus
16+
17+
func configValue(for: String) -> RemoteConfigValueCompatible {
18+
MockRemoteConfigValue(
19+
stringValue: stringValueClosure(),
20+
numberValue: numberValueClosure(),
21+
boolValue: boolValueClosure(),
22+
jsonValue: jsonValueClosure(),
23+
source: remoteConfigSourceClosure()
24+
)
25+
}
26+
27+
func allKeys(from: RemoteConfigSource) -> [String] {
28+
allKeysClosure()
29+
}
30+
31+
private var stringValueClosure: () -> String?
32+
33+
private var numberValueClosure: () -> NSNumber
34+
35+
private var boolValueClosure: () -> Bool
36+
37+
private var jsonValueClosure: () -> Any?
38+
39+
private var allKeysClosure: () -> [String]
40+
41+
private var remoteConfigSourceClosure: () -> RemoteConfigSource
42+
43+
init(
44+
lastFetchTime: Date? = nil,
45+
lastFetchStatus: RemoteConfigFetchStatus = .noFetchYet,
46+
stringValueClosure: @escaping () -> String? = ({ () -> String? in "" }),
47+
numberValueClosure: @escaping () -> NSNumber = ({ () -> NSNumber in 0 }),
48+
boolValueClosure: @escaping () -> Bool = ({ () -> Bool in false }),
49+
jsonValueClosure: @escaping () -> Any? = ({ () -> Any? in nil }),
50+
allKeysClosure: @escaping () -> [String] = ({ () -> [String] in [] }),
51+
remoteConfigSourceClosure: @escaping () -> RemoteConfigSource = ({ () -> RemoteConfigSource in .remote })
52+
) {
53+
self.lastFetchTime = lastFetchTime
54+
self.lastFetchStatus = lastFetchStatus
55+
self.stringValueClosure = stringValueClosure
56+
self.numberValueClosure = numberValueClosure
57+
self.boolValueClosure = boolValueClosure
58+
self.jsonValueClosure = jsonValueClosure
59+
self.allKeysClosure = allKeysClosure
60+
self.remoteConfigSourceClosure = remoteConfigSourceClosure
61+
}
62+
}

0 commit comments

Comments
 (0)