diff --git a/README.md b/README.md index 6037db9..2e35b82 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ let client = OpenFeatureAPI.shared.getClient() let flagValue = client.getBooleanValue(key: "boolFlag", defaultValue: false) ``` -Setting a new provider or setting a new evaluation context might trigger asynchronous operations (e.g. fetching flag evaluations from the backend and store them in a local cache). It's advised to not interact with the OpenFeature client until the `ProviderReady` event has been emitted (see [Eventing](#eventing) below). +Setting a new provider or setting a new evaluation context might trigger asynchronous operations (e.g. fetching flag evaluations from the backend and store them in a local cache). It's advised to not interact with the OpenFeature client until the `ProviderReady` event has been sent (see [Eventing](#eventing) below). ## 🌟 Features @@ -174,12 +174,13 @@ Some providers support additional events, such as `PROVIDER_CONFIGURATION_CHANGE Please refer to the documentation of the provider you're using to see what events are supported. ```swift -OpenFeatureAPI.shared.addHandler( - observer: self, selector: #selector(readyEventEmitted(notification:)), event: .ready -) - -func readyEventEmitted(notification: NSNotification) { - // to something now that the provider is ready +let cancellable = OpenFeatureAPI.shared.observe().sink { event in + switch event { + case ProviderEvent.ready: + // ... + default: + // ... + } } ``` diff --git a/Sources/OpenFeature/Client.swift b/Sources/OpenFeature/Client.swift index c4783fc..083f28e 100644 --- a/Sources/OpenFeature/Client.swift +++ b/Sources/OpenFeature/Client.swift @@ -11,15 +11,4 @@ public protocol Client: Features { /// Hooks are run in the order they're added in the before stage. They are run in reverse order for all /// other stages. func addHooks(_ hooks: any Hook...) - - /// Add a handler for a particular provider event - /// - Parameter observer: The object observing the event. - /// - Parameter selector: The selector to call for this event. - /// - Parameter event: The event to listen for. - func addHandler(observer: Any, selector: Selector, event: ProviderEvent) - - /// Remove a handler for a particular provider event - /// - Parameter observer: The object observing the event. - /// - Parameter event: The event being listened to. - func removeHandler(observer: Any, event: ProviderEvent) } diff --git a/Sources/OpenFeature/EventHandler.swift b/Sources/OpenFeature/EventHandler.swift new file mode 100644 index 0000000..f1a5680 --- /dev/null +++ b/Sources/OpenFeature/EventHandler.swift @@ -0,0 +1,28 @@ +import Foundation +import Combine + +public class EventHandler: EventSender, EventPublisher { + private let eventState: CurrentValueSubject + + public init(_ state: ProviderEvent) { + eventState = CurrentValueSubject(ProviderEvent.stale) + } + + public func observe() -> AnyPublisher { + return eventState.eraseToAnyPublisher() + } + + public func send( + _ event: ProviderEvent + ) { + eventState.send(event) + } +} + +public protocol EventPublisher { + func observe() -> AnyPublisher +} + +public protocol EventSender { + func send(_ event: ProviderEvent) +} diff --git a/Sources/OpenFeature/OpenFeatureAPI.swift b/Sources/OpenFeature/OpenFeatureAPI.swift index a5535a3..4da3415 100644 --- a/Sources/OpenFeature/OpenFeatureAPI.swift +++ b/Sources/OpenFeature/OpenFeatureAPI.swift @@ -1,13 +1,20 @@ import Foundation +import Combine /// A global singleton which holds base configuration for the OpenFeature library. /// Configuration here will be shared across all ``Client``s. public class OpenFeatureAPI { - private var _provider: FeatureProvider? + private var _provider: FeatureProvider? { + get { + providerSubject.value + } + set { + providerSubject.send(newValue) + } + } private var _context: EvaluationContext? private(set) var hooks: [any Hook] = [] - - private let providerNotificationCentre = NotificationCenter() + private var providerSubject = CurrentValueSubject(nil) /// The ``OpenFeatureAPI`` singleton static public let shared = OpenFeatureAPI() @@ -24,7 +31,6 @@ public class OpenFeatureAPI { if let context = initialContext { self._context = context } - provider.initialize(initialContext: self._context) } @@ -65,41 +71,17 @@ public class OpenFeatureAPI { public func clearHooks() { self.hooks.removeAll() } -} -// MARK: Provider Events - -extension OpenFeatureAPI { - public func addHandler(observer: Any, selector: Selector, event: ProviderEvent) { - providerNotificationCentre.addObserver( - observer, - selector: selector, - name: event.notification, - object: nil - ) - } - - public func removeHandler(observer: Any, event: ProviderEvent) { - providerNotificationCentre.removeObserver(observer, name: event.notification, object: nil) - } - - public func emitEvent( - _ event: ProviderEvent, - provider: FeatureProvider, - error: Error? = nil, - details: [AnyHashable: Any]? = nil - ) { - var userInfo: [AnyHashable: Any] = [:] - userInfo[providerEventDetailsKeyProvider] = provider - - if let error { - userInfo[providerEventDetailsKeyError] = error + public func observe() -> AnyPublisher { + return providerSubject.map { provider in + if let provider = provider { + return provider.observe() + } else { + return Empty() + .eraseToAnyPublisher() + } } - - if let details { - userInfo.merge(details) { $1 } // Merge, keeping value from `details` if any conflicts - } - - providerNotificationCentre.post(name: event.notification, object: nil, userInfo: userInfo) + .switchToLatest() + .eraseToAnyPublisher() } } diff --git a/Sources/OpenFeature/OpenFeatureClient.swift b/Sources/OpenFeature/OpenFeatureClient.swift index e5fd63e..cdd933f 100644 --- a/Sources/OpenFeature/OpenFeatureClient.swift +++ b/Sources/OpenFeature/OpenFeatureClient.swift @@ -14,15 +14,11 @@ public class OpenFeatureClient: Client { private var hookSupport = HookSupport() private var logger = Logger() - private let providerNotificationCentre = NotificationCenter() - public init(openFeatureApi: OpenFeatureAPI, name: String?, version: String?) { self.openFeatureApi = openFeatureApi self.name = name self.version = version self.metadata = Metadata(name: name) - - subscribeToAllProviderEvents() } public func addHooks(_ hooks: any Hook...) { @@ -200,42 +196,3 @@ extension OpenFeatureClient { throw OpenFeatureError.generalError(message: "Unable to match default value type with flag value type") } } - -// MARK: Events - -extension OpenFeatureClient { - public func subscribeToAllProviderEvents() { - ProviderEvent.allCases.forEach { event in - OpenFeatureAPI.shared.addHandler( - observer: self, - selector: #selector(handleProviderEvent(notification:)), - event: event) - } - } - - public func unsubscribeFromAllProviderEvents() { - ProviderEvent.allCases.forEach { event in - OpenFeatureAPI.shared.removeHandler(observer: self, event: event) - } - } - - @objc public func handleProviderEvent(notification: Notification) { - var userInfo: [AnyHashable: Any] = notification.userInfo ?? [:] - userInfo[providerEventDetailsKeyClient] = self - - providerNotificationCentre.post(name: notification.name, object: nil, userInfo: userInfo) - } - - public func addHandler(observer: Any, selector: Selector, event: ProviderEvent) { - providerNotificationCentre.addObserver( - observer, - selector: selector, - name: event.notification, - object: nil - ) - } - - public func removeHandler(observer: Any, event: ProviderEvent) { - providerNotificationCentre.removeObserver(observer, name: event.notification, object: nil) - } -} diff --git a/Sources/OpenFeature/Provider/FeatureProvider.swift b/Sources/OpenFeature/Provider/FeatureProvider.swift index 44a5c7b..a5af609 100644 --- a/Sources/OpenFeature/Provider/FeatureProvider.swift +++ b/Sources/OpenFeature/Provider/FeatureProvider.swift @@ -1,7 +1,7 @@ import Foundation /// The interface implemented by upstream flag providers to resolve flags for their service. -public protocol FeatureProvider { +public protocol FeatureProvider: EventPublisher { var hooks: [any Hook] { get } var metadata: ProviderMetadata { get } diff --git a/Sources/OpenFeature/Provider/NoOpProvider.swift b/Sources/OpenFeature/Provider/NoOpProvider.swift index 7727d9e..6f35cbc 100644 --- a/Sources/OpenFeature/Provider/NoOpProvider.swift +++ b/Sources/OpenFeature/Provider/NoOpProvider.swift @@ -1,8 +1,10 @@ import Foundation +import Combine /// A ``FeatureProvider`` that simply returns the default values passed to it. class NoOpProvider: FeatureProvider { public static let passedInDefault = "Passed in default" + private let eventHandler = EventHandler(.ready) public enum Mode { case normal @@ -13,11 +15,11 @@ class NoOpProvider: FeatureProvider { var hooks: [any Hook] = [] func onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext) { - OpenFeatureAPI.shared.emitEvent(.configurationChanged, provider: self) + eventHandler.send(.ready) } func initialize(initialContext: EvaluationContext?) { - OpenFeatureAPI.shared.emitEvent(.ready, provider: self) + eventHandler.send(.ready) } func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws @@ -64,6 +66,10 @@ class NoOpProvider: FeatureProvider { return ProviderEvaluation( value: defaultValue, variant: NoOpProvider.passedInDefault, reason: Reason.defaultReason.rawValue) } + + func observe() -> AnyPublisher { + return eventHandler.observe() + } } extension NoOpProvider { diff --git a/Sources/OpenFeature/Provider/ProviderEvents.swift b/Sources/OpenFeature/Provider/ProviderEvents.swift index 133ec7a..30f44cf 100644 --- a/Sources/OpenFeature/Provider/ProviderEvents.swift +++ b/Sources/OpenFeature/Provider/ProviderEvents.swift @@ -9,8 +9,4 @@ public enum ProviderEvent: String, CaseIterable { case error = "PROVIDER_ERROR" case configurationChanged = "PROVIDER_CONFIGURATION_CHANGED" case stale = "PROVIDER_STALE" - - var notification: NSNotification.Name { - NSNotification.Name(rawValue) - } } diff --git a/Tests/OpenFeatureTests/DeveloperExperienceTests.swift b/Tests/OpenFeatureTests/DeveloperExperienceTests.swift index 908af80..64e9ded 100644 --- a/Tests/OpenFeatureTests/DeveloperExperienceTests.swift +++ b/Tests/OpenFeatureTests/DeveloperExperienceTests.swift @@ -19,6 +19,36 @@ final class DeveloperExperienceTests: XCTestCase { XCTAssertFalse(flagValue) } + func testObserveGlobalEvents() { + let readyExpectation = XCTestExpectation(description: "Ready") + let errorExpectation = XCTestExpectation(description: "Error") + let staleExpectation = XCTestExpectation(description: "Stale") + var eventState = OpenFeatureAPI.shared.observe().sink { event in + switch event { + case ProviderEvent.ready: + readyExpectation.fulfill() + case ProviderEvent.error: + errorExpectation.fulfill() + case ProviderEvent.stale: + staleExpectation.fulfill() + default: + XCTFail("Unexpected event") + } + } + let provider = DoSomethingProvider() + OpenFeatureAPI.shared.setProvider(provider: provider) + wait(for: [readyExpectation], timeout: 5) + + // Clearing the Provider shouldn't send further global events from it + // Dropping the first event, which reflects the current state before clearing + eventState = OpenFeatureAPI.shared.observe().dropFirst().sink { _ in + XCTFail("Unexpected event") + } + OpenFeatureAPI.shared.clearProvider() + provider.initialize(initialContext: MutableContext(attributes: ["Test": Value.string("Test")])) + XCTAssertNotNil(eventState) + } + func testClientHooks() { OpenFeatureAPI.shared.setProvider(provider: NoOpProvider()) let client = OpenFeatureAPI.shared.getClient() diff --git a/Tests/OpenFeatureTests/FlagEvaluationTests.swift b/Tests/OpenFeatureTests/FlagEvaluationTests.swift index c9da8f0..21d0e07 100644 --- a/Tests/OpenFeatureTests/FlagEvaluationTests.swift +++ b/Tests/OpenFeatureTests/FlagEvaluationTests.swift @@ -1,19 +1,12 @@ import Foundation import XCTest +import Combine @testable import OpenFeature final class FlagEvaluationTests: XCTestCase { override func setUp() { super.setUp() - - OpenFeatureAPI.shared.addHandler( - observer: self, selector: #selector(readyEventEmitted(notification:)), event: .ready - ) - - OpenFeatureAPI.shared.addHandler( - observer: self, selector: #selector(errorEventEmitted(notification:)), event: .error - ) } func testSingletonPersists() { @@ -23,7 +16,6 @@ final class FlagEvaluationTests: XCTestCase { func testApiSetsProvider() { let provider = NoOpProvider() OpenFeatureAPI.shared.setProvider(provider: provider) - XCTAssertTrue((OpenFeatureAPI.shared.getProvider() as? NoOpProvider) === provider) } @@ -64,12 +56,28 @@ final class FlagEvaluationTests: XCTestCase { } func testSimpleFlagEvaluation() { - OpenFeatureAPI.shared.setProvider(provider: DoSomethingProvider()) + let provider = DoSomethingProvider() + let readyExpectation = XCTestExpectation(description: "Ready") + let errorExpectation = XCTestExpectation(description: "Error") + let staleExpectation = XCTestExpectation(description: "Stale") + let eventState = provider.observe().sink { event in + switch event { + case ProviderEvent.ready: + readyExpectation.fulfill() + case ProviderEvent.error: + errorExpectation.fulfill() + case ProviderEvent.stale: + staleExpectation.fulfill() + default: + XCTFail("Unexpected event") + } + } + + wait(for: [staleExpectation], timeout: 5) + OpenFeatureAPI.shared.setProvider(provider: provider) wait(for: [readyExpectation], timeout: 5) - let client = OpenFeatureAPI.shared.getClient() let key = "key" - XCTAssertEqual(client.getValue(key: key, defaultValue: false), true) XCTAssertEqual(client.getValue(key: key, defaultValue: false), true) XCTAssertEqual( @@ -100,15 +108,31 @@ final class FlagEvaluationTests: XCTestCase { XCTAssertEqual(value, .null) value = client.getValue(key: key, defaultValue: .structure([:]), options: FlagEvaluationOptions()) XCTAssertEqual(value, .null) + XCTAssertNotNil(eventState) } func testDetailedFlagEvaluation() async { - OpenFeatureAPI.shared.setProvider(provider: DoSomethingProvider()) - wait(for: [readyExpectation], timeout: 5) + let provider = DoSomethingProvider() + let readyExpectation = XCTestExpectation(description: "Ready") + let errorExpectation = XCTestExpectation(description: "Error") + let staleExpectation = XCTestExpectation(description: "Stale") + let eventState = provider.observe().sink { event in + switch event { + case ProviderEvent.ready: + readyExpectation.fulfill() + case ProviderEvent.error: + errorExpectation.fulfill() + case ProviderEvent.stale: + staleExpectation.fulfill() + default: + XCTFail("Unexpected event") + } + } + OpenFeatureAPI.shared.setProvider(provider: provider) + wait(for: [readyExpectation], timeout: 5) let client = OpenFeatureAPI.shared.getClient() let key = "key" - let booleanDetails = FlagEvaluationDetails(flagKey: key, value: true, variant: nil) XCTAssertEqual(client.getDetails(key: key, defaultValue: false), booleanDetails) XCTAssertEqual(client.getDetails(key: key, defaultValue: false), booleanDetails) @@ -145,10 +169,28 @@ final class FlagEvaluationTests: XCTestCase { client.getDetails( key: key, defaultValue: .structure([:]), options: FlagEvaluationOptions()), objectDetails) + XCTAssertNotNil(eventState) } func testHooksAreFired() async { - OpenFeatureAPI.shared.setProvider(provider: NoOpProvider()) + let provider = NoOpProvider() + let readyExpectation = XCTestExpectation(description: "Ready") + let errorExpectation = XCTestExpectation(description: "Error") + let staleExpectation = XCTestExpectation(description: "Stale") + let eventState = provider.observe().sink { event in + switch event { + case ProviderEvent.ready: + readyExpectation.fulfill() + case ProviderEvent.error: + errorExpectation.fulfill() + case ProviderEvent.stale: + staleExpectation.fulfill() + default: + XCTFail("Unexpected event") + } + } + + OpenFeatureAPI.shared.setProvider(provider: provider) wait(for: [readyExpectation], timeout: 5) let client = OpenFeatureAPI.shared.getClient() @@ -164,10 +206,27 @@ final class FlagEvaluationTests: XCTestCase { XCTAssertEqual(clientHook.beforeCalled, 1) XCTAssertEqual(invocationHook.beforeCalled, 1) + XCTAssertNotNil(eventState) } func testBrokenProvider() { - OpenFeatureAPI.shared.setProvider(provider: AlwaysBrokenProvider()) + let provider = AlwaysBrokenProvider() + let readyExpectation = XCTestExpectation(description: "Ready") + let errorExpectation = XCTestExpectation(description: "Error") + let staleExpectation = XCTestExpectation(description: "Stale") + let eventState = provider.observe().sink { event in + switch event { + case ProviderEvent.ready: + readyExpectation.fulfill() + case ProviderEvent.error: + errorExpectation.fulfill() + case ProviderEvent.stale: + staleExpectation.fulfill() + default: + XCTFail("Unexpected event") + } + } + OpenFeatureAPI.shared.setProvider(provider: provider) wait(for: [errorExpectation], timeout: 5) let client = OpenFeatureAPI.shared.getClient() @@ -178,6 +237,7 @@ final class FlagEvaluationTests: XCTestCase { XCTAssertEqual(details.errorCode, .flagNotFound) XCTAssertEqual(details.reason, Reason.error.rawValue) XCTAssertEqual(details.errorMessage, "Could not find flag for key: testkey") + XCTAssertNotNil(eventState) } func testClientMetadata() { @@ -187,17 +247,4 @@ final class FlagEvaluationTests: XCTestCase { let client = OpenFeatureAPI.shared.getClient(name: "test", version: nil) XCTAssertEqual(client.metadata.name, "test") } - - // MARK: Event Handlers - let readyExpectation = XCTestExpectation(description: "Ready") - - func readyEventEmitted(notification: NSNotification) { - readyExpectation.fulfill() - } - - let errorExpectation = XCTestExpectation(description: "Error") - - func errorEventEmitted(notification: NSNotification) { - errorExpectation.fulfill() - } } diff --git a/Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift b/Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift index 68057df..f660e7a 100644 --- a/Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift +++ b/Tests/OpenFeatureTests/Helpers/AlwaysBrokenProvider.swift @@ -1,19 +1,19 @@ import Foundation +import Combine @testable import OpenFeature class AlwaysBrokenProvider: FeatureProvider { var metadata: ProviderMetadata = AlwaysBrokenMetadata() var hooks: [any Hook] = [] + private let eventHandler = EventHandler(.stale) func onContextSet(oldContext: OpenFeature.EvaluationContext?, newContext: OpenFeature.EvaluationContext) { - let error = OpenFeatureError.generalError(message: "Always Fails") - OpenFeatureAPI.shared.emitEvent(.error, provider: self, error: error) + eventHandler.send(.error) } func initialize(initialContext: OpenFeature.EvaluationContext?) { - let error = OpenFeatureError.generalError(message: "Always Fails") - OpenFeatureAPI.shared.emitEvent(.error, provider: self, error: error) + eventHandler.send(.error) } func getBooleanEvaluation(key: String, defaultValue: Bool, context: EvaluationContext?) throws @@ -45,6 +45,10 @@ class AlwaysBrokenProvider: FeatureProvider { { throw OpenFeatureError.flagNotFoundError(key: key) } + + func observe() -> AnyPublisher { + eventHandler.observe() + } } extension AlwaysBrokenProvider { diff --git a/Tests/OpenFeatureTests/Helpers/DoSomethingProvider.swift b/Tests/OpenFeatureTests/Helpers/DoSomethingProvider.swift index acbfff2..c58a4e2 100644 --- a/Tests/OpenFeatureTests/Helpers/DoSomethingProvider.swift +++ b/Tests/OpenFeatureTests/Helpers/DoSomethingProvider.swift @@ -1,15 +1,18 @@ import Foundation import OpenFeature +import Combine class DoSomethingProvider: FeatureProvider { public static let name = "Something" + private let eventHandler = EventHandler(.ready) + private var holdit: AnyCancellable? func onContextSet(oldContext: OpenFeature.EvaluationContext?, newContext: OpenFeature.EvaluationContext) { - OpenFeatureAPI.shared.emitEvent(.configurationChanged, provider: self) + eventHandler.send(.ready) } func initialize(initialContext: OpenFeature.EvaluationContext?) { - OpenFeatureAPI.shared.emitEvent(.ready, provider: self) + eventHandler.send(.ready) } var hooks: [any OpenFeature.Hook] = [] @@ -55,6 +58,10 @@ class DoSomethingProvider: FeatureProvider { return ProviderEvaluation(value: .null) } + func observe() -> AnyPublisher { + eventHandler.observe() + } + public struct DoMetadata: ProviderMetadata { public var name: String? = DoSomethingProvider.name } diff --git a/Tests/OpenFeatureTests/HookSpecTests.swift b/Tests/OpenFeatureTests/HookSpecTests.swift index 9fe9304..174dee1 100644 --- a/Tests/OpenFeatureTests/HookSpecTests.swift +++ b/Tests/OpenFeatureTests/HookSpecTests.swift @@ -6,18 +6,26 @@ import XCTest final class HookSpecTests: XCTestCase { override func setUp() { super.setUp() - - OpenFeatureAPI.shared.addHandler( - observer: self, selector: #selector(readyEventEmitted(notification:)), event: .ready - ) - - OpenFeatureAPI.shared.addHandler( - observer: self, selector: #selector(errorEventEmitted(notification:)), event: .error - ) } func testNoErrorHookCalled() { - OpenFeatureAPI.shared.setProvider(provider: NoOpProvider()) + let provider = NoOpProvider() + let readyExpectation = XCTestExpectation(description: "Ready") + let errorExpectation = XCTestExpectation(description: "Error") + let staleExpectation = XCTestExpectation(description: "Stale") + let eventState = provider.observe().sink { event in + switch event { + case ProviderEvent.ready: + readyExpectation.fulfill() + case ProviderEvent.error: + errorExpectation.fulfill() + case ProviderEvent.stale: + staleExpectation.fulfill() + default: + XCTFail("Unexpected event") + } + } + OpenFeatureAPI.shared.setProvider(provider: provider) wait(for: [readyExpectation], timeout: 5) let client = OpenFeatureAPI.shared.getClient() @@ -33,10 +41,27 @@ final class HookSpecTests: XCTestCase { XCTAssertEqual(hook.afterCalled, 1) XCTAssertEqual(hook.errorCalled, 0) XCTAssertEqual(hook.finallyAfterCalled, 1) + XCTAssertNotNil(eventState) } func testErrorHookButNoAfterCalled() { - OpenFeatureAPI.shared.setProvider(provider: AlwaysBrokenProvider()) + let provider = AlwaysBrokenProvider() + let readyExpectation = XCTestExpectation(description: "Ready") + let errorExpectation = XCTestExpectation(description: "Error") + let staleExpectation = XCTestExpectation(description: "Stale") + let eventState = provider.observe().sink { event in + switch event { + case ProviderEvent.ready: + readyExpectation.fulfill() + case ProviderEvent.error: + errorExpectation.fulfill() + case ProviderEvent.stale: + staleExpectation.fulfill() + default: + XCTFail("Unexpected event") + } + } + OpenFeatureAPI.shared.setProvider(provider: provider) wait(for: [errorExpectation], timeout: 5) let client = OpenFeatureAPI.shared.getClient() @@ -51,6 +76,7 @@ final class HookSpecTests: XCTestCase { XCTAssertEqual(hook.afterCalled, 0) XCTAssertEqual(hook.errorCalled, 1) XCTAssertEqual(hook.finallyAfterCalled, 1) + XCTAssertNotNil(eventState) } func testHookEvaluationOrder() { @@ -62,6 +88,21 @@ final class HookSpecTests: XCTestCase { let providerMock = NoOpProviderMock(hooks: [ BooleanHookMock(prefix: "provider", addEval: addEval) ]) + let readyExpectation = XCTestExpectation(description: "Ready") + let errorExpectation = XCTestExpectation(description: "Error") + let staleExpectation = XCTestExpectation(description: "Stale") + let eventState = providerMock.observe().sink { event in + switch event { + case ProviderEvent.ready: + readyExpectation.fulfill() + case ProviderEvent.error: + errorExpectation.fulfill() + case ProviderEvent.stale: + staleExpectation.fulfill() + default: + XCTFail("Unexpected event") + } + } OpenFeatureAPI.shared.setProvider(provider: providerMock) wait(for: [readyExpectation], timeout: 5) @@ -90,19 +131,7 @@ final class HookSpecTests: XCTestCase { "client finallyAfter", "api finallyAfter", ]) - } - - // MARK: Event Handlers - let readyExpectation = XCTestExpectation(description: "Ready") - - func readyEventEmitted(notification: NSNotification) { - readyExpectation.fulfill() - } - - let errorExpectation = XCTestExpectation(description: "Error") - - func errorEventEmitted(notification: NSNotification) { - errorExpectation.fulfill() + XCTAssertNotNil(eventState) } } diff --git a/Tests/OpenFeatureTests/OpenFeatureClientTests.swift b/Tests/OpenFeatureTests/OpenFeatureClientTests.swift index 6c0b38f..1b29f07 100644 --- a/Tests/OpenFeatureTests/OpenFeatureClientTests.swift +++ b/Tests/OpenFeatureTests/OpenFeatureClientTests.swift @@ -21,46 +21,4 @@ final class OpenFeatureClientTests: XCTestCase { let doubleDetails = client.getDetails(key: "key", defaultValue: 123.1) XCTAssertEqual(doubleDetails.value, 12_310) } - - func testProviderEvents() { - setupExpectations() - - let provider = DoSomethingProvider() - OpenFeatureAPI.shared.setProvider(provider: provider) - - let client = OpenFeatureAPI.shared.getClient() - ProviderEvent.allCases.forEach { event in - client.addHandler(observer: self, selector: #selector(eventEmitted(notification:)), event: event) - - OpenFeatureAPI.shared.emitEvent(event, provider: provider) - if let expectation = eventExpectations[event] { - wait(for: [expectation], timeout: 5) - } else { - XCTFail("No expectation for provider event: \(event)") - } - } - } - - // MARK: Event Handlers - private var eventExpectations: [ProviderEvent: XCTestExpectation] = [:] - - func setupExpectations() { - ProviderEvent.allCases.forEach { event in - eventExpectations[event] = XCTestExpectation(description: event.rawValue) - } - } - - func eventEmitted(notification: NSNotification) { - guard let providerEvent = ProviderEvent(rawValue: notification.name.rawValue) else { - XCTFail("Unexpected provider event: \(notification.name)") - return - } - - guard let expectation = eventExpectations[providerEvent] else { - XCTFail("No expectation for provider event: \(providerEvent)") - return - } - - expectation.fulfill() - } } diff --git a/Tests/OpenFeatureTests/ProviderEventsTests.swift b/Tests/OpenFeatureTests/ProviderEventsTests.swift index ed8218b..648ae9f 100644 --- a/Tests/OpenFeatureTests/ProviderEventsTests.swift +++ b/Tests/OpenFeatureTests/ProviderEventsTests.swift @@ -5,26 +5,18 @@ import XCTest final class ProviderEventsTests: XCTestCase { let provider = DoSomethingProvider() - func testReadyEventEmitted() { - OpenFeatureAPI.shared.addHandler( - observer: self, selector: #selector(readyEventEmitted(notification:)), event: .ready - ) - + func testReadyEventSent() { + let readyExpectation = XCTestExpectation(description: "Ready") + let eventState = provider + .observe() + .filter { event in + event == ProviderEvent.ready + } + .sink { _ in + readyExpectation.fulfill() + } OpenFeatureAPI.shared.setProvider(provider: provider) wait(for: [readyExpectation], timeout: 5) - } - - // MARK: Event Handlers - let readyExpectation = XCTestExpectation(description: "Ready") - - func readyEventEmitted(notification: NSNotification) { - readyExpectation.fulfill() - - let maybeProvider = notification.userInfo?[providerEventDetailsKeyProvider] - guard let eventProvider = maybeProvider as? DoSomethingProvider else { - XCTFail("Provider not passed in notification") - return - } - XCTAssertEqual(eventProvider.metadata.name, provider.metadata.name) + XCTAssertNotNil(eventState) } }