func testSyncOperationAtSetupAndReconnect()

in AWSAppSyncIntegrationTests/AWSAppSyncAPIKeyAuthTests.swift [251:610]


    func testSyncOperationAtSetupAndReconnect() throws {
        // Let result handlers inspect the current phase of the sync watcher's "lifecycle" so they can properly fulfill
        // expectations
        var _currentSyncWatcherLifecyclePhase = SyncWatcherLifecyclePhase.baseQueryNotYetComplete
        func currentSyncWatcherLifecyclePhase() -> SyncWatcherLifecyclePhase {
            return _currentSyncWatcherLifecyclePhase
        }

        // This tests needs a physical DB for the SubscriptionMetadataCache to properly return a "lastSynced" value.
        let rootDirectory = FileManager.default.temporaryDirectory.appendingPathComponent("testSyncOperationAtSetupAndReconnect")
        try? FileManager.default.removeItem(at: rootDirectory)
        let cacheConfiguration = try AWSAppSyncCacheConfiguration(withRootDirectory: rootDirectory)

        let appSyncClient = try AWSAppSyncAPIKeyAuthTests.makeAppSyncClient(
            authType: self.authType,
            cacheConfiguration: cacheConfiguration
        )

        var syncWatcher: Cancellable?
        defer {
            syncWatcher?.cancel()
        }

        let baseRefreshIntervalInSeconds = 86_400

        let postCreated = expectation(description: "Post created successfully.")
        let addPost = DefaultTestPostData.defaultCreatePostWithoutFileUsingParametersMutation

        var idHolder: GraphQLID?
        appSyncClient.perform(mutation: addPost, queue: AWSAppSyncAPIKeyAuthTests.mutationQueue, resultHandler:  { result, error in
            print("CreatePost result handler invoked")
            XCTAssertNil(error)
            XCTAssertNotNil(result?.data?.createPostWithoutFileUsingParameters?.id)
            XCTAssertEqual(result!.data!.createPostWithoutFileUsingParameters?.author, DefaultTestPostData.author)
            idHolder = result?.data?.createPostWithoutFileUsingParameters?.id
            postCreated.fulfill()
        })
        wait(for: [postCreated], timeout: AWSAppSyncAPIKeyAuthTests.networkOperationTimeout)

        guard let id = idHolder else {
            XCTFail("Expected ID from addPost mutation")
            return
        }

        // Set up the expectations for the initial connection (simulates the first time the app was launched)
        let initialBaseQueryHandlerShouldBeInvokedToHydrateFromCache =
            expectation(description: "Initial base query result handler should be invoked to hydrate subscription from cache")

        let initialBaseQueryShouldBeInvokedToPopulateFromService =
            expectation(description: "Initial base query result handler should be invoked to populate subscription from service")

        let initialBaseQueryShouldNotBeInvokedAgainAfterCompleted =
            expectation(description: "Initial base query result handler should not be invoked after it has successfully completed")
        initialBaseQueryShouldNotBeInvokedAgainAfterCompleted.isInverted = true

        let initialBaseQueryShouldNotBeInvokedDuringMonitoring =
            expectation(description: "Initial base query result handler should not be invoked during monitoring")
        initialBaseQueryShouldNotBeInvokedDuringMonitoring.isInverted = true

        let initialSubscriptionHandlerShouldNotBeInvokedDuringSetup =
            expectation(description: "Initial subscription query result handler should not be invoked during setup")
        initialSubscriptionHandlerShouldNotBeInvokedDuringSetup.isInverted = true

        let initialSubscriptionHandlerShouldBeInvokedDuringMonitoring =
            expectation(description: "Initial subscription query result handler should be invoked during monitoring")

        let initialDeltaHandlerShouldNotBeInvokedDuringSetup =
            expectation(description: "Initial delta query result handler should not be invoked during setup")
        initialDeltaHandlerShouldNotBeInvokedDuringSetup.isInverted = true

        let initialDeltaHandlerShouldNotBeInvokedDuringMonitoring =
            expectation(description: "Initial delta query result handler should not be invoked during monitoring")
        initialDeltaHandlerShouldNotBeInvokedDuringMonitoring.isInverted = true

        let initialBaseQueryResultHandler: (GraphQLResult<ListPostsQuery.Data>?, Error?) -> Void = {
            result, error in
            print("Initial base query result handler invoked during phase \(currentSyncWatcherLifecyclePhase())")
            XCTAssertNil(error)

            switch currentSyncWatcherLifecyclePhase() {
            case .baseQueryNotYetComplete:
                switch result?.source {
                case .none:
                    // We get a .none source hydrating from an empty cache
                    initialBaseQueryHandlerShouldBeInvokedToHydrateFromCache.fulfill()
                    XCTAssertNil(result)
                case .some(let source):
                    switch source {
                    case .cache:
                        // This would be unexpected for an empty cache, but we will include the case here in case
                        // we change that behavior in future
                        initialBaseQueryHandlerShouldBeInvokedToHydrateFromCache.fulfill()
                        XCTAssertNotNil(result)
                    case .server:
                        initialBaseQueryShouldBeInvokedToPopulateFromService.fulfill()
                        _currentSyncWatcherLifecyclePhase = .baseQueryComplete
                        XCTAssertNotNil(result)
                    }
                }
            case .baseQueryComplete:
                initialBaseQueryShouldNotBeInvokedAgainAfterCompleted.fulfill()
            case .monitoringSubscriptions:
                initialBaseQueryShouldNotBeInvokedDuringMonitoring.fulfill()
            }
        }

        let initialSubscriptionResultHandler: (GraphQLResult<OnUpvotePostSubscription.Data>?, ApolloStore.ReadWriteTransaction?, Error?) -> Void = {
            result, transaction, error in
            print("Initial subscription result handler invoked")
            XCTAssertNotNil(result)
            XCTAssertNotNil(transaction)
            XCTAssertNil(error)
            switch currentSyncWatcherLifecyclePhase() {
            case .baseQueryNotYetComplete:
                initialSubscriptionHandlerShouldNotBeInvokedDuringSetup.fulfill()
            case .baseQueryComplete:
                initialSubscriptionHandlerShouldNotBeInvokedDuringSetup.fulfill()
            case .monitoringSubscriptions:
                initialSubscriptionHandlerShouldBeInvokedDuringMonitoring.fulfill()
            }
        }

        let initialDeltaQueryResultHandler: (GraphQLResult<ListPostsQuery.Data>?, ApolloStore.ReadWriteTransaction?, Error?) -> Void = {
            _, _, _ in
            print("Initial delta query result handler invoked unexpectedly")
            switch currentSyncWatcherLifecyclePhase() {
            case .baseQueryNotYetComplete:
                initialDeltaHandlerShouldNotBeInvokedDuringSetup.fulfill()
            case .baseQueryComplete:
                // This case is allowable--if the test environment fails and retries the base query (e.g., due to a
                // transient network error), the delta query would be invoked. We will leave the
                // "baseQueryNotYetComplete" case above though, since we want to ensure that the delta query always
                // returns after the base query
                break
            case .monitoringSubscriptions:
                initialDeltaHandlerShouldNotBeInvokedDuringMonitoring.fulfill()
            }
        }

        // Refresh interval defaults to one day, but we'll make it explicit here in case that changes in the future
        let syncConfiguration = SyncConfiguration(baseRefreshIntervalInSeconds: baseRefreshIntervalInSeconds)

        let listPostsQuery = ListPostsQuery()
        let subscription = OnUpvotePostSubscription(id: id)

        syncWatcher = appSyncClient.sync(
            baseQuery: listPostsQuery,
            baseQueryResultHandler: initialBaseQueryResultHandler,
            subscription: subscription,
            subscriptionResultHandler: initialSubscriptionResultHandler,
            deltaQuery: listPostsQuery,
            deltaQueryResultHandler: initialDeltaQueryResultHandler,
            syncConfiguration: syncConfiguration
        )

        XCTAssertNotNil(syncWatcher, "Initial subscription sync watcher expected to be non nil.")

        // Wait to ensure the new watcher is properly initialized. We don't expect the delta query expectation to be fulfilled
        // during either initialization or subsequent subscription. However, we're only allowed to `wait` on an expectation one
        // time, so we'll wait on the "setup" expectation here, and the "monitoring" expectation below
        wait(
            for: [
                initialBaseQueryHandlerShouldBeInvokedToHydrateFromCache,
                initialBaseQueryShouldBeInvokedToPopulateFromService
            ],
            timeout: AWSAppSyncAPIKeyAuthTests.networkOperationTimeout
        )

        // We aren't going to sit and wait for the other handlers *not* to be invoked. As long as they aren't invoked
        // before the initial base queries are done, we're comfortable that the correct order of operations is being
        // preserved. Use this to simply assert that they weren't invoked while waiting for the initial setup to
        // complete
        wait(
            for: [
                initialBaseQueryShouldNotBeInvokedAgainAfterCompleted,
                initialSubscriptionHandlerShouldNotBeInvokedDuringSetup,
                initialDeltaHandlerShouldNotBeInvokedDuringSetup
            ],
            timeout: 0.1
        )

        // Now that we've subscribed, mutate the post to trigger the subscription
        _currentSyncWatcherLifecyclePhase = .monitoringSubscriptions
        let firstUpvoteMutation = UpvotePostMutation(id: id)
        let firstUpvoteComplete = expectation(description: "First upvote should be completed on service")

        // Wait 3 seconds to ensure sync/subscription is active, then trigger the mutation
        DispatchQueue.global().async {
            sleep(3)
            self.appSyncClient?.perform(mutation: firstUpvoteMutation,
                                        queue: AWSAppSyncAPIKeyAuthTests.mutationQueue, resultHandler:  { result, error in
                                            print("Received first upvote mutation response")
                                            XCTAssertNil(error)
                                            XCTAssertNotNil(result?.data?.upvotePost?.id)
                                            firstUpvoteComplete.fulfill()
                                        })
        }

        wait(
            for: [
                firstUpvoteComplete,
                initialSubscriptionHandlerShouldBeInvokedDuringMonitoring,
            ],
            timeout: AWSAppSyncAPIKeyAuthTests.networkOperationTimeout
        )
        wait(
            for: [
                initialBaseQueryShouldNotBeInvokedDuringMonitoring,
                initialDeltaHandlerShouldNotBeInvokedDuringMonitoring
            ],
            timeout: 0.1
        )

        // Cancel the syncWatcher to simulate an app restart
        syncWatcher?.cancel()
        _currentSyncWatcherLifecyclePhase = .baseQueryNotYetComplete

        // Now set up the expectations for the "restarted" app
        let restartedBaseQueryHandlerShouldBeInvokedToHydrateFromCache =
            expectation(description: "Restarted base query result handler should be invoked to hydrate subscription from cache")

        let restartedBaseQueryShouldNotBeInvokedToPopulateFromService =
            expectation(description: "Restarted base query result handler should not be invoked to populate subscription from service since it is within deltaSync refresh time")
        restartedBaseQueryShouldNotBeInvokedToPopulateFromService.isInverted = true

        let restartedBaseQueryShouldNotBeInvokedDuringMonitoring =
            expectation(description: "Restarted base query result handler should not be invoked during monitoring")
        restartedBaseQueryShouldNotBeInvokedDuringMonitoring.isInverted = true

        let restartedSubscriptionHandlerShouldNotBeInvokedDuringSetup =
            expectation(description: "Restarted subscription query result handler should not be invoked during setup")
        restartedSubscriptionHandlerShouldNotBeInvokedDuringSetup.isInverted = true

        let restartedSubscriptionHandlerShouldBeInvokedDuringMonitoring =
            expectation(description: "Restarted subscription query result handler should be invoked during monitoring")

        let restartedDeltaHandlerShouldBeInvokedDuringSetup =
            expectation(description: "Restarted delta query result handler should be invoked during setup")

        let restartedDeltaHandlerShouldNotBeInvokedDuringMonitoring =
            expectation(description: "Restarted delta query result handler should not be invoked during monitoring")
        restartedDeltaHandlerShouldNotBeInvokedDuringMonitoring.isInverted = true

        let restartedBaseQueryResultHandler: (GraphQLResult<ListPostsQuery.Data>?, Error?) -> Void = {
            result, error in
            print("Restarted base query result handler invoked")
            guard let result = result else {
                XCTFail("result unexpectedly nil in restartedBaseQueryResultHandler")
                return
            }
            XCTAssertNil(error)

            switch currentSyncWatcherLifecyclePhase() {
            case .baseQueryNotYetComplete:
                switch result.source {
                case .cache:
                    restartedBaseQueryHandlerShouldBeInvokedToHydrateFromCache.fulfill()
                case .server:
                    restartedBaseQueryShouldNotBeInvokedToPopulateFromService.fulfill()
                }
            case .baseQueryComplete:
                restartedBaseQueryShouldNotBeInvokedToPopulateFromService.fulfill()
            case .monitoringSubscriptions:
                restartedBaseQueryShouldNotBeInvokedDuringMonitoring.fulfill()
            }
        }

        let restartedSubscriptionResultHandler: (GraphQLResult<OnUpvotePostSubscription.Data>?, ApolloStore.ReadWriteTransaction?, Error?) -> Void = {
            result, transaction, error in
            print("Restarted subscription result handler invoked")
            XCTAssertNotNil(result)
            XCTAssertNotNil(transaction)
            XCTAssertNil(error)
            switch currentSyncWatcherLifecyclePhase() {
            case .baseQueryNotYetComplete:
                restartedSubscriptionHandlerShouldNotBeInvokedDuringSetup.fulfill()
            case .baseQueryComplete:
                restartedSubscriptionHandlerShouldNotBeInvokedDuringSetup.fulfill()
            case .monitoringSubscriptions:
                restartedSubscriptionHandlerShouldBeInvokedDuringMonitoring.fulfill()
            }
        }

        let restartedDeltaQueryResultHandler: (GraphQLResult<ListPostsQuery.Data>?, ApolloStore.ReadWriteTransaction?, Error?) -> Void = {
            result, transaction, error in
            print("Restarted delta query result handler invoked")
            XCTAssertNotNil(result)
            XCTAssertNotNil(transaction)
            XCTAssertNil(error)
            switch currentSyncWatcherLifecyclePhase() {
            case .baseQueryNotYetComplete:
                restartedDeltaHandlerShouldBeInvokedDuringSetup.fulfill()
            case .baseQueryComplete:
                restartedDeltaHandlerShouldBeInvokedDuringSetup.fulfill()
            case .monitoringSubscriptions:
                restartedDeltaHandlerShouldNotBeInvokedDuringMonitoring.fulfill()
            }
        }

        syncWatcher = appSyncClient.sync(
            baseQuery: listPostsQuery,
            baseQueryResultHandler: restartedBaseQueryResultHandler,
            subscription: subscription,
            subscriptionResultHandler: restartedSubscriptionResultHandler,
            deltaQuery: listPostsQuery,
            deltaQueryResultHandler: restartedDeltaQueryResultHandler,
            syncConfiguration: syncConfiguration
        )

        XCTAssertNotNil(syncWatcher, "Restart sync watcher expected to be non nil")

        // Wait to ensure the new watcher is properly initialized
        wait(
            for: [
                restartedBaseQueryHandlerShouldBeInvokedToHydrateFromCache,
                restartedDeltaHandlerShouldBeInvokedDuringSetup
            ],
            timeout: AWSAppSyncAPIKeyAuthTests.networkOperationTimeout
        )
        wait(
            for: [
                restartedBaseQueryShouldNotBeInvokedToPopulateFromService,
                restartedSubscriptionHandlerShouldNotBeInvokedDuringSetup
            ],
            timeout: 0.1
        )

        // Trigger the restarted watcher's subscription
        _currentSyncWatcherLifecyclePhase = .monitoringSubscriptions
        let secondUpvoteMutation = UpvotePostMutation(id: id)
        let secondUpvoteComplete = expectation(description: "Second upvote should be completed on service")

        // Wait 3 seconds to ensure sync/subscription is active, then trigger the mutation
        DispatchQueue.global().async {
            sleep(3)
            self.appSyncClient?.perform(mutation: secondUpvoteMutation,
                                        queue: AWSAppSyncAPIKeyAuthTests.mutationQueue, resultHandler:  { result, error in
                                            print("Received second upvote mutation response")
                                            XCTAssertNil(error)
                                            XCTAssertNotNil(result?.data?.upvotePost?.id)
                                            secondUpvoteComplete.fulfill()
                                        })
        }

        wait(
            for: [
                secondUpvoteComplete,
                restartedSubscriptionHandlerShouldBeInvokedDuringMonitoring
            ],
            timeout: AWSAppSyncAPIKeyAuthTests.networkOperationTimeout
        )
        wait(
            for: [
                restartedBaseQueryShouldNotBeInvokedDuringMonitoring,
                restartedDeltaHandlerShouldNotBeInvokedDuringMonitoring
            ],
            timeout: 0.1
        )

    }