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
)
}