amplify-swift/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/SyncMutationToCloudOperatio...

277 lines
14 KiB
Swift

//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//
import Foundation
import XCTest
import Combine
@testable import Amplify
@testable import AmplifyTestCommon
@testable import AWSPluginsCore
@testable import AWSDataStorePlugin
class SyncMutationToCloudOperationTests: XCTestCase {
let defaultAsyncWaitTimeout = 2.0
let secondsInADay = 60 * 60 * 24
var mockAPIPlugin: MockAPICategoryPlugin!
var reachabilityPublisher: CurrentValueSubject<ReachabilityUpdate, Never>!
var publisher: AnyPublisher<ReachabilityUpdate, Never> {
return reachabilityPublisher.eraseToAnyPublisher()
}
override func setUp() async throws {
reachabilityPublisher = CurrentValueSubject<ReachabilityUpdate, Never>(ReachabilityUpdate(isOnline: false))
await tryOrFail {
try await setUpWithAPI()
}
ModelRegistry.register(modelType: Post.self)
ModelRegistry.register(modelType: Comment.self)
}
func testRetryOnTimeoutOfWaiting() async throws {
let expectMutationRequestCompletion = expectation(description: "Expect to complete mutation request")
let expectFirstCallToAPIMutate = expectation(description: "First call to API.mutate")
let expectSecondCallToAPIMutate = expectation(description: "Second call to API.mutate")
let post1 = Post(title: "post1", content: "content1", createdAt: .now())
let mutationEvent = try MutationEvent(model: post1, modelSchema: post1.schema, mutationType: .create)
var listenerFromFirstRequestOptional: GraphQLOperation<MutationSync<AnyModel>>.ResultListener?
var listenerFromSecondRequestOptional: GraphQLOperation<MutationSync<AnyModel>>.ResultListener?
var numberOfTimesEntered = 0
let responder = MutateRequestListenerResponder<MutationSync<AnyModel>> { _, eventListener in
if numberOfTimesEntered == 0 {
listenerFromFirstRequestOptional = eventListener
expectFirstCallToAPIMutate.fulfill()
} else if numberOfTimesEntered == 1 {
listenerFromSecondRequestOptional = eventListener
expectSecondCallToAPIMutate.fulfill()
} else {
XCTFail("This should not be called more than once")
}
numberOfTimesEntered += 1
// We could return an operation here, but we don't need to.
// The main reason for having this responder is to get the eventListener.
// the eventListener block will execute the the call to validateResponseFromCloud
return nil
}
mockAPIPlugin.responders[.mutateRequestListener] = responder
let completion: GraphQLOperation<MutationSync<AnyModel>>.ResultListener = { _ in
expectMutationRequestCompletion.fulfill()
}
let operation = await SyncMutationToCloudOperation(mutationEvent: mutationEvent,
api: mockAPIPlugin,
authModeStrategy: AWSDefaultAuthModeStrategy(),
networkReachabilityPublisher: publisher,
currentAttemptNumber: 1,
completion: completion)
let queue = OperationQueue()
queue.addOperation(operation)
wait(for: [expectFirstCallToAPIMutate], timeout: defaultAsyncWaitTimeout)
guard let listenerFromFirstRequest = listenerFromFirstRequestOptional else {
XCTFail("Listener was not called through MockAPICategoryPlugin")
return
}
let urlError = URLError(URLError.notConnectedToInternet)
listenerFromFirstRequest(.failure(APIError.networkError("mock NotConnectedToInternetError", nil, urlError)))
wait(for: [expectSecondCallToAPIMutate], timeout: defaultAsyncWaitTimeout)
guard let listenerFromSecondRequest = listenerFromSecondRequestOptional else {
XCTFail("Listener was not called through MockAPICategoryPlugin")
return
}
let model = MockSynced(id: "id-1")
let anyModel = try model.eraseToAnyModel()
let remoteSyncMetadata = MutationSyncMetadata(modelId: model.id,
modelName: model.modelName,
deleted: false,
lastChangedAt: Date().unixSeconds,
version: 2)
let remoteMutationSync = MutationSync(model: anyModel, syncMetadata: remoteSyncMetadata)
listenerFromSecondRequest(.success(.success(remoteMutationSync)))
// waitForExpectations(timeout: 1)
wait(for: [expectMutationRequestCompletion], timeout: defaultAsyncWaitTimeout)
}
func testRetryOnChangeReachability() async throws {
let mockRequestRetryPolicy = MockRequestRetryablePolicy()
let waitForeverToRetry = RequestRetryAdvice(shouldRetry: true, retryInterval: .seconds(secondsInADay))
mockRequestRetryPolicy.pushOnRetryRequestAdvice(response: waitForeverToRetry)
let expectMutationRequestCompletion = expectation(description: "Expect to complete mutation request")
let expectFirstCallToAPIMutate = expectation(description: "First call to API.mutate")
let expectSecondCallToAPIMutate = expectation(description: "Second call to API.mutate")
let post1 = Post(title: "post1", content: "content1", createdAt: .now())
let mutationEvent = try MutationEvent(model: post1, modelSchema: post1.schema, mutationType: .create)
var listenerFromFirstRequestOptional: GraphQLOperation<MutationSync<AnyModel>>.ResultListener?
var listenerFromSecondRequestOptional: GraphQLOperation<MutationSync<AnyModel>>.ResultListener?
var numberOfTimesEntered = 0
let responder = MutateRequestListenerResponder<MutationSync<AnyModel>> { _, eventListener in
if numberOfTimesEntered == 0 {
listenerFromFirstRequestOptional = eventListener
expectFirstCallToAPIMutate.fulfill()
} else if numberOfTimesEntered == 1 {
listenerFromSecondRequestOptional = eventListener
expectSecondCallToAPIMutate.fulfill()
} else {
XCTFail("This should not be called more than once")
}
numberOfTimesEntered += 1
// We could return an operation here, but we don't need to.
// The main reason for having this responder is to get the eventListener.
// the eventListener block will execute the the call to validateResponseFromCloud
return nil
}
mockAPIPlugin.responders[.mutateRequestListener] = responder
let completion: GraphQLOperation<MutationSync<AnyModel>>.ResultListener = { _ in
expectMutationRequestCompletion.fulfill()
}
let operation = await SyncMutationToCloudOperation(mutationEvent: mutationEvent,
api: mockAPIPlugin,
authModeStrategy: AWSDefaultAuthModeStrategy(),
networkReachabilityPublisher: publisher,
currentAttemptNumber: 1,
requestRetryablePolicy: mockRequestRetryPolicy,
completion: completion)
let queue = OperationQueue()
queue.addOperation(operation)
wait(for: [expectFirstCallToAPIMutate], timeout: defaultAsyncWaitTimeout)
guard let listenerFromFirstRequest = listenerFromFirstRequestOptional else {
XCTFail("Listener was not called through MockAPICategoryPlugin")
return
}
let urlError = URLError(URLError.notConnectedToInternet)
listenerFromFirstRequest(.failure(APIError.networkError("mock NotConnectedToInternetError", nil, urlError)))
reachabilityPublisher.send(ReachabilityUpdate(isOnline: true))
wait(for: [expectSecondCallToAPIMutate], timeout: defaultAsyncWaitTimeout)
guard let listenerFromSecondRequest = listenerFromSecondRequestOptional else {
XCTFail("Listener was not called through MockAPICategoryPlugin")
return
}
let model = MockSynced(id: "id-1")
let anyModel = try model.eraseToAnyModel()
let remoteSyncMetadata = MutationSyncMetadata(modelId: model.id,
modelName: model.modelName,
deleted: false,
lastChangedAt: Date().unixSeconds,
version: 2)
let remoteMutationSync = MutationSync(model: anyModel, syncMetadata: remoteSyncMetadata)
listenerFromSecondRequest(.success(.success(remoteMutationSync)))
wait(for: [expectMutationRequestCompletion], timeout: defaultAsyncWaitTimeout)
}
func testAbilityToCancel() async throws {
let mockRequestRetryPolicy = MockRequestRetryablePolicy()
let waitForeverToRetry = RequestRetryAdvice(shouldRetry: true, retryInterval: .seconds(secondsInADay))
mockRequestRetryPolicy.pushOnRetryRequestAdvice(response: waitForeverToRetry)
let expectMutationRequestFailed = expectation(description: "Expect to fail mutation request")
let expectFirstCallToAPIMutate = expectation(description: "First call to API.mutate")
let post1 = Post(title: "post1", content: "content1", createdAt: .now())
let mutationEvent = try MutationEvent(model: post1, modelSchema: post1.schema, mutationType: .create)
var listenerFromFirstRequestOptional: GraphQLOperation<MutationSync<AnyModel>>.ResultListener?
var numberOfTimesEntered = 0
let responder = MutateRequestListenerResponder<MutationSync<AnyModel>> { _, eventListener in
if numberOfTimesEntered == 0 {
listenerFromFirstRequestOptional = eventListener
expectFirstCallToAPIMutate.fulfill()
} else {
XCTFail("This should not be called more than once")
}
numberOfTimesEntered += 1
// We could return an operation here, but we don't need to.
// The main reason for having this responder is to get the eventListener.
// the eventListener block will execute the the call to validateResponseFromCloud
return nil
}
mockAPIPlugin.responders[.mutateRequestListener] = responder
let completion: GraphQLOperation<MutationSync<AnyModel>>.ResultListener = { asyncEvent in
switch asyncEvent {
case .failure:
expectMutationRequestFailed.fulfill()
default:
break
}
}
let operation = await SyncMutationToCloudOperation(mutationEvent: mutationEvent,
api: mockAPIPlugin,
authModeStrategy: AWSDefaultAuthModeStrategy(),
networkReachabilityPublisher: publisher,
currentAttemptNumber: 1,
requestRetryablePolicy: mockRequestRetryPolicy,
completion: completion)
let queue = OperationQueue()
queue.addOperation(operation)
wait(for: [expectFirstCallToAPIMutate], timeout: defaultAsyncWaitTimeout)
guard let listenerFromFirstRequest = listenerFromFirstRequestOptional else {
XCTFail("Listener was not called through MockAPICategoryPlugin")
return
}
let urlError = URLError(URLError.notConnectedToInternet)
listenerFromFirstRequest(.failure(APIError.networkError("mock NotConnectedToInternetError", nil, urlError)))
// At this point, we will be "waiting forever" to retry our request or until the operation is canceled
operation.cancel()
wait(for: [expectMutationRequestFailed], timeout: defaultAsyncWaitTimeout)
}
}
extension SyncMutationToCloudOperationTests {
private func setUpCore() async throws -> AmplifyConfiguration {
await Amplify.reset()
let dataStorePublisher = DataStorePublisher()
let dataStorePlugin = AWSDataStorePlugin(modelRegistration: TestModelRegistration(),
storageEngineBehaviorFactory: MockStorageEngineBehavior.mockStorageEngineBehaviorFactory,
dataStorePublisher: dataStorePublisher,
validAPIPluginKey: "MockAPICategoryPlugin",
validAuthPluginKey: "MockAuthCategoryPlugin")
try Amplify.add(plugin: dataStorePlugin)
let dataStoreConfig = DataStoreCategoryConfiguration(plugins: [
"awsDataStorePlugin": true
])
let amplifyConfig = AmplifyConfiguration(dataStore: dataStoreConfig)
return amplifyConfig
}
private func setUpAPICategory(config: AmplifyConfiguration) throws -> AmplifyConfiguration {
mockAPIPlugin = MockAPICategoryPlugin()
try Amplify.add(plugin: mockAPIPlugin)
let apiConfig = APICategoryConfiguration(plugins: [
"MockAPICategoryPlugin": true
])
let amplifyConfig = AmplifyConfiguration(api: apiConfig, dataStore: config.dataStore)
return amplifyConfig
}
private func setUpWithAPI() async throws {
let configWithoutAPI = try await setUpCore()
let configWithAPI = try setUpAPICategory(config: configWithoutAPI)
try Amplify.configure(configWithAPI)
}
}