diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c82c46101..0f08eea3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,28 +8,172 @@ on: - '.github/**' - 'README.md' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + KMP_XCFRAMEWORK_PATH: OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework + jobs: - build: - name: Build and Test using any available iPhone simulator - runs-on: macos-15-large + lint: + name: Swift Lint + runs-on: macos-latest steps: - - name: Select Xcode Version - run: | - sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer - name: Checkout OneSignal-iOS-SDK - uses: actions/checkout@v4 + uses: actions/checkout@v7 + - name: Run Swift Lint + uses: cirruslabs/swiftlint-action@v1 + with: + version: latest + + kmp-xcframework: + name: Build OneSignalKMP XCFramework + runs-on: macos-latest + + steps: + - name: Checkout OneSignal-iOS-SDK + uses: actions/checkout@v7 with: submodules: recursive + - name: Create KMP cache key + id: kmp + run: | + kmp_sha="$(git rev-parse HEAD:OneSignal-KMP-SDK)" + xcode_hash="$(xcodebuild -version | shasum -a 256 | awk '{print $1}')" + echo "cache-key=kmp-xcframework-${{ runner.os }}-${{ runner.arch }}-${xcode_hash}-${kmp_sha}" >> "$GITHUB_OUTPUT" + - name: Restore OneSignalKMP XCFramework + id: kmp-cache + uses: actions/cache/restore@v6 + with: + path: ${{ env.KMP_XCFRAMEWORK_PATH }} + key: ${{ steps.kmp.outputs.cache-key }} - name: Setup JDK 17 - uses: actions/setup-java@v4 + if: steps.kmp-cache.outputs.cache-hit != 'true' + uses: actions/setup-java@v5 with: distribution: temurin java-version: "17" - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - - name: Build OneSignalKMP XCFramework - run: iOS_SDK/OneSignalSDK/build_kmp_xcframework.sh + if: steps.kmp-cache.outputs.cache-hit != 'true' + uses: gradle/actions/setup-gradle@v6 + - name: Assemble OneSignalKMP XCFramework + if: steps.kmp-cache.outputs.cache-hit != 'true' + run: | + iOS_SDK/OneSignalSDK/build_kmp_xcframework.sh \ + :kmp:assembleOneSignalKMPReleaseXCFramework + - name: Save OneSignalKMP XCFramework + if: steps.kmp-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v6 + with: + path: ${{ env.KMP_XCFRAMEWORK_PATH }} + key: ${{ steps.kmp.outputs.cache-key }} + - name: Package OneSignalKMP XCFramework + run: | + tar -czf "$RUNNER_TEMP/OneSignalKMP.xcframework.tar.gz" \ + -C "$(dirname "$KMP_XCFRAMEWORK_PATH")" \ + "$(basename "$KMP_XCFRAMEWORK_PATH")" + - name: Upload OneSignalKMP XCFramework + uses: actions/upload-artifact@v7 + with: + name: OneSignalKMP-XCFramework + path: ${{ runner.temp }}/OneSignalKMP.xcframework.tar.gz + retention-days: 1 + + ios-simulator: + name: Build and Test using any available iPhone simulator + runs-on: macos-latest + needs: kmp-xcframework + + steps: + - name: Checkout OneSignal-iOS-SDK + uses: actions/checkout@v7 + - name: Download OneSignalKMP XCFramework + uses: actions/download-artifact@v8 + with: + name: OneSignalKMP-XCFramework + path: ${{ runner.temp }} + - name: Extract OneSignalKMP XCFramework + run: | + mkdir -p "$(dirname "$KMP_XCFRAMEWORK_PATH")" + tar -xzf "$RUNNER_TEMP/OneSignalKMP.xcframework.tar.gz" \ + -C "$(dirname "$KMP_XCFRAMEWORK_PATH")" + - name: Select iPhone simulator + id: simulator + run: | + runtime_id="$(xcrun simctl list runtimes available --json | jq -r \ + '[.runtimes[] | select(.platform == "iOS")] | sort_by(.version | split(".") | map(tonumber)) | last.identifier')" + device_id="$(xcrun simctl list devices available --json | jq -r --arg runtime "$runtime_id" \ + '.devices[$runtime] | map(select(.name | startswith("iPhone"))) | first.udid')" + if [[ -z "$device_id" || "$device_id" == "null" ]]; then + echo "No available iPhone simulator found" >&2 + exit 1 + fi + echo "device-id=$device_id" >> "$GITHUB_OUTPUT" + - name: Build + env: + scheme: ${{ 'UnitTestApp' }} + file_to_build: ${{ 'iOS_SDK/OneSignalSDK/OneSignal.xcodeproj' }} + filetype_parameter: ${{ 'project' }} + run: | + xcodebuild build-for-testing \ + -quiet \ + -scheme "$scheme" \ + -"$filetype_parameter" "$file_to_build" \ + -destination "generic/platform=iOS Simulator" \ + -enableCodeCoverage NO \ + ARCHS=arm64 \ + ONLY_ACTIVE_ARCH=YES + - name: Start simulator boot + env: + device_id: ${{ steps.simulator.outputs.device-id }} + run: | + device_state="$(xcrun simctl list devices --json | jq -r --arg device "$device_id" \ + '[.devices[][] | select(.udid == $device)] | first.state')" + if [[ "$device_state" != "Booted" ]]; then + xcrun simctl boot "$device_id" + fi + - name: Wait for simulator boot + env: + device_id: ${{ steps.simulator.outputs.device-id }} + run: xcrun simctl bootstatus "$device_id" -b + - name: Test + env: + scheme: ${{ 'UnitTestApp' }} + test_plan: ${{ 'UnitTestApp_TestPlan_Reduced' }} + platform: ${{ 'iOS Simulator' }} + file_to_build: ${{ 'iOS_SDK/OneSignalSDK/OneSignal.xcodeproj' }} + filetype_parameter: ${{ 'project' }} + device_id: ${{ steps.simulator.outputs.device-id }} + run: | + xcodebuild test-without-building \ + -quiet \ + -scheme "$scheme" \ + -testPlan "$test_plan" \ + -"$filetype_parameter" "$file_to_build" \ + -destination "platform=$platform,id=$device_id,arch=arm64" \ + -maximum-parallel-testing-workers 1 \ + -enableCodeCoverage NO + + catalyst: + name: KMP logger Mac Catalyst integration + runs-on: macos-latest + needs: kmp-xcframework + + steps: + - name: Checkout OneSignal-iOS-SDK + uses: actions/checkout@v7 + - name: Download OneSignalKMP XCFramework + uses: actions/download-artifact@v8 + with: + name: OneSignalKMP-XCFramework + path: ${{ runner.temp }} + - name: Extract OneSignalKMP XCFramework + run: | + mkdir -p "$(dirname "$KMP_XCFRAMEWORK_PATH")" + tar -xzf "$RUNNER_TEMP/OneSignalKMP.xcframework.tar.gz" \ + -C "$(dirname "$KMP_XCFRAMEWORK_PATH")" - name: Archive OneSignalFramework for Catalyst run: | xcodebuild archive \ @@ -57,32 +201,3 @@ jobs: iOS_SDK/OneSignalSDK/CatalystLoggerHost/main.swift \ -o "$host" DYLD_FRAMEWORK_PATH="$frameworks" "$host" - - name: Set Default Scheme - run: | - default="UnitTestApp" - echo $default | cat >default - echo Using default scheme: $default - - name: Run Swift Lint - run: | - swiftlint - - name: Build - env: - scheme: ${{ 'UnitTestApp' }} - platform: ${{ 'iOS Simulator' }} - file_to_build: ${{ 'iOS_SDK/OneSignalSDK/OneSignal.xcodeproj' }} - filetype_parameter: ${{ 'project' }} - run: | - # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959) - device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"` - xcodebuild build-for-testing -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -destination "platform=$platform,name=$device" - - name: Test - env: - scheme: ${{ 'UnitTestApp' }} - test_plan: ${{ 'UnitTestApp_TestPlan_Reduced' }} - platform: ${{ 'iOS Simulator' }} - file_to_build: ${{ 'iOS_SDK/OneSignalSDK/OneSignal.xcodeproj' }} - filetype_parameter: ${{ 'project' }} - run: | - # xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959) - device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"` - xcodebuild test-without-building -scheme "$scheme" -testPlan "$test_plan" -"$filetype_parameter" "$file_to_build" -destination "platform=$platform,name=$device" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6159106de..f7f85b5c4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -14,12 +14,13 @@ name: "CodeQL" on: push: branches: [ "main" ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ "main" ] schedule: - cron: '35 6 * * 2' +concurrency: + group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true + jobs: analyze: name: Analyze @@ -46,11 +47,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v7 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -64,7 +65,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v4 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -77,6 +78,6 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v4 with: category: "/language:${{matrix.language}}" diff --git a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOSDispatchQueue.swift b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOSDispatchQueue.swift index 842dd1be8..1e911b30d 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOSDispatchQueue.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOSDispatchQueue.swift @@ -25,33 +25,43 @@ THE SOFTWARE. */ +import Foundation import OneSignalOSCore public class MockDispatchQueue: OSDispatchQueue { let requestDispatch = DispatchQueue(label: "MockDispatchQueue") - var numDispatches = 0 + private let dispatchCondition = NSCondition() + private var numDispatches = 0 public init() {} public func async(execute work: @escaping @convention(block) () -> Void) { requestDispatch.async { work() - self.numDispatches += 1 + self.recordDispatch() } } public func asyncAfterTime(deadline: DispatchTime, execute work: @escaping @Sendable @convention(block) () -> Void) { requestDispatch.asyncAfterTime(deadline: deadline) { work() - self.numDispatches += 1 + self.recordDispatch() } } public func waitForDispatches(_ numDispatches: Int) { + dispatchCondition.lock() + defer { dispatchCondition.unlock() } + while self.numDispatches < numDispatches { - requestDispatch.sync { - Thread.sleep(forTimeInterval: TimeInterval(1)) - } + dispatchCondition.wait() } } + + private func recordDispatch() { + dispatchCondition.lock() + numDispatches += 1 + dispatchCondition.broadcast() + dispatchCondition.unlock() + } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift index a388fc6f9..88265f72c 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift @@ -38,6 +38,7 @@ public class MockOneSignalClient: NSObject, IOneSignalClient { public var executedRequests: [OneSignalRequest] = [] /// Requests that have entered `execute` (including those still held / delayed). public private(set) var startedRequests: [OneSignalRequest] = [] + public private(set) var completedRequests: [OneSignalRequest] = [] public var executeInstantaneously = false /// Set to true to make it unnecessary to setup mock responses for every request possible public var fireSuccessForAllRequests = false @@ -91,6 +92,7 @@ public class MockOneSignalClient: NSObject, IOneSignalClient { networkRequestCount = 0 executedRequests.removeAll() startedRequests.removeAll() + completedRequests.removeAll() heldExecutions.removeAll() holdResponses = false executeInstantaneously = true @@ -181,6 +183,10 @@ public class MockOneSignalClient: NSObject, IOneSignalClient { allRequestsHandled = false print("🧪 cannot find a mock response for request: \(stringifiedRequest)") } + + lock.withLock { + completedRequests.append(request) + } } func didCompleteRequest(_ request: OneSignalRequest) { @@ -214,9 +220,10 @@ extension MockOneSignalClient { */ @objc public func onlyOneRequest(contains path: String, contains payload: [String: Any]) -> Bool { + let requests = lock.withLock { executedRequests } var found = false - for request in executedRequests { + for request in requests { guard let params = request.parameters as? NSDictionary else { continue } @@ -238,7 +245,8 @@ extension MockOneSignalClient { } public func hasExecutedRequestOfType(_ type: AnyClass, expectedCount: Int? = nil) -> Bool { - let matchingCount = executedRequests.filter { request in + let requests = lock.withLock { executedRequests } + let matchingCount = requests.filter { request in request.isKind(of: type) }.count @@ -248,4 +256,27 @@ extension MockOneSignalClient { return matchingCount > 0 } } + + public func hasCompletedRequestOfType(_ type: AnyClass, expectedCount: Int? = nil) -> Bool { + let matchingCount = completedRequestCount(ofType: type) + + if let expectedCount { + return matchingCount == expectedCount + } + return matchingCount > 0 + } + + public func completedRequestCount(ofType type: AnyClass) -> Int { + let requests = lock.withLock { completedRequests } + return requests.filter { request in + request.isKind(of: type) + }.count + } + + public func startedRequestCount(ofType type: AnyClass) -> Int { + let requests = lock.withLock { startedRequests } + return requests.filter { request in + request.isKind(of: type) + }.count + } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/OneSignalCoreMocks.swift b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/OneSignalCoreMocks.swift index 20272b36e..20cddf0cd 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/OneSignalCoreMocks.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalCoreMocks/OneSignalCoreMocks.swift @@ -43,11 +43,30 @@ public class OneSignalCoreMocks: NSObject { } } - /** Wait specified number of seconds for any async methods to run */ - @objc - public static func waitForBackgroundThreads(seconds: Double) { - let expectation = XCTestExpectation(description: "Wait for \(seconds) seconds") - _ = XCTWaiter.wait(for: [expectation], timeout: seconds) + public static func waitUntil( + _ description: String, + timeout: TimeInterval = 5, + file: StaticString = #filePath, + line: UInt = #line, + condition: @escaping () -> Bool + ) { + XCTAssertTrue(waitForCondition(timeout: timeout, condition), description, file: file, line: line) + } + + @objc(waitUntilWithTimeout:condition:) + public static func waitUntilForObjC( + timeout: TimeInterval, + condition: @escaping @convention(block) () -> Bool + ) -> Bool { + waitForCondition(timeout: timeout, condition) + } + + private static func waitForCondition(timeout: TimeInterval, _ condition: () -> Bool) -> Bool { + let deadline = Date().addingTimeInterval(timeout) + while !condition() && Date() < deadline { + RunLoop.current.run(until: min(deadline, Date().addingTimeInterval(0.01))) + } + return condition() } @objc public static func backgroundApp() { diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/EarlyTriggerTrackingTests.swift b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/EarlyTriggerTrackingTests.swift index bedfe909e..fda6367cd 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/EarlyTriggerTrackingTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/EarlyTriggerTrackingTests.swift @@ -74,6 +74,7 @@ final class EarlyTriggerTrackingTests: XCTestCase { func testHasCompletedFirstFetch_isSetAfterFirstFetch() throws { /* Setup */ let client = MockOneSignalClient() + client.executeInstantaneously = true OneSignalCoreImpl.setSharedClient(client) OSMessagingController.start() let controller = OSMessagingController.sharedInstance() @@ -92,7 +93,11 @@ final class EarlyTriggerTrackingTests: XCTestCase { /* Execute */ OneSignalUserManagerImpl.sharedInstance.start() - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + let fetchCompleted = XCTNSPredicateExpectation( + predicate: NSPredicate { _, _ in controller.hasCompletedFirstFetch }, + object: nil + ) + XCTAssertEqual(XCTWaiter.wait(for: [fetchCompleted], timeout: 5), .completed) /* Verify */ XCTAssertTrue(controller.hasCompletedFirstFetch) @@ -191,7 +196,9 @@ final class EarlyTriggerTrackingTests: XCTestCase { // Start the SDK and trigger first fetch OneSignalUserManagerImpl.sharedInstance.start() - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 2.0) + OneSignalCoreMocks.waitUntil("Initial IAM fetch did not complete") { + controller.hasCompletedFirstFetch + } // Verify first fetch completed XCTAssertTrue(controller.hasCompletedFirstFetch) @@ -293,7 +300,9 @@ final class EarlyTriggerTrackingTests: XCTestCase { /* Execute */ // Start the SDK and trigger first fetch OneSignalUserManagerImpl.sharedInstance.start() - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 2.0) + OneSignalCoreMocks.waitUntil("IAM messages were not loaded") { + controller.hasCompletedFirstFetch && controller.messages.count == 3 + } /* Verify */ // First fetch should have completed @@ -369,7 +378,9 @@ final class EarlyTriggerTrackingTests: XCTestCase { /* Execute */ // Start the SDK and trigger first fetch OneSignalUserManagerImpl.sharedInstance.start() - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 1.0) + OneSignalCoreMocks.waitUntil("IAM message was not loaded") { + controller.hasCompletedFirstFetch && controller.messages.count == 1 + } /* Verify */ XCTAssertTrue(controller.hasCompletedFirstFetch) @@ -439,7 +450,9 @@ final class EarlyTriggerTrackingTests: XCTestCase { /* Execute */ OneSignalUserManagerImpl.sharedInstance.start() - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 1.0) + OneSignalCoreMocks.waitUntil("IAM messages were not loaded") { + controller.hasCompletedFirstFetch && controller.messages.count == 2 + } /* Verify */ let messages = controller.messages as! [OSInAppMessageInternal] diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/IAMIntegrationTests.swift b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/IAMIntegrationTests.swift index 9efcb6f51..43f128826 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/IAMIntegrationTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/IAMIntegrationTests.swift @@ -27,8 +27,8 @@ with services provided by OneSignal. import XCTest @testable import OneSignalInAppMessages +@testable import OneSignalUser import OneSignalOSCore -import OneSignalUser import OneSignalCoreMocks import OneSignalOSCoreMocks import OneSignalUserMocks @@ -38,6 +38,8 @@ import OneSignalInAppMessagesMocks These tests can include some Obj-C InAppMessagingIntegrationTests migrations. */ final class IAMIntegrationTests: XCTestCase { + private let testOneSignalId = "test-onesignal-id-12345" + override func setUpWithError() throws { OneSignalCoreMocks.clearUserDefaults() OneSignalUserMocks.reset() @@ -85,7 +87,11 @@ final class IAMIntegrationTests: XCTestCase { OneSignalIdentifiers.currentAppId = "test-app-id" // 2. Set up mock responses for the anonymous user, as the user needs an OSID - MockUserRequests.setDefaultCreateAnonUserResponses(with: client) + MockUserRequests.setDefaultCreateAnonUserResponses( + with: client, + onesignalId: testOneSignalId, + subscriptionId: testPushSubId + ) // 3. Set up mock responses for fetching IAMs let message = IAMTestHelpers.testMessageJsonWithTrigger(kind: OS_DYNAMIC_TRIGGER_KIND_CUSTOM, property: "session_time", triggerId: "test_id1", type: 1, value: 10.0) @@ -94,19 +100,21 @@ final class IAMIntegrationTests: XCTestCase { request: "", response: response) - // 4. Unblock the Consistency Manager to allow fetching of IAMs - ConsistencyManagerTestHelpers.setDefaultRywToken(id: anonUserOSID) - - // 5. Pausing should prevent messages from being evaluated and shown + // 4. Pausing should prevent messages from being evaluated and shown OneSignalInAppMessages.__paused(true) - // 6. Start the user manager to generate a user instance + // 5. Start the user manager to generate a user instance OneSignalUserManagerImpl.sharedInstance.start() - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Anonymous user creation did not complete") { + client.hasCompletedRequestOfType(OSRequestCreateUser.self) + } - // 7. Fetch IAMs + // 6. Unblock the Consistency Manager and fetch IAMs + ConsistencyManagerTestHelpers.setDefaultRywToken(id: testOneSignalId) OneSignalInAppMessages.getFromServer(testPushSubId) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("IAM fetch did not complete") { + client.hasCompletedRequestOfType(OSRequestGetInAppMessages.self) + } // Make sure no IAM is showing, and the queue has no IAMs XCTAssertFalse(OSMessagingController.sharedInstance().isInAppMessageShowing) diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/OSMessagingControllerUserStateTests.swift b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/OSMessagingControllerUserStateTests.swift index 8d7ada7cb..1facda710 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/OSMessagingControllerUserStateTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessagesTests/OSMessagingControllerUserStateTests.swift @@ -83,7 +83,10 @@ final class OSMessagingControllerUserStateTests: XCTestCase { /* Execute */ OneSignalInAppMessages.getFromServer(testSubscriptionId) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Deferred IAM subscription ID was not stored") { + OSMessagingController.sharedInstance() + .value(forKey: "shouldFetchOnUserChangeWithSubscriptionID") as? String == self.testSubscriptionId + } /* Verify */ // The controller should have stored the subscription ID for retry @@ -126,7 +129,9 @@ final class OSMessagingControllerUserStateTests: XCTestCase { // First attempt: Try to fetch IAMs without OneSignal ID (should be deferred) OneSignalInAppMessages.getFromServer(testSubscriptionId) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Deferred IAM subscription ID was not stored") { + controller.value(forKey: "shouldFetchOnUserChangeWithSubscriptionID") as? String == self.testSubscriptionId + } // Verify the subscription ID was stored and no IAM fetch occurred XCTAssertEqual(controller.value(forKey: "shouldFetchOnUserChangeWithSubscriptionID") as! String, testSubscriptionId) @@ -136,7 +141,10 @@ final class OSMessagingControllerUserStateTests: XCTestCase { MockUserRequests.setDefaultIdentifyUserResponses(with: client, externalId: testExternalId) OneSignalUserManagerImpl.sharedInstance.userExecutor?.userRequestQueue.first?.sentToClient = false OneSignalUserManagerImpl.sharedInstance.userExecutor?.executePendingRequests() - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Deferred IAM fetch was not retried") { + client.hasCompletedRequestOfType(OSRequestGetInAppMessages.self) + && controller.value(forKey: "shouldFetchOnUserChangeWithSubscriptionID") == nil + } /* Verify */ // The fetch should have been retried now that OneSignal ID is available @@ -172,7 +180,9 @@ final class OSMessagingControllerUserStateTests: XCTestCase { ) ConsistencyManagerTestHelpers.setDefaultRywToken(id: testOneSignalId) OneSignalUserManagerImpl.sharedInstance.start() - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Initial IAM fetch did not complete") { + client.hasCompletedRequestOfType(OSRequestGetInAppMessages.self) + } /* Verify */ // IAM is fetched and no retry is pending @@ -183,7 +193,9 @@ final class OSMessagingControllerUserStateTests: XCTestCase { // Trigger a normal user state change by login MockUserRequests.setDefaultIdentifyUserResponses(with: client, externalId: testExternalId) OneSignalUserManagerImpl.sharedInstance.login(externalId: testExternalId, token: nil) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Identify user request did not complete") { + client.hasCompletedRequestOfType(OSRequestIdentifyUser.self) + } /* Verify */ // Does not fetch IAMs again diff --git a/iOS_SDK/OneSignalSDK/OneSignalLiveActivitiesTests/OSLiveActivitiesExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalLiveActivitiesTests/OSLiveActivitiesExecutorTests.swift index dc02f994c..43c13db6e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalLiveActivitiesTests/OSLiveActivitiesExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalLiveActivitiesTests/OSLiveActivitiesExecutorTests.swift @@ -49,15 +49,11 @@ final class OSLiveActivitiesExecutorTests: XCTestCase { override func tearDownWithError() throws { } - // Subscribes a user, then resets the client so tests assert only on the requests they make. private func setUpSubscribedUser() -> MockOneSignalClient { let mockClient = MockOneSignalClient() + mockClient.executeInstantaneously = true OneSignalCoreImpl.setSharedClient(mockClient) - OneSignalUserDefaults.initShared().saveString(forKey: OSUD_LEGACY_PLAYER_ID, withValue: "my-subscription-id") - OneSignalUserManagerImpl.sharedInstance.start() - // Wait for any user setup requests to complete - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2) - mockClient.reset() + OneSignalUserDefaults.initShared().saveString(forKey: OSUD_PUSH_SUBSCRIPTION_ID, withValue: "my-subscription-id") return mockClient } diff --git a/iOS_SDK/OneSignalSDK/OneSignalNotificationsTests/OneSignalNotificationsTests.swift b/iOS_SDK/OneSignalSDK/OneSignalNotificationsTests/OneSignalNotificationsTests.swift index e178a5732..3b60c1d20 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalNotificationsTests/OneSignalNotificationsTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalNotificationsTests/OneSignalNotificationsTests.swift @@ -61,15 +61,27 @@ final class OneSignalNotificationsTests: XCTestCase { } } + private func setBadgeCountAndWait(_ count: Int) { + let badgeSet = expectation(description: "Badge set") + setBadgeCount(count) { + badgeSet.fulfill() + } + wait(for: [badgeSet], timeout: 5) + } + + private func waitForCachedBadgeCount(_ count: Int) { + let badgeUpdated = XCTNSPredicateExpectation( + predicate: NSPredicate { _, _ in self.getCachedBadgeCount() == count }, + object: nil + ) + XCTAssertEqual(XCTWaiter.wait(for: [badgeUpdated], timeout: 5), .completed) + } + func testClearBadgesWhenAppEntersForeground() throws { // NotificationManager Start to register lifecycle listener OSNotificationsManager.startSwizzling() // Set badge count > 0 - let expectation = self.expectation(description: "Badge set") - setBadgeCount(1) { - expectation.fulfill() - } - wait(for: [expectation], timeout: 0.5) + setBadgeCountAndWait(1) // Verify badge was set XCTAssertEqual(getCachedBadgeCount(), 1) @@ -79,8 +91,7 @@ final class OneSignalNotificationsTests: XCTestCase { // Foreground the app OneSignalCoreMocks.foregroundApp() - // Wait for async badge clearing on iOS 16+ - Thread.sleep(forTimeInterval: 0.1) + waitForCachedBadgeCount(0) // Ensure that badge count == 0 XCTAssertEqual(getCachedBadgeCount(), 0) @@ -90,11 +101,7 @@ final class OneSignalNotificationsTests: XCTestCase { // NotificationManager Start to register lifecycle listener OSNotificationsManager.startSwizzling() // Set badge count > 0 - let expectation = self.expectation(description: "Badge set") - setBadgeCount(1) { - expectation.fulfill() - } - wait(for: [expectation], timeout: 0.5) + setBadgeCountAndWait(1) // Verify badge was set XCTAssertEqual(getCachedBadgeCount(), 1) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift index 223080697..394dc5798 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift @@ -131,6 +131,12 @@ public class OSOperationRepo: NSObject { } } + func flushAndWait() { + dispatchQueue.sync { + flushDeltaQueue() + } + } + private func flushDeltaQueue(inBackground: Bool = false) { guard !paused else { OneSignalLog.onesignalLog(.LL_DEBUG, message: "OSOperationRepo not flushing queue due to being paused") diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreMocks/MockNewRecordsState.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreMocks/MockNewRecordsState.swift index 25a6444f7..f76aae04d 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCoreMocks/MockNewRecordsState.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreMocks/MockNewRecordsState.swift @@ -25,6 +25,7 @@ THE SOFTWARE. */ +import Foundation @testable import OneSignalOSCore public class MockNewRecordsState: OSNewRecordsState { @@ -33,11 +34,18 @@ public class MockNewRecordsState: OSNewRecordsState { let overwrite: Bool } - public var records: [MockNewRecord] = [] + private let lock = NSLock() + private var storedRecords: [MockNewRecord] = [] + + public var records: [MockNewRecord] { + lock.withLock { storedRecords } + } override public func add(_ key: String, _ overwrite: Bool = false) { let record = MockNewRecord(key: key, overwrite: overwrite) - records.append(record) + lock.withLock { + storedRecords.append(record) + } super.add(key, overwrite) } diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSConsistencyManagerTests.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSConsistencyManagerTests.swift index 9a686d644..b48125850 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSConsistencyManagerTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSConsistencyManagerTests.swift @@ -249,48 +249,38 @@ class OSConsistencyManagerTests: XCTestCase { } func testConcurrentUpdatesToTokens() { - let expectation = self.expectation(description: "Concurrent updates handled correctly") - let id = "test_id" - let key = OSIamFetchOffsetKey.userUpdate - let rywToken1 = "123" - let rywToken2 = "456" let rywDelay = 0 as NSNumber - let value1 = OSReadYourWriteData(rywToken: rywToken1, rywDelay: rywDelay) - let value2 = OSReadYourWriteData(rywToken: rywToken2, rywDelay: rywDelay) - - // Set up concurrent queues - let queue1 = DispatchQueue(label: "com.test.queue1", attributes: .concurrent) - let queue2 = DispatchQueue(label: "com.test.queue2", attributes: .concurrent) - - // Perform concurrent token updates - queue1.async { - self.consistencyManager.setRywTokenAndDelay( - id: id, - key: key, - value: OSReadYourWriteData(rywToken: rywToken1, rywDelay: rywDelay) - ) + let value1 = OSReadYourWriteData(rywToken: "123", rywDelay: rywDelay) + let value2 = OSReadYourWriteData(rywToken: "456", rywDelay: rywDelay) + let updates: [(OSIamFetchOffsetKey, OSReadYourWriteData)] = [ + (.userUpdate, value1), + (.subscriptionUpdate, value2) + ] + let updateGroup = DispatchGroup() + + for (key, value) in updates { + updateGroup.enter() + DispatchQueue.global().async { + self.consistencyManager.setRywTokenAndDelay(id: id, key: key, value: value) + updateGroup.leave() + } } - queue2.async { - self.consistencyManager.setRywTokenAndDelay( - id: id, - key: key, - value: OSReadYourWriteData(rywToken: rywToken2, rywDelay: rywDelay) - ) + guard updateGroup.wait(timeout: .now() + 2) == .success else { + XCTFail("Concurrent token updates timed out") + return } - // Allow some time for the updates to happen - DispatchQueue.global().asyncAfter(deadline: .now() + 0.5) { - // Check that the most recent value was correctly set - let condition = TestMetCondition(expectedTokens: [id: [NSNumber(value: key.rawValue): value2]]) - let rywData = self.consistencyManager.getRywTokenFromAwaitableCondition(condition, forId: id) - - XCTAssertEqual(rywData?.rywToken, "456") - expectation.fulfill() - } + let condition = TestMetCondition(expectedTokens: [ + id: [ + NSNumber(value: OSIamFetchOffsetKey.userUpdate.rawValue): value1, + NSNumber(value: OSIamFetchOffsetKey.subscriptionUpdate.rawValue): value2 + ] + ]) + let rywData = consistencyManager.getRywTokenFromAwaitableCondition(condition, forId: id) - waitForExpectations(timeout: 2.0, handler: nil) + XCTAssertEqual(rywData?.rywToken, "456") } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalUserMocks.swift b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalUserMocks.swift index da94af841..1e0f513c2 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalUserMocks.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalUserMocks.swift @@ -37,6 +37,13 @@ public class OneSignalUserMocks: NSObject { // TODO: create mocked server responses to user requests @objc public static func reset() { + OSResilientStorage.setStrings([ + OSResilientStorage.keyAppId: "", + OSResilientStorage.keySubscriptionId: "", + OSResilientStorage.keyReceiveReceiptsEnabled: "", + OSResilientStorage.keyHasPriorSession: "" + ]) + _ = OSResilientStorage.snapshot() OSCoreMocks.resetOperationRepo() OneSignalUserManagerImpl.sharedInstance.reset() } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/CustomEventsIntegrationTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/CustomEventsIntegrationTests.swift index e64b057dc..dfd03fda9 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/CustomEventsIntegrationTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/CustomEventsIntegrationTests.swift @@ -63,7 +63,9 @@ final class CustomEventsIntegrationTests: XCTestCase { /* When */ userManager.trackEvent(name: "test_event", properties: properties) OSOperationRepo.sharedInstance.addFlushDeltaQueueToDispatchQueue() - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Custom event request did not complete") { + client.hasCompletedRequestOfType(OSRequestCustomEvents.self) + } /* Then */ XCTAssertTrue(client.hasExecutedRequestOfType(OSRequestCustomEvents.self)) @@ -81,7 +83,9 @@ final class CustomEventsIntegrationTests: XCTestCase { /* When */ userManager.trackEvent(name: "test_event", properties: nil) OSOperationRepo.sharedInstance.addFlushDeltaQueueToDispatchQueue() - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Custom event request did not complete") { + client.hasCompletedRequestOfType(OSRequestCustomEvents.self) + } /* Then */ XCTAssertTrue(client.hasExecutedRequestOfType(OSRequestCustomEvents.self)) @@ -99,7 +103,9 @@ final class CustomEventsIntegrationTests: XCTestCase { /* When */ userManager.trackEvent(name: "test_event", properties: [:]) OSOperationRepo.sharedInstance.addFlushDeltaQueueToDispatchQueue() - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Custom event request did not complete") { + client.hasCompletedRequestOfType(OSRequestCustomEvents.self) + } /* Then */ XCTAssertTrue(client.hasExecutedRequestOfType(OSRequestCustomEvents.self)) @@ -120,7 +126,6 @@ final class CustomEventsIntegrationTests: XCTestCase { /* When */ userManager.trackEvent(name: "test_event", properties: invalidProperties) OSOperationRepo.sharedInstance.addFlushDeltaQueueToDispatchQueue() - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) /* Then - No request should be made */ XCTAssertFalse(client.hasExecutedRequestOfType(OSRequestCustomEvents.self)) @@ -155,7 +160,9 @@ final class CustomEventsIntegrationTests: XCTestCase { /* When */ userManager.trackEvent(name: "complex_event", properties: complexProperties) OSOperationRepo.sharedInstance.addFlushDeltaQueueToDispatchQueue() - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Custom event request did not complete") { + client.hasCompletedRequestOfType(OSRequestCustomEvents.self) + } /* Then */ XCTAssertTrue(client.hasExecutedRequestOfType(OSRequestCustomEvents.self)) @@ -221,7 +228,9 @@ final class CustomEventsIntegrationTests: XCTestCase { /* When */ userManager.trackEvent(name: "array_event", properties: properties) OSOperationRepo.sharedInstance.addFlushDeltaQueueToDispatchQueue() - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Custom event request did not complete") { + client.hasCompletedRequestOfType(OSRequestCustomEvents.self) + } /* Then */ XCTAssertTrue(client.hasExecutedRequestOfType(OSRequestCustomEvents.self)) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/OSCustomEventsExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/OSCustomEventsExecutorTests.swift index 3ea47ff03..dedab9808 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/OSCustomEventsExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/OSCustomEventsExecutorTests.swift @@ -85,7 +85,9 @@ final class OSCustomEventsExecutorTests: XCTestCase { /* When */ mocks.customEventsExecutor.enqueueDelta(delta) mocks.customEventsExecutor.processDeltaQueue(inBackground: false) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Custom event request did not complete") { + mocks.client.hasCompletedRequestOfType(OSRequestCustomEvents.self) + } /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCustomEvents.self)) @@ -151,7 +153,9 @@ final class OSCustomEventsExecutorTests: XCTestCase { /* When */ mocks.customEventsExecutor.enqueueDelta(delta) mocks.customEventsExecutor.processDeltaQueue(inBackground: false) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Custom event request did not complete") { + mocks.client.hasCompletedRequestOfType(OSRequestCustomEvents.self) + } /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCustomEvents.self)) @@ -190,7 +194,9 @@ final class OSCustomEventsExecutorTests: XCTestCase { /* When */ mocks.customEventsExecutor.enqueueDelta(delta) mocks.customEventsExecutor.processDeltaQueue(inBackground: false) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Custom event request did not complete") { + mocks.client.hasCompletedRequestOfType(OSRequestCustomEvents.self) + } /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCustomEvents.self)) @@ -229,7 +235,9 @@ final class OSCustomEventsExecutorTests: XCTestCase { mocks.customEventsExecutor.enqueueDelta(delta2) mocks.customEventsExecutor.enqueueDelta(delta3) mocks.customEventsExecutor.processDeltaQueue(inBackground: false) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Custom event requests did not complete") { + mocks.client.hasCompletedRequestOfType(OSRequestCustomEvents.self, expectedCount: 3) + } /* Then */ // Should have 3 separate requests, one per event (no batching) @@ -276,7 +284,9 @@ final class OSCustomEventsExecutorTests: XCTestCase { mocks.customEventsExecutor.enqueueDelta(deltaUserA2) mocks.customEventsExecutor.enqueueDelta(deltaUserB1) mocks.customEventsExecutor.processDeltaQueue(inBackground: false) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Custom event requests did not complete") { + mocks.client.hasCompletedRequestOfType(OSRequestCustomEvents.self, expectedCount: 3) + } /* Then */ // Should have 3 separate requests, one per event (no batching) @@ -322,7 +332,13 @@ final class OSCustomEventsExecutorTests: XCTestCase { /* When */ mocks.customEventsExecutor.enqueueDelta(delta) mocks.customEventsExecutor.processDeltaQueue(inBackground: false) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Blocked custom event delta was not cached") { + let deltas = OneSignalUserDefaults.initShared().getSavedCodeableData( + forKey: OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY, + defaultValue: [] + ) as? [OSDelta] + return deltas?.count == 1 + } /* Then */ // No request should be made @@ -342,7 +358,13 @@ final class OSCustomEventsExecutorTests: XCTestCase { /* When */ mocks.customEventsExecutor.enqueueDelta(delta) mocks.customEventsExecutor.cacheDeltaQueue() - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.3) + OneSignalCoreMocks.waitUntil("Custom event delta was not cached") { + let deltas = OneSignalUserDefaults.initShared().getSavedCodeableData( + forKey: OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY, + defaultValue: [] + ) as? [OSDelta] + return deltas?.count == 1 + } /* Then - Verify delta is cached */ let cachedDeltas = OneSignalUserDefaults.initShared().getSavedCodeableData( @@ -369,7 +391,9 @@ final class OSCustomEventsExecutorTests: XCTestCase { mocks.client.fireSuccessForAllRequests = true mocks.customEventsExecutor.processDeltaQueue(inBackground: false) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Uncached custom event request did not complete") { + mocks.client.hasCompletedRequestOfType(OSRequestCustomEvents.self) + } /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCustomEvents.self)) @@ -398,7 +422,9 @@ final class OSCustomEventsExecutorTests: XCTestCase { /* When */ mocks.customEventsExecutor.enqueueDelta(delta) mocks.customEventsExecutor.processDeltaQueue(inBackground: false) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Custom event request did not complete") { + mocks.client.hasCompletedRequestOfType(OSRequestCustomEvents.self) + } /* Then */ guard let request = mocks.client.executedRequests.first as? OSRequestCustomEvents, diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionUpdateRaceTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionUpdateRaceTests.swift index 43c1746e9..6726ff1d8 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionUpdateRaceTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionUpdateRaceTests.swift @@ -98,7 +98,13 @@ final class SubscriptionUpdateRaceTests: XCTestCase { value: promptedNeverAnswered )) executor.processDeltaQueue(inBackground: false) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2) + OneSignalCoreMocks.waitUntil("Blocked subscription update was not cached") { + let requests = OneSignalUserDefaults.initShared().getSavedCodeableData( + forKey: OS_SUBSCRIPTION_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, + defaultValue: [] + ) as? [OSRequestUpdateSubscription] + return requests?.count == 1 + } XCTAssertTrue(client.executedRequests.isEmpty, "Update should still be pending without subscriptionId") @@ -115,7 +121,9 @@ final class SubscriptionUpdateRaceTests: XCTestCase { value: subscribedNotificationTypes )) executor.processDeltaQueue(inBackground: false) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Subscription update did not complete") { + client.hasCompletedRequestOfType(OSRequestUpdateSubscription.self) + } let updateRequests = client.executedRequests.compactMap { $0 as? OSRequestUpdateSubscription } XCTAssertFalse(updateRequests.isEmpty, "Expected at least one UpdateSubscription after id hydration") @@ -155,7 +163,9 @@ final class SubscriptionUpdateRaceTests: XCTestCase { value: promptedNeverAnswered )) executor.processDeltaQueue(inBackground: false) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2) + OneSignalCoreMocks.waitUntil("First subscription update did not start") { + client.startedRequestCount(ofType: OSRequestUpdateSubscription.self) == 1 + } XCTAssertEqual(client.startedRequests.count, 1, "First UpdateSubscription should be in flight") let firstPayload = try XCTUnwrap( @@ -176,12 +186,20 @@ final class SubscriptionUpdateRaceTests: XCTestCase { value: subscribedNotificationTypes )) executor.processDeltaQueue(inBackground: false) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.2) + OneSignalCoreMocks.waitUntil("Follow-up subscription update was not queued") { + let requests = OneSignalUserDefaults.initShared().getSavedCodeableData( + forKey: OS_SUBSCRIPTION_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, + defaultValue: [] + ) as? [OSRequestUpdateSubscription] + return requests?.count == 2 + } XCTAssertEqual(client.startedRequests.count, 1, "Follow-up must wait for in-flight UpdateSubscription") client.releaseHeldResponses() - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Follow-up subscription update did not start") { + client.startedRequestCount(ofType: OSRequestUpdateSubscription.self) == 2 + } XCTAssertEqual(client.startedRequests.count, 2, "Pending follow-up should send after in-flight completes") let secondPayload = try XCTUnwrap( @@ -219,7 +237,9 @@ final class SubscriptionUpdateRaceTests: XCTestCase { value: promptedNeverAnswered )) executor.processDeltaQueue(inBackground: false) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Retryable subscription update did not complete") { + client.hasCompletedRequestOfType(OSRequestUpdateSubscription.self) + } XCTAssertEqual(client.executedRequests.count, 1, "First update should have been attempted and failed retryably") @@ -236,7 +256,9 @@ final class SubscriptionUpdateRaceTests: XCTestCase { value: subscribedNotificationTypes )) executor.processDeltaQueue(inBackground: false) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Follow-up subscription update did not complete") { + client.hasCompletedRequestOfType(OSRequestUpdateSubscription.self, expectedCount: 2) + } let updateRequests = client.executedRequests.compactMap { $0 as? OSRequestUpdateSubscription } XCTAssertEqual(updateRequests.count, 2, "Follow-up update must still send after a retryable failure") diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift index 703079453..36a2c712d 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift @@ -72,7 +72,10 @@ final class UserExecutorTests: XCTestCase { /* When */ mocks.userExecutor.createUser(mocks.createUserInstance(externalId: userA_EUID)) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Create user response was not applied") { + mocks.newRecordsState.contains(userA_OSID) + && mocks.newRecordsState.contains("push-sub-id") + } /* Then */ XCTAssertTrue(mocks.newRecordsState.contains(userA_OSID)) @@ -88,7 +91,9 @@ final class UserExecutorTests: XCTestCase { let identityModel = OSIdentityModel(aliases: [OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) mocks.userExecutor.createUser(aliasLabel: OS_EXTERNAL_ID, aliasId: userA_EUID, identityModel: identityModel) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Create user request did not complete") { + mocks.client.hasCompletedRequestOfType(OSRequestCreateUser.self) + } /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateUser.self)) @@ -107,13 +112,14 @@ final class UserExecutorTests: XCTestCase { /* When */ let anonIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID], changeNotifier: OSEventProducer()) - let newIdentityModel = OSIdentityModel(aliases: [OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) - - // The current user needs to be the same, set it in the user manager - OneSignalUserManagerImpl.sharedInstance.identityModelStore.add(id: OS_IDENTITY_MODEL_KEY, model: newIdentityModel, hydrating: false) + let newIdentityModel = OneSignalUserMocks + .setUserManagerInternalUser(externalId: userA_EUID, onesignalId: nil) + .identityModel mocks.userExecutor.identifyUser(externalId: userA_EUID, identityModelToIdentify: anonIdentityModel, identityModelToUpdate: newIdentityModel) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Identify user response was not applied") { + mocks.newRecordsState.wasOverwritten(userA_OSID) + } /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestIdentifyUser.self)) @@ -135,7 +141,9 @@ final class UserExecutorTests: XCTestCase { let newIdentityModel = OSIdentityModel(aliases: [OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) mocks.userExecutor.identifyUser(externalId: userA_EUID, identityModelToIdentify: anonIdentityModel, identityModelToUpdate: newIdentityModel) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Identify user request did not complete") { + mocks.client.hasCompletedRequestOfType(OSRequestIdentifyUser.self) + } /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestIdentifyUser.self)) @@ -158,7 +166,11 @@ final class UserExecutorTests: XCTestCase { /* When */ mocks.userExecutor.identifyUser(externalId: userB_EUID, identityModelToIdentify: anonIdentityModel, identityModelToUpdate: newIdentityModel) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + let userCreated = XCTNSPredicateExpectation( + predicate: NSPredicate { _, _ in mocks.newRecordsState.contains(userB_OSID) }, + object: nil + ) + XCTAssertEqual(XCTWaiter.wait(for: [userCreated], timeout: 5), .completed) /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestIdentifyUser.self)) @@ -180,7 +192,9 @@ final class UserExecutorTests: XCTestCase { /* When */ mocks.userExecutor.identifyUser(externalId: userB_EUID, identityModelToIdentify: anonIdentityModel, identityModelToUpdate: newIdentityModel) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Conflict create user request did not complete") { + mocks.client.hasCompletedRequestOfType(OSRequestCreateUser.self) + } /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestIdentifyUser.self)) @@ -212,7 +226,9 @@ final class UserExecutorTests: XCTestCase { /* When */ mocks.userExecutor.fetchUser(aliasLabel: OS_ONESIGNAL_ID, aliasId: userA_OSID, identityModel: staleIdentityModel, onNewSession: true) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Stale fetch user request did not complete") { + mocks.client.hasCompletedRequestOfType(OSRequestFetchUser.self) + } /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestFetchUser.self)) @@ -236,7 +252,10 @@ final class UserExecutorTests: XCTestCase { /* When */ mocks.userExecutor.fetchUser(aliasLabel: OS_ONESIGNAL_ID, aliasId: userA_OSID, identityModel: currentUser.identityModel, onNewSession: false) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Current user fetch response was not applied") { + currentUser.identityModel.aliases["stale_label"] == nil + && currentUser.identityModel.externalId == userA_EUID + } /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestFetchUser.self)) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserObjcTests.m b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserObjcTests.m index 9348f639e..1693e2ab7 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserObjcTests.m +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserObjcTests.m @@ -32,14 +32,12 @@ - (void)testSendPurchases { /* Setup */ MockOneSignalClient* client = [MockOneSignalClient new]; - - // 0. Purchases will be dropped if there is no user instance. - [OneSignalUserManagerImpl.sharedInstance start]; - - // 1. Set up mock responses for the anonymous user [MockUserRequests setDefaultCreateAnonUserResponsesWith:client onesignalId:nil subscriptionId:nil]; [OneSignalCoreImpl setSharedClient:client]; + // Purchases will be dropped if there is no user instance. + [OneSignalUserManagerImpl.sharedInstance start]; + /* When */ NSMutableArray* arrayOfPurchases = [NSMutableArray new]; @@ -66,14 +64,14 @@ - (void)testSendPurchases { [OneSignalUserManagerImpl.sharedInstance sendPurchases:arrayOfPurchases]; - // Run background threads - [OneSignalCoreMocks waitForBackgroundThreadsWithSeconds:0.5]; - /* Then */ NSString* path = [NSString stringWithFormat:@"apps/test-app-id/users/by/onesignal_id/%@", @"test_anon_user_onesignal_id"]; NSDictionary *payload = [NSDictionary dictionaryWithObject:[NSDictionary dictionaryWithObject:arrayOfPurchases forKey:@"purchases"] forKey:@"deltas"]; + XCTAssertTrue([OneSignalCoreMocks waitUntilWithTimeout:5 condition:^BOOL{ + return [client onlyOneRequestWithContains:path contains:payload]; + }]); XCTAssertTrue([client onlyOneRequestWithContains:path contains:payload]); } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift index a66d10394..e130af24b 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift @@ -129,8 +129,7 @@ final class OneSignalUserTests: XCTestCase { // Increase flush interval to allow all the updates to batch OSOperationRepo.sharedInstance.pollIntervalMilliseconds = 300 - // Wait to let any pending flushes in the Operation Repo to run - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.1) + OSOperationRepo.sharedInstance.flushAndWait() /* When */ @@ -169,7 +168,9 @@ final class OneSignalUserTests: XCTestCase { /* Then */ - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 1) + OneSignalCoreMocks.waitUntil("Combined property update did not complete") { + client.hasCompletedRequestOfType(OSRequestUpdateProperties.self) + } let expectedPayload: [String: Any] = [ "deltas": [ @@ -239,7 +240,9 @@ final class OneSignalUserTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.start() // Let the anonymous user be created so it has a OneSignal ID for the update request - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Anonymous user creation did not complete") { + client.hasCompletedRequestOfType(OSRequestCreateUser.self) + } /* When */ // Tags are applied optimistically to the local model and queued as an update request @@ -251,7 +254,9 @@ final class OneSignalUserTests: XCTestCase { XCTAssertTrue(OneSignalUserManagerImpl.sharedInstance.getTags().isEmpty) // Let the queued UpdateProperties request flush and its 202 echo be processed - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 1) + OneSignalCoreMocks.waitUntil("Confirmed tags were not restored") { + OneSignalUserManagerImpl.sharedInstance.getTags() == tags + } /* Then */ // The confirmed tags from the 202 response are merged back into the local model @@ -275,12 +280,13 @@ final class OneSignalUserTests: XCTestCase { let checkedUser = manager.currentUser(matching: userA.identityModel.modelId) // A concurrent login switches the current user before the response is applied let userB = OneSignalUserMocks.setUserManagerInternalUser(externalId: userB_EUID, onesignalId: userB_OSID) + let userBLanguage = userB.propertiesModel.language checkedUser?.propertiesModel.hydrate(["language": "language-for-user-a"]) /* Then */ // The response's data went to the user it was for, and the new current user is untouched XCTAssertEqual(userA.propertiesModel.language, "language-for-user-a") - XCTAssertNil(userB.propertiesModel.language) + XCTAssertEqual(userB.propertiesModel.language, userBLanguage) XCTAssertEqual(manager._user?.identityModel.externalId, userB_EUID) } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/SwitchUserIntegrationTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/SwitchUserIntegrationTests.swift index 2c598b6bb..2c04b9c0a 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/SwitchUserIntegrationTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/SwitchUserIntegrationTests.swift @@ -50,8 +50,16 @@ final class SwitchUserIntegrationTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.login(externalId: userB_EUID, token: nil) OneSignalUserManagerImpl.sharedInstance.addTag(key: "tag_b", value: "value_b") - // 3. Run background threads - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + let userBTagsSent = XCTNSPredicateExpectation( + predicate: NSPredicate { _, _ in + client.onlyOneRequest( + contains: "apps/test-app-id/users/by/onesignal_id/\(userB_OSID)", + contains: ["properties": ["tags": tagsUserB]] + ) + }, + object: nil + ) + XCTAssertEqual(XCTWaiter.wait(for: [userBTagsSent], timeout: 5), .completed) /* Then */ @@ -108,6 +116,12 @@ final class SwitchUserIntegrationTests: XCTestCase { // Returns mocked user data to test hydration MockUserRequests.setDefaultFetchUserResponseForHydration(with: client, externalId: userA_EUID) + OneSignalUserManagerImpl.sharedInstance.start() + OneSignalCoreMocks.waitUntil("Anonymous user creation did not complete") { + client.hasCompletedRequestOfType(OSRequestCreateUser.self) + } + OSOperationRepo.sharedInstance.paused = true + /* When */ // 1. Anonymous user @@ -123,8 +137,12 @@ final class SwitchUserIntegrationTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.addAlias(label: "alias_a", id: "id_a") OneSignalUserManagerImpl.sharedInstance.addEmail("email_a@example.com") - // 3. Run background threads - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OSOperationRepo.sharedInstance.paused = false + OSOperationRepo.sharedInstance.flushAndWait() + + OneSignalCoreMocks.waitUntil("User A updates and hydration did not complete") { + self.userAUpdatesAndHydrationCompleted(client, tagsUserA) + } /* Then */ @@ -190,8 +208,6 @@ final class SwitchUserIntegrationTests: XCTestCase { */ func testAnonUser_thenIdentifyUserWithConflict_thenLogout_sendsCorrectUpdatesWithNoFetch() throws { /* Setup */ - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - let client = MockOneSignalClient() OneSignalCoreImpl.setSharedClient(client) @@ -215,6 +231,12 @@ final class SwitchUserIntegrationTests: XCTestCase { MockUserRequests.setAddAliasesResponse(with: client, aliases: ["alias_b": "id_b"]) MockUserRequests.setAddEmailResponse(with: client, email: "email_b@example.com") + OneSignalUserManagerImpl.sharedInstance.start() + OneSignalCoreMocks.waitUntil("Anonymous user creation did not complete") { + client.hasCompletedRequestOfType(OSRequestCreateUser.self) + } + OSOperationRepo.sharedInstance.paused = true + /* When */ // 1. Anonymous user starts @@ -237,8 +259,12 @@ final class SwitchUserIntegrationTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.addAlias(label: "alias_b", id: "id_b") OneSignalUserManagerImpl.sharedInstance.addEmail("email_b@example.com") - // 4. Run background threads - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 1) + OSOperationRepo.sharedInstance.paused = false + OSOperationRepo.sharedInstance.flushAndWait() + + OneSignalCoreMocks.waitUntil("Logged-out user updates were not sent") { + self.userUpdatesCompleted(client, tagsUserA, tagsUserB, anonUserOSID) + } /* Then */ @@ -320,8 +346,7 @@ final class SwitchUserIntegrationTests: XCTestCase { // Increase flush interval to allow all the updates to batch OSOperationRepo.sharedInstance.pollIntervalMilliseconds = 300 - // Wait to let any pending flushes in the Operation Repo to run - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.3) + OSOperationRepo.sharedInstance.flushAndWait() // 1. Set up mock responses for the first anonymous user let tagsUserAnon = ["tag_anon": "value_anon"] @@ -368,8 +393,9 @@ final class SwitchUserIntegrationTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.addAlias(label: "alias_b", id: "id_b") OneSignalUserManagerImpl.sharedInstance.addEmail("email_b@example.com") - // 3. Run background threads - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 2) + OneSignalCoreMocks.waitUntil("User B updates and hydration did not complete") { + self.userUpdatesAndHydrationCompleted(client, tagsUserA, tagsUserB) + } /* Then */ @@ -424,4 +450,67 @@ final class SwitchUserIntegrationTests: XCTestCase { XCTAssertNotNil(OneSignalUserManagerImpl.sharedInstance.user.identityModel.aliases["remote_alias"]) XCTAssertNotNil(OneSignalUserManagerImpl.sharedInstance.subscriptionModelStore.getModel(key: "remote_email@example.com")) } + + private func userUpdatesAndHydrationCompleted( + _ client: MockOneSignalClient, + _ tagsUserA: [String: String], + _ tagsUserB: [String: String] + ) -> Bool { + userUpdatesCompleted(client, tagsUserA, tagsUserB, userB_OSID) + && OneSignalUserManagerImpl.sharedInstance.subscriptionModelStore + .getModel(key: "remote_email@example.com") != nil + } + + private func userUpdatesCompleted( + _ client: MockOneSignalClient, + _ tagsUserA: [String: String], + _ tagsUserB: [String: String], + _ userBOneSignalId: String + ) -> Bool { + client.onlyOneRequest( + contains: "apps/test-app-id/users/by/onesignal_id/\(userA_OSID)", + contains: ["properties": ["language": "lang_a", "tags": tagsUserA]] + ) + && client.onlyOneRequest( + contains: "apps/test-app-id/users/by/onesignal_id/\(userA_OSID)/identity", + contains: ["identity": ["alias_a": "id_a"]] + ) + && client.onlyOneRequest( + contains: "apps/test-app-id/users/by/onesignal_id/\(userA_OSID)/subscriptions", + contains: ["subscription": ["token": "email_a@example.com"]] + ) + && client.onlyOneRequest( + contains: "apps/test-app-id/users/by/onesignal_id/\(userBOneSignalId)", + contains: ["properties": ["language": "lang_b", "tags": tagsUserB]] + ) + && client.onlyOneRequest( + contains: "apps/test-app-id/users/by/onesignal_id/\(userBOneSignalId)/identity", + contains: ["identity": ["alias_b": "id_b"]] + ) + && client.onlyOneRequest( + contains: "apps/test-app-id/users/by/onesignal_id/\(userBOneSignalId)/subscriptions", + contains: ["subscription": ["token": "email_b@example.com"]] + ) + } + + private func userAUpdatesAndHydrationCompleted( + _ client: MockOneSignalClient, + _ tagsUserA: [String: String] + ) -> Bool { + client.allRequestsHandled + && client.onlyOneRequest( + contains: "apps/test-app-id/users/by/onesignal_id/\(userA_OSID)", + contains: ["properties": ["language": "lang_a", "tags": tagsUserA]] + ) + && client.onlyOneRequest( + contains: "apps/test-app-id/users/by/onesignal_id/\(userA_OSID)/identity", + contains: ["identity": ["alias_a": "id_a"]] + ) + && client.onlyOneRequest( + contains: "apps/test-app-id/users/by/onesignal_id/\(userA_OSID)/subscriptions", + contains: ["subscription": ["token": "email_a@example.com"]] + ) + && OneSignalUserManagerImpl.sharedInstance.subscriptionModelStore + .getModel(key: "remote_email@example.com") != nil + } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserConcurrencyTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserConcurrencyTests.swift index 94d3f4388..c3835f051 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserConcurrencyTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserConcurrencyTests.swift @@ -110,8 +110,9 @@ final class UserConcurrencyTests: XCTestCase { executor.executeDeleteSubscriptionRequest(OSRequestDeleteSubscription(subscriptionModel: OSSubscriptionModel(type: .email, address: nil, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer())), inBackground: false) } - // 4. Run background threads - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Concurrent subscription requests did not complete") { + client.completedRequestCount(ofType: OSRequestDeleteSubscription.self) >= 100 + } /* Then */ // Previously caused crash: signal SIGABRT - malloc: double free for ptr @@ -149,8 +150,9 @@ final class UserConcurrencyTests: XCTestCase { executor.executeAddAliasesRequest(OSRequestAddAliases(aliases: aliases, identityModel: OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer())), inBackground: false) } - // 4. Run background threads - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Concurrent identity requests did not complete") { + client.completedRequestCount(ofType: OSRequestAddAliases.self) >= 100 + } /* Then */ // Previously caused crash: signal SIGABRT - malloc: double free for ptr @@ -189,8 +191,9 @@ final class UserConcurrencyTests: XCTestCase { executor.executeUpdatePropertiesRequest(OSRequestUpdateProperties(params: ["properties": ["language": UUID().uuidString], "refresh_device_metadata": false], identityModel: identityModel), inBackground: false) } - // 4. Run background threads - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Concurrent property requests did not complete") { + client.completedRequestCount(ofType: OSRequestUpdateProperties.self) >= 50 + } /* Then */ // No crash @@ -228,8 +231,10 @@ final class UserConcurrencyTests: XCTestCase { userExecutor.executeFetchUserRequest(fetchRequest) } - // Run background threads - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitUntil("Concurrent user requests did not complete") { + client.completedRequestCount(ofType: OSRequestIdentifyUser.self) >= 50 + && client.completedRequestCount(ofType: OSRequestFetchUser.self) >= 50 + } /* Then */ // No crash diff --git a/iOS_SDK/OneSignalSDK/build_kmp_xcframework.sh b/iOS_SDK/OneSignalSDK/build_kmp_xcframework.sh index c7e6da90b..3706c4df3 100755 --- a/iOS_SDK/OneSignalSDK/build_kmp_xcframework.sh +++ b/iOS_SDK/OneSignalSDK/build_kmp_xcframework.sh @@ -4,13 +4,22 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" KMP_REPO="$SCRIPT_DIR/../../OneSignal-KMP-SDK" +KMP_TASK="${1:-:kmp:verifyOneSignalKMPXCFramework}" if [[ ! -f "$KMP_REPO/gradlew" ]]; then echo "OneSignal-KMP-SDK is missing. Run: git submodule update --init --recursive" >&2 exit 1 fi +case "$KMP_TASK" in + :kmp:assembleOneSignalKMPReleaseXCFramework|:kmp:verifyOneSignalKMPXCFramework) ;; + *) + echo "Unsupported KMP XCFramework task: $KMP_TASK" >&2 + exit 1 + ;; +esac + "$KMP_REPO/gradlew" \ -p "$KMP_REPO" \ - :kmp:verifyOneSignalKMPXCFramework \ + "$KMP_TASK" \ --console=plain