diff --git a/Package.resolved b/Package.resolved index 7e4ae14..049ce9d 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/splitio/ios-client.git", "state": { "branch": null, - "revision": "0833365635d3019f1283a9a39a5e3830f41daf34", - "version": "3.4.1" + "revision": "73b8b0d1e94f13383eee534cfea4ef657760f0ae", + "version": "3.4.2" } }, { diff --git a/Package.swift b/Package.swift index 807d6da..99bd63f 100644 --- a/Package.swift +++ b/Package.swift @@ -4,7 +4,7 @@ import PackageDescription let package = Package( - name: "swift-provider", + name: "SplitProvider", platforms: [ .iOS(.v14), .macOS(.v11), @@ -14,28 +14,28 @@ let package = Package( products: [ // Products define the executables and libraries a package produces, making them visible to other packages. .library( - name: "swift-provider", - targets: ["swift-provider"] + name: "SplitProvider", + targets: ["SplitProvider"] ) ], dependencies: [ // Dependencies declare other packages that this package depends on. .package(url: "https://github.com/open-feature/swift-sdk.git", from: "0.4.0"), - .package(url: "https://github.com/splitio/ios-client.git", from: "3.4.1"), + .package(url: "https://github.com/splitio/ios-client.git", from: "3.4.2"), ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. .target( - name: "swift-provider", + name: "SplitProvider", dependencies: [ .product(name: "OpenFeature", package: "swift-sdk"), .product(name: "Split", package: "ios-client"), ] ), .testTarget( - name: "swift-providerTests", - dependencies: ["swift-provider"] + name: "SplitProviderTests", + dependencies: ["SplitProvider"] ), ] ) diff --git a/Sources/SplitProvider/Constants.swift b/Sources/SplitProvider/Constants.swift new file mode 100644 index 0000000..c723e06 --- /dev/null +++ b/Sources/SplitProvider/Constants.swift @@ -0,0 +1,8 @@ +// Created by Martin Cardozo on 24/10/2025. + +internal enum Constants: String { + case API_KEY = "API_KEY" + case USER_KEY = "USER_KEY" + case CONTROL = "CONTROL" + case PROVIDER_NAME = "Split" +} diff --git a/Sources/SplitProvider/EventHandler.swift b/Sources/SplitProvider/EventHandler.swift new file mode 100644 index 0000000..66b2acc --- /dev/null +++ b/Sources/SplitProvider/EventHandler.swift @@ -0,0 +1,17 @@ +// Created by Martin Cardozo on 22/10/2025. + +import Combine +import OpenFeature + +// Used by Combine to propagate events +internal final class EventHandler { + private let subject = PassthroughSubject() + + var publisher: AnyPublisher { + subject.eraseToAnyPublisher() + } + + func send(_ event: OpenFeature.ProviderEvent) { + subject.send(event) + } +} diff --git a/Sources/SplitProvider/InitContext.swift b/Sources/SplitProvider/InitContext.swift new file mode 100644 index 0000000..35f3d79 --- /dev/null +++ b/Sources/SplitProvider/InitContext.swift @@ -0,0 +1,44 @@ +// Created by Martin Cardozo on 22/10/2025. + +import OpenFeature + +internal struct InitContext: OpenFeature.EvaluationContext { + let apiKey: String + let userKey: String + + var props: [String: OpenFeature.Value] { + [ + "API_KEY": .string(apiKey), + "USER_KEY": .string(userKey) + ] + } + + func keySet() -> Set { + [Constants.API_KEY.rawValue, Constants.USER_KEY.rawValue] + } + + func getTargetingKey() -> String { + userKey + } + + func deepCopy() -> any OpenFeature.EvaluationContext { + InitContext(apiKey: apiKey, userKey: userKey) + } + + func getValue(key: String) -> OpenFeature.Value? { + props[key] + } + + func asMap() -> [String: OpenFeature.Value] { + props + } + + func asObjectMap() -> [String: AnyHashable?] { + props.mapValues { value in + switch value { + case .string(let str): return str + default: return nil + } + } + } +} diff --git a/Sources/SplitProvider/SplitErrors.swift b/Sources/SplitProvider/SplitErrors.swift new file mode 100644 index 0000000..818f539 --- /dev/null +++ b/Sources/SplitProvider/SplitErrors.swift @@ -0,0 +1,8 @@ +// Created by Martin Cardozo on 22/10/2025. + +public enum SplitError: Int, Error { + case notImplemented + case missingInitContext + case missingInitData + case timeout +} diff --git a/Sources/SplitProvider/SplitProvider.swift b/Sources/SplitProvider/SplitProvider.swift new file mode 100644 index 0000000..354d714 --- /dev/null +++ b/Sources/SplitProvider/SplitProvider.swift @@ -0,0 +1,111 @@ +// Created by Martin Cardozo on 22/10/2025. + +import Combine +import Foundation +import OpenFeature +import Split + +public class SplitProvider: FeatureProvider { + + // Split Components + internal var splitClient: SplitClient? + internal var splitClientConfig: SplitClientConfig? + internal var factory: SplitFactory? + + // Open Feature Components + public var hooks: [any OpenFeature.Hook] = [] + public var metadata: any OpenFeature.ProviderMetadata = SplitProviderMetadata() + private let eventHandler = EventHandler() + private var splitContext: InitContext? + + // MARK: Custom Initialization + public init(_ config: SplitClientConfig? = nil) { + splitClientConfig = config + } + + public func initialize(initialContext: (any OpenFeature.EvaluationContext)?) async throws { + + guard let initialContext = initialContext else { + eventHandler.send(.error(errorCode: .invalidContext, message: "Initialization context is missing for Split provider.")) + throw SplitError.missingInitContext + } + + // 1. Unpack Context + let apiKeyValue = initialContext.getValue(key: Constants.API_KEY.rawValue)?.asString() + let userKeyValue = initialContext.getValue(key: Constants.USER_KEY.rawValue)?.asString() + guard let API_KEY = apiKeyValue, apiKeyValue != "" else { + eventHandler.send(.error(errorCode: .invalidContext, message: "Initialization data is missing for Split provider.")) + throw SplitError.missingInitData + } + guard let USER_KEY = userKeyValue, userKeyValue != "" else { + eventHandler.send(.error(errorCode: .targetingKeyMissing, message: "Initialization data is missing for Split provider.")) + throw SplitError.missingInitData + } + + // 2. Client setup + splitContext = InitContext(apiKey: API_KEY, userKey: USER_KEY) + let key: Key = Key(matchingKey: USER_KEY) + if factory == nil { + factory = DefaultSplitFactoryBuilder().setApiKey(API_KEY).setKey(key).setConfig(splitClientConfig ?? SplitClientConfig()).build() + } + splitClient = factory?.client + + // 3. Wait for SDK + await withCheckedContinuation { (continuation: CheckedContinuation) in + var didResume = false + + // Avoid crash by multiple countinuations + func resumeOnce(error: Bool = false) { + guard !didResume else { return } + didResume = true + + if error { + eventHandler.send(.error(errorCode: .general, message: "Provider timed out")) + } else { + continuation.resume() + } + } + + splitClient?.on(event: .sdkReady) { resumeOnce() } + splitClient?.on(event: .sdkReadyFromCache) { resumeOnce() } + splitClient?.on(event: .sdkReadyTimedOut) { resumeOnce(error: true) } + } + } + + // MARK: Context Change + public func onContextSet(oldContext: (any OpenFeature.EvaluationContext)?, newContext: any OpenFeature.EvaluationContext) async throws { + throw SplitError.notImplemented + } +} + +// MARK: Evaluation Methods +extension SplitProvider { + + public func getBooleanEvaluation(key: String, defaultValue: Bool, context: (any OpenFeature.EvaluationContext)?) throws -> OpenFeature.ProviderEvaluation { + ProviderEvaluation(value: false) + } + + public func getStringEvaluation(key: String, defaultValue: String, context: (any OpenFeature.EvaluationContext)?) throws -> OpenFeature.ProviderEvaluation { + ProviderEvaluation(value: splitClient?.getTreatment(key) ?? Constants.CONTROL.rawValue) + } + + public func getIntegerEvaluation(key: String, defaultValue: Int64, context: (any OpenFeature.EvaluationContext)?) throws -> OpenFeature.ProviderEvaluation { + ProviderEvaluation(value: 1) + } + + public func getDoubleEvaluation(key: String, defaultValue: Double, context: (any OpenFeature.EvaluationContext)?) throws -> OpenFeature.ProviderEvaluation { + ProviderEvaluation(value: 1.0) + } + + public func getObjectEvaluation(key: String, defaultValue: OpenFeature.Value, context: (any OpenFeature.EvaluationContext)?) throws -> OpenFeature.ProviderEvaluation { + throw SplitError.notImplemented + } + + public func observe() -> AnyPublisher { + eventHandler.publisher.eraseToAnyPublisher() + } +} + +struct SplitProviderMetadata: ProviderMetadata { + let name: String? = Constants.PROVIDER_NAME.rawValue +} diff --git a/Sources/swift-provider/SplitProvider.swift b/Sources/swift-provider/SplitProvider.swift deleted file mode 100644 index a13cffe..0000000 --- a/Sources/swift-provider/SplitProvider.swift +++ /dev/null @@ -1,63 +0,0 @@ -import Combine -import Foundation -import OpenFeature -import Split - -public class SplitProvider: FeatureProvider { - public var hooks: [any OpenFeature.Hook] = [] - - public var metadata: any OpenFeature.ProviderMetadata = SwiftProviderMetadata() - - public func initialize(initialContext: (any OpenFeature.EvaluationContext)?) async throws { - throw ProviderError.notImplemented - } - - public func onContextSet( - oldContext: (any OpenFeature.EvaluationContext)?, - newContext: any OpenFeature.EvaluationContext - ) async throws { - throw ProviderError.notImplemented - } - - public func getBooleanEvaluation( - key: String, defaultValue: Bool, context: (any OpenFeature.EvaluationContext)? - ) throws -> OpenFeature.ProviderEvaluation { - throw ProviderError.notImplemented - } - - public func getStringEvaluation( - key: String, defaultValue: String, context: (any OpenFeature.EvaluationContext)? - ) throws -> OpenFeature.ProviderEvaluation { - throw ProviderError.notImplemented - } - - public func getIntegerEvaluation( - key: String, defaultValue: Int64, context: (any OpenFeature.EvaluationContext)? - ) throws -> OpenFeature.ProviderEvaluation { - throw ProviderError.notImplemented - } - - public func getDoubleEvaluation( - key: String, defaultValue: Double, context: (any OpenFeature.EvaluationContext)? - ) throws -> OpenFeature.ProviderEvaluation { - throw ProviderError.notImplemented - } - - public func getObjectEvaluation( - key: String, defaultValue: OpenFeature.Value, context: (any OpenFeature.EvaluationContext)? - ) throws -> OpenFeature.ProviderEvaluation { - throw ProviderError.notImplemented - } - - public func observe() -> AnyPublisher { - return Empty().eraseToAnyPublisher() - } -} - -struct SwiftProviderMetadata: ProviderMetadata { - var name: String? = "Split" -} - -enum ProviderError: Error { - case notImplemented -} diff --git a/Tests/SplitProviderTests/InitContextTests.swift b/Tests/SplitProviderTests/InitContextTests.swift new file mode 100644 index 0000000..c245528 --- /dev/null +++ b/Tests/SplitProviderTests/InitContextTests.swift @@ -0,0 +1,52 @@ +// Created by Martin Cardozo on 24/10/2025. + +import XCTest +import OpenFeature +@testable import SplitProvider + +final class InitContextTests: XCTestCase { + + var SUT: InitContext! + + override func setUp() { + SUT = InitContext(apiKey: "skhjcgkjhfgasdhka", userKey: "martin") + } + + override func tearDown() {} + + func testValues() { + XCTAssertEqual(SUT.getValue(key: Constants.API_KEY.rawValue)?.asString(), "skhjcgkjhfgasdhka") + XCTAssertEqual(SUT.getValue(key: Constants.USER_KEY.rawValue)?.asString(), "martin") + } + + func testKeySet() { + XCTAssertEqual(SUT.keySet(), [Constants.API_KEY.rawValue, Constants.USER_KEY.rawValue]) + } + + func testTargetingKey() { + XCTAssertEqual(SUT.getTargetingKey(), "martin") + } + + func testDeepCopy() { + let deepCopy = SUT.deepCopy() + + XCTAssertEqual(deepCopy.getValue(key: Constants.API_KEY.rawValue)?.asString(), "skhjcgkjhfgasdhka") + XCTAssertEqual(deepCopy.getValue(key: Constants.USER_KEY.rawValue)?.asString(), "martin") + } + + func testAsMap() { + let map = SUT.asMap() + + XCTAssertEqual(map[Constants.API_KEY.rawValue]?.asString(), "skhjcgkjhfgasdhka") + XCTAssertEqual(map[Constants.USER_KEY.rawValue]?.asString(), "martin") + } + + func testAsObjectMap() { + let asObjectMap = SUT.asObjectMap() + + XCTAssertEqual(asObjectMap[Constants.API_KEY.rawValue], "skhjcgkjhfgasdhka") + XCTAssertEqual(asObjectMap[Constants.USER_KEY.rawValue], "martin") + } +} + + diff --git a/Tests/SplitProviderTests/SplitProviderMocks.swift b/Tests/SplitProviderTests/SplitProviderMocks.swift new file mode 100644 index 0000000..6a87143 --- /dev/null +++ b/Tests/SplitProviderTests/SplitProviderMocks.swift @@ -0,0 +1,173 @@ +// Created by Martin Cardozo on 23/10/2025. + +import Split +import Foundation + +internal final class FactoryMock: SplitFactory { + + var client: any SplitClient = ClientMock() + var manager: any SplitManager = SplitManagerMock() + var userConsent: UserConsent = .granted + var version: String = "1" + + func client(key: Key) -> any SplitClient { ClientMock() } + func client(matchingKey: String) -> any SplitClient { ClientMock() } + func client(matchingKey: String, bucketingKey: String?) -> any SplitClient { ClientMock() } + func setUserConsent(enabled: Bool) {} + + // For testing + func getClient() -> ClientMock { + client as! ClientMock + } +} + +internal final class ClientMock: SplitClient { + var treatment = "Treatment" + var timeout = false + + init() { + print(":: Mock Client started") + } + + // MARK: Treatments + func getTreatment(_ split: String, attributes: [String : Any]?) -> String { + treatment + } + + func getTreatment(_ split: String) -> String { + treatment + } + + func getTreatments(splits: [String], attributes: [String : Any]?) -> [String : String] { + [treatment:treatment] + } + + func getTreatmentWithConfig(_ split: String) -> SplitResult { + SplitResult(treatment: treatment, config: nil) + } + + func getTreatmentWithConfig(_ split: String, attributes: [String : Any]?) -> SplitResult { + SplitResult(treatment: treatment, config: nil) + } + + func getTreatmentsWithConfig(splits: [String], attributes: [String : Any]?) -> [String : SplitResult] { + [treatment: SplitResult(treatment: treatment, config: nil)] + } + + func getTreatment(_ split: String, attributes: [String : Any]?, evaluationOptions: EvaluationOptions?) -> String { + treatment + } + + func getTreatments(splits: [String], attributes: [String : Any]?, evaluationOptions: EvaluationOptions?) -> [String : String] { + [treatment:treatment] + } + + func getTreatmentWithConfig(_ split: String, attributes: [String : Any]?, evaluationOptions: EvaluationOptions?) -> SplitResult { + SplitResult(treatment: treatment, config: nil) + } + + func getTreatmentsWithConfig(splits: [String], attributes: [String : Any]?, evaluationOptions: EvaluationOptions?) -> [String : SplitResult] { + [treatment: SplitResult(treatment: treatment, config: nil)] + } + + func getTreatmentsByFlagSet(_ flagSet: String, attributes: [String : Any]?) -> [String : String] { + [treatment:treatment] + } + + func getTreatmentsByFlagSets(_ flagSets: [String], attributes: [String : Any]?) -> [String : String] { + [treatment:treatment] + } + + func getTreatmentsWithConfigByFlagSet(_ flagSet: String, attributes: [String : Any]?) -> [String : SplitResult] { + [treatment: SplitResult(treatment: treatment, config: nil)] + } + + func getTreatmentsWithConfigByFlagSets(_ flagSets: [String], attributes: [String : Any]?) -> [String : SplitResult] { + [treatment: SplitResult(treatment: treatment, config: nil)] + } + + func getTreatmentsByFlagSet(_ flagSet: String, attributes: [String : Any]?, evaluationOptions: EvaluationOptions?) -> [String : String] { + [treatment: treatment] + } + + func getTreatmentsByFlagSets(_ flagSets: [String], attributes: [String : Any]?, evaluationOptions: EvaluationOptions?) -> [String : String] { + [treatment: treatment] + } + + func getTreatmentsWithConfigByFlagSet(_ flagSet: String, attributes: [String : Any]?, evaluationOptions: EvaluationOptions?) -> [String : SplitResult] { + [treatment: SplitResult(treatment: treatment, config: nil)] + } + + func getTreatmentsWithConfigByFlagSets(_ flagSets: [String], attributes: [String : Any]?, evaluationOptions: EvaluationOptions?) -> [String : SplitResult] { + [treatment: SplitResult(treatment: treatment, config: nil)] + } + + var events: [SplitEvent: ()->()] = [:] + + // MARK: Events + func on(event: SplitEvent, execute action: @escaping SplitAction) { + + events[event] = action + + if timeout { + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { + print(":: Mock Client :: SDK_TIMEOUT") + self.events[.sdkReadyTimedOut]!() + } + return + } + + switch event { + case .sdkUpdated: + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + print(":: Mock Client :: SDK_UPDATED") + self.events[.sdkUpdated]!() + } + case .sdkReady: + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + print(":: Mock Client :: SDK_READY") + self.events[.sdkReady]!() + } + case .sdkReadyTimedOut: + return + case .sdkReadyFromCache: + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + print(":: Mock Client :: SDK_READY_FROM_CACHE") + self.events[.sdkReadyFromCache]!() + } + } + } + + func on(event: SplitEvent, runInBackground: Bool, execute action: @escaping SplitAction) {} + + func on(event: SplitEvent, queue: DispatchQueue, execute action: @escaping SplitAction) {} + + // MARK: Attributes + func setAttribute(name: String, value: Any) -> Bool { true } + func getAttribute(name: String) -> Any? { nil } + func setAttributes(_ values: [String : Any]) -> Bool { true} + func getAttributes() -> [String : Any]? { nil } + func removeAttribute(name: String) -> Bool { true } + func clearAttributes() -> Bool { true } + + // MARK: Lifecycle + func flush() {} + func destroy() {} + func destroy(completion: (() -> Void)?) {} + + // MARK: Track + func track(trafficType: String, eventType: String, properties: [String : Any]?) -> Bool { true } + func track(trafficType: String, eventType: String, value: Double, properties: [String : Any]?) -> Bool { true } + func track(eventType: String, properties: [String : Any]?) -> Bool { true } + func track(eventType: String, value: Double, properties: [String : Any]?) -> Bool { true } + func track(trafficType: String, eventType: String) -> Bool { true } + func track(trafficType: String, eventType: String, value: Double) -> Bool { true } + func track(eventType: String) -> Bool { true } + func track(eventType: String, value: Double) -> Bool { true } +} + +internal final class SplitManagerMock: SplitManager { + var splits: [SplitView] = [] + var splitNames: [String] = [] + func split(featureName: String) -> SplitView? { nil } +} diff --git a/Tests/SplitProviderTests/SplitProviderTests.swift b/Tests/SplitProviderTests/SplitProviderTests.swift new file mode 100644 index 0000000..1008e4f --- /dev/null +++ b/Tests/SplitProviderTests/SplitProviderTests.swift @@ -0,0 +1,232 @@ +// Created by Martin Cardozo on 22/10/2025. + +import XCTest +import Combine +import Foundation +@testable import SplitProvider +@testable import OpenFeature +@testable import Split + +final class SplitProviderTests: XCTestCase { + + private var provider: SplitProvider! + private var providerCancellable: AnyCancellable? + + private let eventHandler = OpenFeature.EventHandler() + + override func setUp() {} + + override func tearDown() { + providerCancellable?.cancel() + } + + func testNameIsCorrect() { + XCTAssertTrue(SplitProvider().metadata.name == Constants.PROVIDER_NAME.rawValue) + } + + func testCorrectInitialization() { + + let readyExp = expectation(description: "SDK Ready") + let openFeatureExp = expectation(description: "OpenFeature Ready") + let nonErrorExp = expectation(description: "There should be no errors") + nonErrorExp.isInverted = true + + // Setup events observer + providerCancellable = OpenFeatureAPI.shared.observe().sink { event in + switch event { + case .ready: + readyExp.fulfill() + case .error(let errorCode, _): + nonErrorExp.fulfill() + default: + break + } + } + + let context = InitContext(apiKey: "sofd75fo7w6ao576oshf567jshdkfrbk746", userKey: "martin") + provider = SplitProvider() + provider.factory = FactoryMock() + + // Kickoff Provider + Task { + await OpenFeatureAPI.shared.setProviderAndWait(provider: provider, initialContext: context) + openFeatureExp.fulfill() + } + + wait(for: [readyExp, openFeatureExp, nonErrorExp], timeout: 4) + } + + func testMissingApiKey() { + + let openFeatureExp = expectation(description: "OpenFeature Ready") + var errorFired = false + + // Setup events observer + providerCancellable = OpenFeatureAPI.shared.observe().sink { event in + switch event { + case .ready: + break + case .error(let errorCode, _): + if errorCode == .invalidContext { + errorFired = true + } + default: + break + } + } + + let context = InitContext(apiKey: "", userKey: "martin") + provider = SplitProvider() + provider.factory = FactoryMock() + + // Kickoff Provider + Task { + await OpenFeatureAPI.shared.setProviderAndWait(provider: provider, initialContext: context) + openFeatureExp.fulfill() + } + + wait(for: [openFeatureExp], timeout: 5) + XCTAssertTrue(errorFired, "If there is no API key, an error should be fired") + } + + func testMissingUserKey() { + + let openFeatureExp = expectation(description: "OpenFeature Ready") + var errorFired = false + + // Setup events observer + providerCancellable = OpenFeatureAPI.shared.observe().sink { event in + switch event { + case .ready: + break + case .error(let errorCode, _): + if errorCode == .targetingKeyMissing { + errorFired = true + } + default: + break + } + } + + let context = InitContext(apiKey: "sofd75fo7w6ao576oshf567jshdkfrbk746", userKey: "") + provider = SplitProvider() + provider.factory = FactoryMock() + + // Kickoff Provider + Task { + await OpenFeatureAPI.shared.setProviderAndWait(provider: provider, initialContext: context) + openFeatureExp.fulfill() + } + + wait(for: [openFeatureExp], timeout: 5) + XCTAssertTrue(errorFired, "If there is no User key, an error should be fired") + } + + func testMissingInitContext() { + + let openFeatureExp = expectation(description: "OpenFeature Ready") + var errorFired = false + + provider = SplitProvider() + + // Setup events observer + providerCancellable = OpenFeatureAPI.shared.observe().sink { [weak self] event in + switch event { + case .ready: + self?.eval("mauro-test-flag") + case .error(let errorCode, _): + if errorCode == .invalidContext { + errorFired = true + } + default: + break + } + } + + provider = SplitProvider() + provider.factory = FactoryMock() + + // Kickoff Provider + Task { + await OpenFeatureAPI.shared.setProviderAndWait(provider: provider, initialContext: nil) + openFeatureExp.fulfill() + } + + wait(for: [openFeatureExp], timeout: 5) + XCTAssertTrue(errorFired, "If there is no initialContext, an error should be fired") + } + + func testInitializationWithConfig() { + + let readyExp = expectation(description: "SDK Ready") + let openFeatureExp = expectation(description: "OpenFeature Ready") + + // Config if needed + let context = InitContext(apiKey: "sofd75fo7w6ao576oshf567jshdkfrbk746", userKey: "martin") + let config = SplitClientConfig() + config.logLevel = .verbose + + provider = SplitProvider(config) + provider.factory = FactoryMock() + + // Setup events observer + providerCancellable = OpenFeatureAPI.shared.observe().sink { event in + switch event { + case .ready: + readyExp.fulfill() + case .error(_,_): + break + default: + break + } + } + + // Kickoff Provider + Task { + await OpenFeatureAPI.shared.setProviderAndWait(provider: provider, initialContext: context) + openFeatureExp.fulfill() + } + + wait(for: [openFeatureExp, readyExp], timeout: 5) + XCTAssertEqual(provider.splitClientConfig?.logLevel, .verbose, "SplitConfig should be correctly propagated") + } + + func testTimeOut() { + + let errorExp = expectation(description: "SDK should timeout") + + // Setup events observer + providerCancellable = OpenFeatureAPI.shared.observe().sink { event in + switch event { + case .ready: + break + case .error(let errorCode, let message): + if errorCode == .general && message == "Provider timed out" { + errorExp.fulfill() + } + default: + break + } + } + + let context = InitContext(apiKey: "sofd75fo7w6ao576oshf567jshdkfrbk746", userKey: "martin") + provider = SplitProvider() + let factory = FactoryMock() + factory.getClient().timeout = true // MARK: Fail point + provider.factory = factory + + // Kickoff Provider + Task { await OpenFeatureAPI.shared.setProviderAndWait(provider: provider, initialContext: context) } + + wait(for: [errorExp], timeout: 4) + } + + fileprivate func eval(_ flag: String) { + do { + let eval = try provider.getStringEvaluation(key: flag, defaultValue: "", context: nil) + print("Flag value:", eval.value) + } catch { + print("Provider error:", error) + } + } +} diff --git a/Tests/swift-providerTests/SplitProviderTests.swift b/Tests/swift-providerTests/SplitProviderTests.swift deleted file mode 100644 index 9147446..0000000 --- a/Tests/swift-providerTests/SplitProviderTests.swift +++ /dev/null @@ -1,13 +0,0 @@ -import XCTest -@testable import swift_provider -@testable import OpenFeature - -final class SplitProviderTests: XCTestCase { - func testSplitProviderImplementsFeatureProvider() throws { - XCTAssertTrue(SplitProvider() is FeatureProvider) - } - - func testNameIsCorrect() { - XCTAssertTrue(SplitProvider().metadata.name == "Split") - } -}