diff --git a/Sources/KlaviyoSwift/KlaviyoAPI.swift b/Sources/KlaviyoSwift/KlaviyoAPI.swift index e61bb31c..347701e1 100644 --- a/Sources/KlaviyoSwift/KlaviyoAPI.swift +++ b/Sources/KlaviyoSwift/KlaviyoAPI.swift @@ -39,12 +39,12 @@ struct KlaviyoAPI { static var requestRateLimited: (KlaviyoRequest) -> Void = { _ in } static var requestHttpError: (KlaviyoRequest, Int, Double) -> Void = { _, _, _ in } - var send: (KlaviyoRequest) async -> Result = { request in + var send: (KlaviyoRequest, Int) async -> Result = { request, attemptNumber in let start = Date() var urlRequest: URLRequest do { - urlRequest = try request.urlRequest() + urlRequest = try request.urlRequest(attemptNumber) } catch { requestFailed(request, error, 0.0) return .failure(.internalRequestError(error)) @@ -86,7 +86,7 @@ struct KlaviyoAPI { } extension KlaviyoAPI.KlaviyoRequest { - func urlRequest() throws -> URLRequest { + func urlRequest(_ attemptNumber: Int = StateManagementConstants.initialAttempt) throws -> URLRequest { guard let url = url else { throw KlaviyoAPI.KlaviyoAPIError.internalError("Invalid url string. API URL: \(environment.analytics.apiURL)") } @@ -97,6 +97,7 @@ extension KlaviyoAPI.KlaviyoRequest { } request.httpBody = body request.httpMethod = "POST" + request.setValue("\(attemptNumber)/50", forHTTPHeaderField: "X-Klaviyo-Attempt-Count") return request } diff --git a/Sources/KlaviyoSwift/KlaviyoState.swift b/Sources/KlaviyoSwift/KlaviyoState.swift index 8eba10ee..6f9a72d4 100644 --- a/Sources/KlaviyoSwift/KlaviyoState.swift +++ b/Sources/KlaviyoSwift/KlaviyoState.swift @@ -98,7 +98,7 @@ struct KlaviyoState: Equatable, Codable { var initalizationState = InitializationState.uninitialized var flushing = false var flushInterval = StateManagementConstants.wifiFlushInterval - var retryInfo = RetryInfo.retry(0) + var retryInfo = RetryInfo.retry(StateManagementConstants.initialAttempt) var pendingRequests: [PendingRequest] = [] var pendingProfile: [Profile.ProfileKey: AnyEncodable]? diff --git a/Sources/KlaviyoSwift/NetworkSession.swift b/Sources/KlaviyoSwift/NetworkSession.swift index 0afe5a7f..d39868d4 100644 --- a/Sources/KlaviyoSwift/NetworkSession.swift +++ b/Sources/KlaviyoSwift/NetworkSession.swift @@ -14,7 +14,8 @@ func createEmphemeralSession(protocolClasses: [AnyClass] = URLProtocolOverrides. "User-Agent": NetworkSession.defaultUserAgent, "revision": NetworkSession.currentApiRevision, "content-type": NetworkSession.applicationJson, - "accept": NetworkSession.applicationJson + "accept": NetworkSession.applicationJson, + "X-Klaviyo-Mobile": NetworkSession.mobileHeader ] configuration.protocolClasses = protocolClasses return URLSession(configuration: configuration) @@ -24,6 +25,7 @@ struct NetworkSession { fileprivate static let currentApiRevision = "2023-07-15" fileprivate static let applicationJson = "application/json" fileprivate static let acceptedEncodings = ["br", "gzip", "deflate"] + fileprivate static let mobileHeader = "1" static let defaultUserAgent = { () -> String in let appContext = environment.analytics.appContextInfo() diff --git a/Sources/KlaviyoSwift/SDKRequestIterator.swift b/Sources/KlaviyoSwift/SDKRequestIterator.swift index 555c667a..78c0f7fe 100644 --- a/Sources/KlaviyoSwift/SDKRequestIterator.swift +++ b/Sources/KlaviyoSwift/SDKRequestIterator.swift @@ -137,6 +137,7 @@ public func requestIterator() -> AsyncStream { KlaviyoAPI.requestFailed = { _, _, _ in } KlaviyoAPI.requestCompleted = { _, _, _ in } KlaviyoAPI.requestHttpError = { _, _, _ in } + KlaviyoAPI.requestRateLimited = { _ in } } KlaviyoAPI.requestStarted = { request in continuation.yield(SDKRequest.fromAPIRequest(request: request, response: .inProgress)) @@ -151,5 +152,8 @@ public func requestIterator() -> AsyncStream { KlaviyoAPI.requestHttpError = { request, statusCode, duration in continuation.yield(SDKRequest.fromAPIRequest(request: request, response: .httpError(statusCode, duration))) } + KlaviyoAPI.requestRateLimited = { request in + continuation.yield(SDKRequest.fromAPIRequest(request: request, response: .reqeustError("Rate Limited", 0.0))) + } } } diff --git a/Sources/KlaviyoSwift/StateManagement.swift b/Sources/KlaviyoSwift/StateManagement.swift index a4e94ee0..d41b8260 100644 --- a/Sources/KlaviyoSwift/StateManagement.swift +++ b/Sources/KlaviyoSwift/StateManagement.swift @@ -21,6 +21,7 @@ enum StateManagementConstants { static let cellularFlushInterval = 30.0 static let wifiFlushInterval = 10.0 static let maxQueueSize = 200 + static let initialAttempt = 1 } enum RetryInfo: Equatable { @@ -293,7 +294,7 @@ struct KlaviyoReducer: ReducerProtocol { state.requestsInFlight.removeAll { inflightRequest in completedRequest.uuid == inflightRequest.uuid } - state.retryInfo = RetryInfo.retry(0) + state.retryInfo = RetryInfo.retry(StateManagementConstants.initialAttempt) if state.requestsInFlight.isEmpty { state.flushing = false return .none @@ -313,8 +314,13 @@ struct KlaviyoReducer: ReducerProtocol { return .none } let retryInfo = state.retryInfo - return .run { send in - let result = await environment.analytics.klaviyoAPI.send(request) + var numAttempts = 1 + if case let .retry(attempts) = retryInfo { + numAttempts = attempts + } + + return .run { [numAttempts] send in + let result = await environment.analytics.klaviyoAPI.send(request, numAttempts) switch result { case .success: // TODO: may want to inspect response further. @@ -361,7 +367,7 @@ struct KlaviyoReducer: ReducerProtocol { switch retryInfo { case let .retry(count): exceededRetries = count > ErrorHandlingConstants.maxRetries - state.retryInfo = .retry(exceededRetries ? 0 : count) + state.retryInfo = .retry(exceededRetries ? 1 : count) case let .retryWithBackoff(requestCount, totalCount, backOff): exceededRetries = requestCount > ErrorHandlingConstants.maxRetries state.retryInfo = .retryWithBackoff(requestCount: exceededRetries ? 0 : requestCount, totalRetryCount: totalCount, currentBackoff: backOff) diff --git a/Tests/KlaviyoSwiftTests/APIRequestErrorHandlingTests.swift b/Tests/KlaviyoSwiftTests/APIRequestErrorHandlingTests.swift index 445abc85..e29f9fff 100644 --- a/Tests/KlaviyoSwiftTests/APIRequestErrorHandlingTests.swift +++ b/Tests/KlaviyoSwiftTests/APIRequestErrorHandlingTests.swift @@ -11,21 +11,22 @@ import XCTest let TIMEOUT_NANOSECONDS: UInt64 = 10_000_000_000 // 10 seconds -@MainActor class APIRequestErrorHandlingTests: XCTestCase { + @MainActor override func setUp() async throws { environment = KlaviyoEnvironment.test() } // MARK: - http error + @MainActor func testSendRequestHttpFailureDequesRequest() async throws { var initialState = INITIALIZED_TEST_STATE() let request = initialState.buildProfileRequest(apiKey: initialState.apiKey!, anonymousId: initialState.anonymousId!) initialState.requestsInFlight = [request] let store = TestStore(initialState: initialState, reducer: KlaviyoReducer()) - environment.analytics.klaviyoAPI.send = { _ in .failure(.httpError(500, TEST_RETURN_DATA)) } + environment.analytics.klaviyoAPI.send = { _, _ in .failure(.httpError(500, TEST_RETURN_DATA)) } _ = await store.send(.sendRequest) @@ -35,13 +36,14 @@ class APIRequestErrorHandlingTests: XCTestCase { } } + @MainActor func testSendRequestHttpFailureForPhoneNumberResetsStateAndDequesRequest() async throws { var initialState = INITIALIZED_TEST_STATE_INVALID_PHONE() let request = initialState.buildProfileRequest(apiKey: initialState.apiKey!, anonymousId: initialState.anonymousId!) initialState.requestsInFlight = [request] let store = TestStore(initialState: initialState, reducer: KlaviyoReducer()) - environment.analytics.klaviyoAPI.send = { _ in .failure(.httpError(400, TEST_FAILURE_JSON_INVALID_PHONE_NUMBER.data(using: .utf8)!)) } + environment.analytics.klaviyoAPI.send = { _, _ in .failure(.httpError(400, TEST_FAILURE_JSON_INVALID_PHONE_NUMBER.data(using: .utf8)!)) } _ = await store.send(.sendRequest) @@ -53,17 +55,18 @@ class APIRequestErrorHandlingTests: XCTestCase { $0.flushing = false $0.queue = [] $0.requestsInFlight = [] - $0.retryInfo = .retry(0) + $0.retryInfo = .retry(1) } } + @MainActor func testSendRequestHttpFailureForEmailResetsStateAndDequesRequest() async throws { var initialState = INITIALIZED_TEST_STATE_INVALID_EMAIL() let request = initialState.buildProfileRequest(apiKey: initialState.apiKey!, anonymousId: initialState.anonymousId!) initialState.requestsInFlight = [request] let store = TestStore(initialState: initialState, reducer: KlaviyoReducer()) - environment.analytics.klaviyoAPI.send = { _ in .failure(.httpError(400, TEST_FAILURE_JSON_INVALID_EMAIL.data(using: .utf8)!)) } + environment.analytics.klaviyoAPI.send = { _, _ in .failure(.httpError(400, TEST_FAILURE_JSON_INVALID_EMAIL.data(using: .utf8)!)) } _ = await store.send(.sendRequest) @@ -75,12 +78,13 @@ class APIRequestErrorHandlingTests: XCTestCase { $0.flushing = false $0.queue = [] $0.requestsInFlight = [] - $0.retryInfo = .retry(0) + $0.retryInfo = .retry(1) } } // MARK: - network error + @MainActor func testSendRequestFailureIncrementsRetryCount() async throws { var initialState = INITIALIZED_TEST_STATE() let request = initialState.buildProfileRequest(apiKey: initialState.apiKey!, anonymousId: initialState.anonymousId!) @@ -88,18 +92,19 @@ class APIRequestErrorHandlingTests: XCTestCase { initialState.requestsInFlight = [request, request2] let store = TestStore(initialState: initialState, reducer: KlaviyoReducer()) - environment.analytics.klaviyoAPI.send = { _ in .failure(.networkError(NSError(domain: "foo", code: NSURLErrorCancelled))) } + environment.analytics.klaviyoAPI.send = { _, _ in .failure(.networkError(NSError(domain: "foo", code: NSURLErrorCancelled))) } _ = await store.send(.sendRequest) - await store.receive(.requestFailed(request, .retry(1)), timeout: TIMEOUT_NANOSECONDS) { + await store.receive(.requestFailed(request, .retry(2)), timeout: TIMEOUT_NANOSECONDS) { $0.flushing = false $0.queue = [request, request2] $0.requestsInFlight = [] - $0.retryInfo = .retry(1) + $0.retryInfo = .retry(2) } } + @MainActor func testSendRequestFailureWithBackoff() async throws { var initialState = INITIALIZED_TEST_STATE() initialState.retryInfo = .retryWithBackoff(requestCount: 1, totalRetryCount: 1, currentBackoff: 1) @@ -108,7 +113,7 @@ class APIRequestErrorHandlingTests: XCTestCase { initialState.requestsInFlight = [request, request2] let store = TestStore(initialState: initialState, reducer: KlaviyoReducer()) - environment.analytics.klaviyoAPI.send = { _ in .failure(.networkError(NSError(domain: "foo", code: NSURLErrorCancelled))) } + environment.analytics.klaviyoAPI.send = { _, _ in .failure(.networkError(NSError(domain: "foo", code: NSURLErrorCancelled))) } _ = await store.send(.sendRequest) @@ -120,6 +125,7 @@ class APIRequestErrorHandlingTests: XCTestCase { } } + @MainActor func testSendRequestMaxRetries() async throws { var initialState = INITIALIZED_TEST_STATE() initialState.retryInfo = .retry(ErrorHandlingConstants.maxRetries) @@ -130,7 +136,7 @@ class APIRequestErrorHandlingTests: XCTestCase { initialState.requestsInFlight = [request, request2] let store = TestStore(initialState: initialState, reducer: KlaviyoReducer()) - environment.analytics.klaviyoAPI.send = { _ in .failure(.networkError(NSError(domain: "foo", code: NSURLErrorCancelled))) } + environment.analytics.klaviyoAPI.send = { _, _ in .failure(.networkError(NSError(domain: "foo", code: NSURLErrorCancelled))) } _ = await store.send(.sendRequest) @@ -138,12 +144,13 @@ class APIRequestErrorHandlingTests: XCTestCase { $0.flushing = false $0.queue = [request2] $0.requestsInFlight = [] - $0.retryInfo = .retry(0) + $0.retryInfo = .retry(1) } } // MARK: - internal error + @MainActor func testSendRequestInternalError() async throws { // NOTE: should really happen but putting this in for possible future cases and test coverage var initialState = INITIALIZED_TEST_STATE() @@ -152,7 +159,7 @@ class APIRequestErrorHandlingTests: XCTestCase { initialState.requestsInFlight = [request] let store = TestStore(initialState: initialState, reducer: KlaviyoReducer()) - environment.analytics.klaviyoAPI.send = { _ in .failure(.internalError("internal error!")) } + environment.analytics.klaviyoAPI.send = { _, _ in .failure(.internalError("internal error!")) } _ = await store.send(.sendRequest) @@ -160,12 +167,13 @@ class APIRequestErrorHandlingTests: XCTestCase { $0.flushing = false $0.queue = [] $0.requestsInFlight = [] - $0.retryInfo = .retry(0) + $0.retryInfo = .retry(1) } } // MARK: - internal request error + @MainActor func testSendRequestInternalRequestError() async throws { var initialState = INITIALIZED_TEST_STATE() @@ -173,7 +181,7 @@ class APIRequestErrorHandlingTests: XCTestCase { initialState.requestsInFlight = [request] let store = TestStore(initialState: initialState, reducer: KlaviyoReducer()) - environment.analytics.klaviyoAPI.send = { _ in .failure(.internalRequestError(KlaviyoAPI.KlaviyoAPIError.internalError("foo"))) } + environment.analytics.klaviyoAPI.send = { _, _ in .failure(.internalRequestError(KlaviyoAPI.KlaviyoAPIError.internalError("foo"))) } _ = await store.send(.sendRequest) @@ -181,12 +189,13 @@ class APIRequestErrorHandlingTests: XCTestCase { $0.flushing = false $0.queue = [] $0.requestsInFlight = [] - $0.retryInfo = .retry(0) + $0.retryInfo = .retry(1) } } // MARK: - unknown error + @MainActor func testSendRequestUnknownError() async throws { var initialState = INITIALIZED_TEST_STATE() @@ -194,7 +203,7 @@ class APIRequestErrorHandlingTests: XCTestCase { initialState.requestsInFlight = [request] let store = TestStore(initialState: initialState, reducer: KlaviyoReducer()) - environment.analytics.klaviyoAPI.send = { _ in .failure(.unknownError(KlaviyoAPI.KlaviyoAPIError.internalError("foo"))) } + environment.analytics.klaviyoAPI.send = { _, _ in .failure(.unknownError(KlaviyoAPI.KlaviyoAPIError.internalError("foo"))) } _ = await store.send(.sendRequest) @@ -202,19 +211,20 @@ class APIRequestErrorHandlingTests: XCTestCase { $0.flushing = false $0.queue = [] $0.requestsInFlight = [] - $0.retryInfo = .retry(0) + $0.retryInfo = .retry(1) } } // MARK: - data decoding error + @MainActor func testSendRequestDataDecodingError() async throws { var initialState = INITIALIZED_TEST_STATE() let request = initialState.buildProfileRequest(apiKey: initialState.apiKey!, anonymousId: initialState.anonymousId!) initialState.requestsInFlight = [request] let store = TestStore(initialState: initialState, reducer: KlaviyoReducer()) - environment.analytics.klaviyoAPI.send = { _ in .failure(.dataEncodingError(request)) } + environment.analytics.klaviyoAPI.send = { _, _ in .failure(.dataEncodingError(request)) } _ = await store.send(.sendRequest) @@ -222,19 +232,20 @@ class APIRequestErrorHandlingTests: XCTestCase { $0.flushing = false $0.queue = [] $0.requestsInFlight = [] - $0.retryInfo = .retry(0) + $0.retryInfo = .retry(1) } } // MARK: - invalid data + @MainActor func testSendRequestInvalidData() async throws { var initialState = INITIALIZED_TEST_STATE() let request = initialState.buildProfileRequest(apiKey: initialState.apiKey!, anonymousId: initialState.anonymousId!) initialState.requestsInFlight = [request] let store = TestStore(initialState: initialState, reducer: KlaviyoReducer()) - environment.analytics.klaviyoAPI.send = { _ in .failure(.invalidData) } + environment.analytics.klaviyoAPI.send = { _, _ in .failure(.invalidData) } _ = await store.send(.sendRequest) @@ -242,30 +253,32 @@ class APIRequestErrorHandlingTests: XCTestCase { $0.flushing = false $0.queue = [] $0.requestsInFlight = [] - $0.retryInfo = .retry(0) + $0.retryInfo = .retry(1) } } // MARK: - rate limit error + @MainActor func testRateLimitErrorWithExistingRetry() async throws { var initialState = INITIALIZED_TEST_STATE() let request = initialState.buildProfileRequest(apiKey: initialState.apiKey!, anonymousId: initialState.anonymousId!) initialState.requestsInFlight = [request] let store = TestStore(initialState: initialState, reducer: KlaviyoReducer()) - environment.analytics.klaviyoAPI.send = { _ in .failure(.rateLimitError(nil)) } + environment.analytics.klaviyoAPI.send = { _, _ in .failure(.rateLimitError(nil)) } _ = await store.send(.sendRequest) - await store.receive(.requestFailed(request, .retryWithBackoff(requestCount: 1, totalRetryCount: 1, currentBackoff: 0)), timeout: TIMEOUT_NANOSECONDS) { + await store.receive(.requestFailed(request, .retryWithBackoff(requestCount: 2, totalRetryCount: 2, currentBackoff: 0)), timeout: TIMEOUT_NANOSECONDS) { $0.flushing = false $0.queue = [request] $0.requestsInFlight = [] - $0.retryInfo = .retryWithBackoff(requestCount: 1, totalRetryCount: 1, currentBackoff: 0) + $0.retryInfo = .retryWithBackoff(requestCount: 2, totalRetryCount: 2, currentBackoff: 0) } } + @MainActor func testRateLimitErrorWithExistingBackoffRetry() async throws { var initialState = INITIALIZED_TEST_STATE() initialState.retryInfo = .retryWithBackoff(requestCount: 2, totalRetryCount: 2, currentBackoff: 4) @@ -273,7 +286,7 @@ class APIRequestErrorHandlingTests: XCTestCase { initialState.requestsInFlight = [request] let store = TestStore(initialState: initialState, reducer: KlaviyoReducer()) - environment.analytics.klaviyoAPI.send = { _ in .failure(.rateLimitError(nil)) } + environment.analytics.klaviyoAPI.send = { _, _ in .failure(.rateLimitError(nil)) } _ = await store.send(.sendRequest) @@ -292,7 +305,7 @@ class APIRequestErrorHandlingTests: XCTestCase { initialState.requestsInFlight = [request] let store = TestStore(initialState: initialState, reducer: KlaviyoReducer()) - environment.analytics.klaviyoAPI.send = { _ in .failure(.rateLimitError(20)) } + environment.analytics.klaviyoAPI.send = { _, _ in .failure(.rateLimitError(20)) } _ = await store.send(.sendRequest) @@ -306,6 +319,7 @@ class APIRequestErrorHandlingTests: XCTestCase { // MARK: - Missing or invalid response + @MainActor func testMissingOrInvalidResponse() async throws { var initialState = INITIALIZED_TEST_STATE() initialState.retryInfo = .retryWithBackoff(requestCount: 2, totalRetryCount: 2, currentBackoff: 4) @@ -313,7 +327,7 @@ class APIRequestErrorHandlingTests: XCTestCase { initialState.requestsInFlight = [request] let store = TestStore(initialState: initialState, reducer: KlaviyoReducer()) - environment.analytics.klaviyoAPI.send = { _ in .failure(.missingOrInvalidResponse(nil)) } + environment.analytics.klaviyoAPI.send = { _, _ in .failure(.missingOrInvalidResponse(nil)) } _ = await store.send(.sendRequest) @@ -321,7 +335,7 @@ class APIRequestErrorHandlingTests: XCTestCase { $0.flushing = false $0.queue = [] $0.requestsInFlight = [] - $0.retryInfo = .retry(0) + $0.retryInfo = .retry(1) } } } diff --git a/Tests/KlaviyoSwiftTests/AppLifeCycleEventsTests.swift b/Tests/KlaviyoSwiftTests/AppLifeCycleEventsTests.swift index 4443181b..64e53866 100644 --- a/Tests/KlaviyoSwiftTests/AppLifeCycleEventsTests.swift +++ b/Tests/KlaviyoSwiftTests/AppLifeCycleEventsTests.swift @@ -10,7 +10,6 @@ import Combine import Foundation import XCTest -@MainActor class AppLifeCycleEventsTests: XCTestCase { let passThroughSubject = PassthroughSubject() @@ -25,12 +24,14 @@ class AppLifeCycleEventsTests: XCTestCase { } } + @MainActor override func setUp() { environment = KlaviyoEnvironment.test() } // MARK: - App Terminate + @MainActor func testAppTerminateStopsReachability() { environment.notificationCenterPublisher = getFilteredNotificaitonPublished(name: UIApplication.willTerminateNotification) let expection = XCTestExpectation(description: "Stop reachability is called.") diff --git a/Tests/KlaviyoSwiftTests/KlaviyoAPITests.swift b/Tests/KlaviyoSwiftTests/KlaviyoAPITests.swift index 449d76fc..c05a58b8 100644 --- a/Tests/KlaviyoSwiftTests/KlaviyoAPITests.swift +++ b/Tests/KlaviyoSwiftTests/KlaviyoAPITests.swift @@ -17,10 +17,6 @@ final class KlaviyoAPITests: XCTestCase { environment = KlaviyoEnvironment.test() } - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - func testInvalidURL() async throws { environment.analytics.apiURL = "" @@ -105,7 +101,6 @@ final class KlaviyoAPITests: XCTestCase { }) } let request = KlaviyoAPI.KlaviyoRequest(apiKey: "foo", endpoint: .createEvent(.init(data: .init(event: .test)))) await sendAndAssert(with: request) { result in - switch result { case let .success(data): assertSnapshot(matching: data, as: .dump) @@ -134,7 +129,7 @@ final class KlaviyoAPITests: XCTestCase { func sendAndAssert(with request: KlaviyoAPI.KlaviyoRequest, assertion: (Result) -> Void) async { - let result = await KlaviyoAPI().send(request) + let result = await KlaviyoAPI().send(request, 0) assertion(result) } } diff --git a/Tests/KlaviyoSwiftTests/KlaviyoStateTests.swift b/Tests/KlaviyoSwiftTests/KlaviyoStateTests.swift index 8677f494..a59445c8 100644 --- a/Tests/KlaviyoSwiftTests/KlaviyoStateTests.swift +++ b/Tests/KlaviyoSwiftTests/KlaviyoStateTests.swift @@ -11,7 +11,6 @@ import Foundation import SnapshotTesting import XCTest -@MainActor final class KlaviyoStateTests: XCTestCase { let TEST_EVENT = [ "event": "$opened_push", diff --git a/Tests/KlaviyoSwiftTests/KlaviyoTestUtils.swift b/Tests/KlaviyoSwiftTests/KlaviyoTestUtils.swift index abecb16d..251cecb4 100644 --- a/Tests/KlaviyoSwiftTests/KlaviyoTestUtils.swift +++ b/Tests/KlaviyoSwiftTests/KlaviyoTestUtils.swift @@ -156,7 +156,7 @@ extension FileClient { } extension KlaviyoAPI { - static let test = { KlaviyoAPI(send: { _ in .success(TEST_RETURN_DATA) }) } + static let test = { KlaviyoAPI(send: { _, _ in .success(TEST_RETURN_DATA) }) } } extension LoggerClient { diff --git a/Tests/KlaviyoSwiftTests/StateChangePublisherTests.swift b/Tests/KlaviyoSwiftTests/StateChangePublisherTests.swift index 94b01934..4cfe4dec 100644 --- a/Tests/KlaviyoSwiftTests/StateChangePublisherTests.swift +++ b/Tests/KlaviyoSwiftTests/StateChangePublisherTests.swift @@ -11,12 +11,13 @@ import Foundation import XCTest @_spi(KlaviyoPrivate) @testable import KlaviyoSwift -@MainActor final class StateChangePublisherTests: XCTestCase { + @MainActor override func setUpWithError() throws { environment = KlaviyoEnvironment.test() } + @MainActor func testStateChangePublisher() throws { let savedCalledExpectation = XCTestExpectation(description: "Save called on initialization") // Third call set email should trigger again @@ -76,6 +77,7 @@ final class StateChangePublisherTests: XCTestCase { XCTAssertEqual(count, 2) } + @MainActor func testStateChangeDuplicateAreRemoved() throws { let savedCalledExpectation = XCTestExpectation(description: "Save called on initialization") savedCalledExpectation.assertForOverFulfill = true diff --git a/Tests/KlaviyoSwiftTests/StateManagementEdgeCaseTests.swift b/Tests/KlaviyoSwiftTests/StateManagementEdgeCaseTests.swift index 35c53af4..09449939 100644 --- a/Tests/KlaviyoSwiftTests/StateManagementEdgeCaseTests.swift +++ b/Tests/KlaviyoSwiftTests/StateManagementEdgeCaseTests.swift @@ -9,14 +9,15 @@ import Foundation import XCTest -@MainActor class StateManagementEdgeCaseTests: XCTestCase { + @MainActor override func setUp() async throws { environment = KlaviyoEnvironment.test() } // MARK: - initialization + @MainActor func testInitializeWhileInitializing() async throws { let initialState = KlaviyoState(queue: [], requestsInFlight: []) let store = TestStore(initialState: initialState, reducer: KlaviyoReducer()) @@ -39,6 +40,7 @@ class StateManagementEdgeCaseTests: XCTestCase { _ = await store.send(.initialize(apiKey)) } + @MainActor func testInitializeAfterInitialized() async throws { let initialState = INITIALIZED_TEST_STATE() let store = TestStore(initialState: initialState, reducer: KlaviyoReducer()) @@ -57,6 +59,7 @@ class StateManagementEdgeCaseTests: XCTestCase { // MARK: - Send Request + @MainActor func testSendRequestBeforeInitialization() async throws { let apiKey = "fake-key" let initialState = KlaviyoState(apiKey: apiKey, @@ -71,6 +74,7 @@ class StateManagementEdgeCaseTests: XCTestCase { // MARK: - Complete Initialization + @MainActor func testCompleteInitializationWhileAlreadyInitialized() async throws { let apiKey = "fake-key" let initialState = KlaviyoState(apiKey: apiKey, @@ -87,6 +91,7 @@ class StateManagementEdgeCaseTests: XCTestCase { _ = await store.send(.completeInitialization(initialState)) } + @MainActor func testCompleteInitializationWithExistingIdentifiers() async throws { let apiKey = "fake-key" let initialState = KlaviyoState(apiKey: apiKey, @@ -110,6 +115,7 @@ class StateManagementEdgeCaseTests: XCTestCase { // MARK: - Set Email + @MainActor func testSetEmailUninitialized() async throws { let expection = XCTestExpectation(description: "fatal error expected") environment.emitDeveloperWarning = { _ in @@ -130,6 +136,7 @@ class StateManagementEdgeCaseTests: XCTestCase { await fulfillment(of: [expection]) } + @MainActor func testSetEmailMissingAnonymousIdStillSetsEmail() async throws { let apiKey = "fake-key" let initialState = KlaviyoState(apiKey: apiKey, @@ -153,6 +160,7 @@ class StateManagementEdgeCaseTests: XCTestCase { // MARK: - Set External Id + @MainActor func testSetExternalIdUninitialized() async throws { let apiKey = "fake-key" let initialState = KlaviyoState(apiKey: apiKey, @@ -166,6 +174,7 @@ class StateManagementEdgeCaseTests: XCTestCase { _ = await store.send(.setExternalId("external-blob-id")) } + @MainActor func testSetExternalIdMissingAnonymousIdStillSetsExternalId() async throws { let apiKey = "fake-key" let initialState = KlaviyoState(apiKey: apiKey, @@ -189,6 +198,7 @@ class StateManagementEdgeCaseTests: XCTestCase { // MARK: - Set Phone number + @MainActor func testSetPhoneNumberUninitialized() async throws { let apiKey = "fake-key" let initialState = KlaviyoState(apiKey: apiKey, @@ -202,6 +212,7 @@ class StateManagementEdgeCaseTests: XCTestCase { _ = await store.send(.setPhoneNumber("1-800-Blobs4u")) } + @MainActor func testSetPhoneNumberMissingApiKeyStillSetsPhoneNumber() async throws { let initialState = KlaviyoState(anonymousId: environment.analytics.uuid().uuidString, queue: [], @@ -224,6 +235,7 @@ class StateManagementEdgeCaseTests: XCTestCase { // MARK: - Set Push Token + @MainActor func testSetPushTokenUninitialized() async throws { let apiKey = "fake-key" let initialState = KlaviyoState(apiKey: apiKey, @@ -237,6 +249,7 @@ class StateManagementEdgeCaseTests: XCTestCase { _ = await store.send(.setPushToken("blob_token", .authorized)) } + @MainActor func testSetPushTokenWithMissingAnonymousId() async throws { let apiKey = "fake-key" let initialState = KlaviyoState(apiKey: apiKey, @@ -254,6 +267,7 @@ class StateManagementEdgeCaseTests: XCTestCase { // MARK: - Stop + @MainActor func testStopUninitialized() async { let apiKey = "fake-key" let initialState = KlaviyoState(apiKey: apiKey, @@ -267,6 +281,7 @@ class StateManagementEdgeCaseTests: XCTestCase { _ = await store.send(.stop) } + @MainActor func testStopInitializing() async { let apiKey = "fake-key" let initialState = KlaviyoState(apiKey: apiKey, @@ -282,6 +297,7 @@ class StateManagementEdgeCaseTests: XCTestCase { // MARK: - Start + @MainActor func testStartUninitialized() async { let apiKey = "fake-key" let initialState = KlaviyoState(apiKey: apiKey, @@ -297,6 +313,7 @@ class StateManagementEdgeCaseTests: XCTestCase { // MARK: - Network Status Changed + @MainActor func testNetworkStatusChangedUninitialized() async { let apiKey = "fake-key" let initialState = KlaviyoState(apiKey: apiKey, @@ -312,6 +329,7 @@ class StateManagementEdgeCaseTests: XCTestCase { // MARK: - Missing api key for token request + @MainActor func testTokenRequestMissingApiKey() async { let initialState = KlaviyoState( anonymousId: environment.analytics.uuid().uuidString, @@ -329,6 +347,7 @@ class StateManagementEdgeCaseTests: XCTestCase { // MARK: - set enqueue event uninitialized + @MainActor func testEnqueueEventUninitialized() async throws { let expection = XCTestExpectation(description: "fatal error expected") environment.emitDeveloperWarning = { _ in @@ -343,6 +362,7 @@ class StateManagementEdgeCaseTests: XCTestCase { // MARK: - set profile uninitialized + @MainActor func testSetProfileUnitialized() async throws { let expection = XCTestExpectation(description: "fatal error expected") environment.emitDeveloperWarning = { _ in diff --git a/Tests/KlaviyoSwiftTests/StateManagementTests.swift b/Tests/KlaviyoSwiftTests/StateManagementTests.swift index aa90c734..dcdadb74 100644 --- a/Tests/KlaviyoSwiftTests/StateManagementTests.swift +++ b/Tests/KlaviyoSwiftTests/StateManagementTests.swift @@ -11,14 +11,15 @@ import Combine import Foundation import XCTest -@MainActor class StateManagementTests: XCTestCase { + @MainActor override func setUp() async throws { environment = KlaviyoEnvironment.test() } // MARK: - Initialization + @MainActor func testInitialize() async throws { let initialState = KlaviyoState(queue: [], requestsInFlight: []) let store = TestStore(initialState: initialState, reducer: KlaviyoReducer()) @@ -41,6 +42,7 @@ class StateManagementTests: XCTestCase { await store.receive(.flushQueue) } + @MainActor func testInitializeSubscribesToAppropriatePublishers() async throws { let lifecycleExpectation = XCTestExpectation(description: "lifecycle is subscribed") let stateChangeIsSubscribed = XCTestExpectation(description: "state change is subscribed") @@ -73,6 +75,7 @@ class StateManagementTests: XCTestCase { // MARK: - Set Email + @MainActor func testSetEmail() async throws { let initialState = INITIALIZED_TEST_STATE() let store = TestStore(initialState: initialState, reducer: KlaviyoReducer()) @@ -87,6 +90,7 @@ class StateManagementTests: XCTestCase { // MARK: Set Phone Number + @MainActor func testSetPhoneNumber() async throws { let initialState = INITIALIZED_TEST_STATE() let store = TestStore(initialState: initialState, reducer: KlaviyoReducer()) @@ -101,6 +105,7 @@ class StateManagementTests: XCTestCase { // MARK: - Set External Id. + @MainActor func testSetExternalId() async throws { let initialState = INITIALIZED_TEST_STATE() let store = TestStore(initialState: initialState, reducer: KlaviyoReducer()) @@ -115,6 +120,7 @@ class StateManagementTests: XCTestCase { // MARK: - Set Push Token + @MainActor func testSetPushToken() async throws { var initialState = INITIALIZED_TEST_STATE() initialState.pushTokenData = nil @@ -141,6 +147,7 @@ class StateManagementTests: XCTestCase { } } + @MainActor func testSetPushTokenMultipleTimes() async throws { var initialState = INITIALIZED_TEST_STATE() initialState.pushTokenData = nil @@ -171,6 +178,7 @@ class StateManagementTests: XCTestCase { // MARK: - flush + @MainActor func testFlushUninitializedQueueDoesNotFlush() async throws { let apiKey = "fake-key" let initialState = KlaviyoState(apiKey: apiKey, @@ -182,6 +190,7 @@ class StateManagementTests: XCTestCase { _ = await store.send(.flushQueue) } + @MainActor func testQueueThatIsFlushingDoesNotFlush() async throws { let apiKey = "fake-key" let initialState = KlaviyoState(apiKey: apiKey, @@ -193,6 +202,7 @@ class StateManagementTests: XCTestCase { _ = await store.send(.flushQueue) } + @MainActor func testEmptyQueueDoesNotFlush() async throws { let apiKey = "fake-key" let initialState = KlaviyoState(apiKey: apiKey, @@ -204,6 +214,7 @@ class StateManagementTests: XCTestCase { _ = await store.send(.flushQueue) } + @MainActor func testFlushQueueWithMultipleRequests() async throws { var count = 0 // request uuids need to be unique :) @@ -246,6 +257,7 @@ class StateManagementTests: XCTestCase { } } + @MainActor func testFlushQueueDuringExponentialBackoff() async throws { var initialState = INITIALIZED_TEST_STATE() initialState.retryInfo = .retryWithBackoff(requestCount: 23, totalRetryCount: 23, currentBackoff: 200) @@ -260,6 +272,7 @@ class StateManagementTests: XCTestCase { } } + @MainActor func testFlushQueueExponentialBackoffGoesToSize() async throws { var initialState = INITIALIZED_TEST_STATE() initialState.retryInfo = .retryWithBackoff(requestCount: 23, totalRetryCount: 23, currentBackoff: Int(initialState.flushInterval) - 2) @@ -280,12 +293,13 @@ class StateManagementTests: XCTestCase { // didn't fake uuid since we are not testing this. await store.receive(.deQueueCompletedResults(request)) { $0.flushing = false - $0.retryInfo = .retry(0) + $0.retryInfo = .retry(1) $0.requestsInFlight = [] $0.queue = [] } } + @MainActor func testSendRequestWhenNotFlushing() async throws { var initialState = INITIALIZED_TEST_STATE() initialState.flushing = false @@ -296,6 +310,7 @@ class StateManagementTests: XCTestCase { // MARK: - send request + @MainActor func testSendRequestWithNoRequestsInFlight() async throws { let initialState = INITIALIZED_TEST_STATE() let store = TestStore(initialState: initialState, reducer: KlaviyoReducer()) @@ -307,6 +322,7 @@ class StateManagementTests: XCTestCase { // MARK: - Network Connectivity Changed + @MainActor func testNetworkConnectivityChanges() async throws { let initialState = INITIALIZED_TEST_STATE() let store = TestStore(initialState: initialState, reducer: KlaviyoReducer()) @@ -330,6 +346,7 @@ class StateManagementTests: XCTestCase { // MARK: - Stop + @MainActor func testStopWithRequestsInFlight() async throws { // This test is a little convoluted but essentially want to make when we stop // that we save our state. @@ -350,6 +367,7 @@ class StateManagementTests: XCTestCase { // MARK: - Test pending profile + @MainActor func testFlushWithPendingProfile() async throws { var initialState = INITIALIZED_TEST_STATE() initialState.flushing = false @@ -426,6 +444,7 @@ class StateManagementTests: XCTestCase { // MARK: - Test set profile + @MainActor func testSetProfileWithExistingProperties() async throws { var initialState = INITIALIZED_TEST_STATE() initialState.phoneNumber = "555BLOB" @@ -441,6 +460,7 @@ class StateManagementTests: XCTestCase { // MARK: - Test enqueue event + @MainActor func testEnqueueEvent() async throws { var initialState = INITIALIZED_TEST_STATE() initialState.phoneNumber = "555BLOB" @@ -452,6 +472,7 @@ class StateManagementTests: XCTestCase { } } + @MainActor func testEnqueueEventWhenInitilizingSendsEvent() async throws { let initialState = INITILIZING_TEST_STATE() let store = TestStore(initialState: initialState, reducer: KlaviyoReducer()) diff --git a/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoAPITests/testSuccessfulResponseWithEvent.1.txt b/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoAPITests/testSuccessfulResponseWithEvent.1.txt index e983bc39..fafe69a5 100644 --- a/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoAPITests/testSuccessfulResponseWithEvent.1.txt +++ b/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoAPITests/testSuccessfulResponseWithEvent.1.txt @@ -9,7 +9,10 @@ ▿ httpMethod: Optional - some: "POST" ▿ allHTTPHeaderFields: Optional> - - some: 0 key/value pairs + ▿ some: 1 key/value pair + ▿ (2 elements) + - key: "X-Klaviyo-Attempt-Count" + - value: "0/50" ▿ httpBody: Optional - some: 0 bytes - httpBodyStream: Optional.none diff --git a/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoAPITests/testSuccessfulResponseWithProfile.1.txt b/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoAPITests/testSuccessfulResponseWithProfile.1.txt index 60cd6017..9563cd57 100644 --- a/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoAPITests/testSuccessfulResponseWithProfile.1.txt +++ b/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoAPITests/testSuccessfulResponseWithProfile.1.txt @@ -9,7 +9,10 @@ ▿ httpMethod: Optional - some: "POST" ▿ allHTTPHeaderFields: Optional> - - some: 0 key/value pairs + ▿ some: 1 key/value pair + ▿ (2 elements) + - key: "X-Klaviyo-Attempt-Count" + - value: "0/50" ▿ httpBody: Optional - some: 0 bytes - httpBodyStream: Optional.none diff --git a/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoAPITests/testSuccessfulResponseWithStoreToken.1.txt b/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoAPITests/testSuccessfulResponseWithStoreToken.1.txt index 98f1c938..b80d173f 100644 --- a/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoAPITests/testSuccessfulResponseWithStoreToken.1.txt +++ b/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoAPITests/testSuccessfulResponseWithStoreToken.1.txt @@ -9,7 +9,10 @@ ▿ httpMethod: Optional - some: "POST" ▿ allHTTPHeaderFields: Optional> - - some: 0 key/value pairs + ▿ some: 1 key/value pair + ▿ (2 elements) + - key: "X-Klaviyo-Attempt-Count" + - value: "0/50" ▿ httpBody: Optional - some: 0 bytes - httpBodyStream: Optional.none diff --git a/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoStateTests/testLoadNewKlaviyoState.1.txt b/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoStateTests/testLoadNewKlaviyoState.1.txt index 4cabef0e..7be1f88b 100644 --- a/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoStateTests/testLoadNewKlaviyoState.1.txt +++ b/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoStateTests/testLoadNewKlaviyoState.1.txt @@ -15,4 +15,4 @@ - queue: 0 elements - requestsInFlight: 0 elements ▿ retryInfo: RetryInfo - - retry: 0 + - retry: 1 diff --git a/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoStateTests/testStateFileExistsInvalidData.1.txt b/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoStateTests/testStateFileExistsInvalidData.1.txt index 4cabef0e..7be1f88b 100644 --- a/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoStateTests/testStateFileExistsInvalidData.1.txt +++ b/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoStateTests/testStateFileExistsInvalidData.1.txt @@ -15,4 +15,4 @@ - queue: 0 elements - requestsInFlight: 0 elements ▿ retryInfo: RetryInfo - - retry: 0 + - retry: 1 diff --git a/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoStateTests/testStateFileExistsInvalidJSON.1.txt b/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoStateTests/testStateFileExistsInvalidJSON.1.txt index 4cabef0e..7be1f88b 100644 --- a/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoStateTests/testStateFileExistsInvalidJSON.1.txt +++ b/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoStateTests/testStateFileExistsInvalidJSON.1.txt @@ -15,4 +15,4 @@ - queue: 0 elements - requestsInFlight: 0 elements ▿ retryInfo: RetryInfo - - retry: 0 + - retry: 1 diff --git a/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoStateTests/testValidStateFileExists.1.txt b/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoStateTests/testValidStateFileExists.1.txt index 4cabef0e..7be1f88b 100644 --- a/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoStateTests/testValidStateFileExists.1.txt +++ b/Tests/KlaviyoSwiftTests/__Snapshots__/KlaviyoStateTests/testValidStateFileExists.1.txt @@ -15,4 +15,4 @@ - queue: 0 elements - requestsInFlight: 0 elements ▿ retryInfo: RetryInfo - - retry: 0 + - retry: 1 diff --git a/Tests/KlaviyoSwiftTests/__Snapshots__/NetworkSessionTests/testCreateEmphemeralSesionHeaders.1.txt b/Tests/KlaviyoSwiftTests/__Snapshots__/NetworkSessionTests/testCreateEmphemeralSesionHeaders.1.txt index 0cce6297..82436a64 100644 --- a/Tests/KlaviyoSwiftTests/__Snapshots__/NetworkSessionTests/testCreateEmphemeralSesionHeaders.1.txt +++ b/Tests/KlaviyoSwiftTests/__Snapshots__/NetworkSessionTests/testCreateEmphemeralSesionHeaders.1.txt @@ -1,5 +1,5 @@ ▿ Optional> - ▿ some: 5 key/value pairs + ▿ some: 6 key/value pairs ▿ (2 elements) - key: "Accept-Encoding" ▿ value: 3 elements @@ -9,6 +9,9 @@ ▿ (2 elements) - key: "User-Agent" - value: "FooApp/1.2.3 (com.klaviyo.fooapp; build:1; iOS 1.1.1) klaviyo-ios/3.0.4" + ▿ (2 elements) + - key: "X-Klaviyo-Mobile" + - value: "1" ▿ (2 elements) - key: "accept" - value: "application/json"