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

633 lines
26 KiB
Swift

//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//
import SQLite
import XCTest
@testable import Amplify
@testable import AmplifyTestCommon
@testable import AWSDataStorePlugin
// swiftlint:disable file_length
// swiftlint:disable type_body_length
// TODO: Split these tests into separate suites
/// Tests in this class have a naming convention of `test_<existing>_<candidate>`, which is to say: given that the
/// mutation queue has an existing record of type `<existing>`, assert the behavior when candidate a mutation of
/// type `<candidate>`.
class MutationIngesterConflictResolutionTests: SyncEngineTestBase {
// MARK: - Existing == .create
/// - Given: An existing MutationEvent of type .create
/// - When:
/// - I submit a .create MutationEvent for the same object
/// - Then:
/// - I receive an error
/// - The mutation queue retains the original event
func test_create_create() async {
let post = Post(id: "post-1",
title: "title",
content: "content",
createdAt: .now())
await tryOrFail {
try setUpStorageAdapter(preCreating: [Post.self, Comment.self])
try saveMutationEvent(of: .create, for: post)
try setUpDataStore()
try await startAmplifyAndWaitForSync()
}
do {
_ = try await Amplify.DataStore.save(post)
XCTFail("Should have caught error")
} catch {
XCTAssertNotNil(error)
}
let mutationEventVerified = expectation(description: "Verified mutation event")
let predicate = MutationEvent.keys.id == SyncEngineTestBase.mutationEventId(for: post)
storageAdapter.query(MutationEvent.self,
predicate: predicate) { result in
switch result {
case .failure(let dataStoreError):
XCTAssertNil(dataStoreError)
case .success(let mutationEvents):
XCTAssertEqual(mutationEvents.count, 1)
XCTAssertEqual(mutationEvents.first?.json, try? post.toJSON())
}
mutationEventVerified.fulfill()
}
await waitForExpectations(timeout: 1)
}
/// - Given: An existing MutationEvent of type .create
/// - When:
/// - I submit a .update MutationEvent for the same object
/// - Then:
/// - The update is saved to DataStore
/// - The mutation event is updated with the new values
func test_create_update() async throws {
let post = Post(id: "post-1",
title: "title",
content: "content",
createdAt: .now())
await tryOrFail {
try setUpStorageAdapter(preCreating: [Post.self, Comment.self])
try saveMutationEvent(of: .create, for: post)
try savePost(post)
try setUpDataStore()
try await startAmplifyAndWaitForSync()
}
var mutatedPost = post
mutatedPost.content = "UPDATED CONTENT"
let savedPost = try await Amplify.DataStore.save(mutatedPost)
XCTAssertEqual(savedPost.content, mutatedPost.content)
let mutationEventVerified = expectation(description: "Verified mutation event")
let predicate = MutationEvent.keys.id == SyncEngineTestBase.mutationEventId(for: post)
storageAdapter.query(MutationEvent.self,
predicate: predicate) { result in
switch result {
case .failure(let dataStoreError):
XCTAssertNil(dataStoreError)
case .success(let mutationEvents):
guard let mutationEvent = mutationEvents.first else {
XCTFail("mutationEvents empty or nil")
return
}
XCTAssertEqual(mutationEvent.json, try? mutatedPost.toJSON())
XCTAssertEqual(mutationEvent.mutationType, GraphQLMutationType.create.rawValue)
}
mutationEventVerified.fulfill()
}
wait(for: [mutationEventVerified], timeout: 1.0)
}
/// - Given: An existing MutationEvent of type .create
/// - When:
/// - I submit a .delete MutationEvent for the same object
/// - Then:
/// - The delete is saved to DataStore
/// - The mutation event is removed from the mutation queue
func test_create_delete() async throws {
let post = Post(id: "post-1",
title: "title",
content: "content",
createdAt: .now())
await tryOrFail {
try setUpStorageAdapter(preCreating: [Post.self, Comment.self])
try saveMutationEvent(of: .create, for: post)
try savePost(post)
try setUpDataStore()
try await startAmplifyAndWaitForSync()
}
try await Amplify.DataStore.delete(post)
let mutationEventVerified = expectation(description: "Verified mutation event")
let predicate = MutationEvent.keys.id == SyncEngineTestBase.mutationEventId(for: post)
storageAdapter.query(MutationEvent.self,
predicate: predicate) { result in
switch result {
case .failure(let dataStoreError):
XCTAssertNil(dataStoreError)
case .success(let mutationEvents):
XCTAssertEqual(mutationEvents.count, 0)
}
mutationEventVerified.fulfill()
}
await waitForExpectations(timeout: 1.0)
}
// MARK: - Existing == .update
/// - Given: An existing MutationEvent of type .update
/// - When:
/// - I submit a .create MutationEvent for the same object
/// - Then:
/// - I receive an error
/// - The mutation queue retains the original event
func test_update_create() async {
let post = Post(id: "post-1",
title: "title",
content: "content",
createdAt: .now())
await tryOrFail {
try setUpStorageAdapter(preCreating: [Post.self, Comment.self])
try saveMutationEvent(of: .update, for: post)
try setUpDataStore()
try await startAmplifyAndWaitForSync()
}
do {
_ = try await Amplify.DataStore.save(post)
XCTFail("Should have caught error")
} catch {
XCTAssertNotNil(error)
}
let mutationEventVerified = expectation(description: "Verified mutation event")
let predicate = MutationEvent.keys.id == SyncEngineTestBase.mutationEventId(for: post)
storageAdapter.query(MutationEvent.self,
predicate: predicate) { result in
switch result {
case .failure(let dataStoreError):
XCTAssertNil(dataStoreError)
case .success(let mutationEvents):
XCTAssertEqual(mutationEvents.count, 1)
XCTAssertEqual(mutationEvents.first?.mutationType,
GraphQLMutationType.update.rawValue)
XCTAssertEqual(mutationEvents.first?.json, try? post.toJSON())
}
mutationEventVerified.fulfill()
}
wait(for: [mutationEventVerified], timeout: 1.0)
}
/// - Given: An existing MutationEvent of type .update
/// - When:
/// - I submit a .update MutationEvent for the same object
/// - Then:
/// - The update is saved to DataStore
/// - The mutation event is updated with the new values
func test_update_update() async throws {
let post = Post(id: "post-1",
title: "title",
content: "content",
createdAt: .now())
await tryOrFail {
try setUpStorageAdapter(preCreating: [Post.self, Comment.self])
try saveMutationEvent(of: .update, for: post)
try savePost(post)
try setUpDataStore()
try await startAmplifyAndWaitForSync()
}
var mutatedPost = post
mutatedPost.content = "UPDATED CONTENT"
let savedPost = try await Amplify.DataStore.save(mutatedPost)
XCTAssertEqual(savedPost.content, mutatedPost.content)
let mutationEventVerified = expectation(description: "Verified mutation event")
let predicate = MutationEvent.keys.id == SyncEngineTestBase.mutationEventId(for: post)
storageAdapter.query(MutationEvent.self,
predicate: predicate) { result in
switch result {
case .failure(let dataStoreError):
XCTAssertNil(dataStoreError)
case .success(let mutationEvents):
guard let mutationEvent = mutationEvents.first else {
XCTFail("mutationEvents empty or nil")
return
}
XCTAssertEqual(mutationEvent.json, try? mutatedPost.toJSON())
XCTAssertEqual(mutationEvent.mutationType, GraphQLMutationType.update.rawValue)
}
mutationEventVerified.fulfill()
}
wait(for: [mutationEventVerified], timeout: 1.0)
}
/// - Given: An existing MutationEvent of type .update
/// - When:
/// - I submit a .update MutationEvent for the same object
/// - Then:
/// - The delete is saved to DataStore
/// - The mutation event is updated to a .delete type
func test_update_delete() async throws {
let post = Post(id: "post-1",
title: "title",
content: "content",
createdAt: .now())
await tryOrFail {
try setUpStorageAdapter(preCreating: [Post.self, Comment.self])
try saveMutationEvent(of: .update, for: post)
try savePost(post)
try setUpDataStore()
try await startAmplifyAndWaitForSync()
}
try await Amplify.DataStore.delete(post)
let mutationEventVerified = expectation(description: "Verified mutation event")
let predicate = MutationEvent.keys.id == SyncEngineTestBase.mutationEventId(for: post)
storageAdapter.query(MutationEvent.self,
predicate: predicate) { result in
switch result {
case .failure(let dataStoreError):
XCTAssertNil(dataStoreError)
case .success(let mutationEvents):
guard let mutationEvent = mutationEvents.first else {
XCTFail("mutationEvents empty or nil")
return
}
XCTAssertEqual(mutationEvent.mutationType, GraphQLMutationType.delete.rawValue)
}
mutationEventVerified.fulfill()
}
wait(for: [mutationEventVerified], timeout: 1.0)
}
// MARK: - Existing == .delete
/// - Given: An existing MutationEvent of type .delete
/// - When:
/// - I submit a .create MutationEvent for the same object
/// - Then:
/// - I receive an error
/// - The mutation queue retains the original event
func test_delete_create() async throws {
let post = Post(id: "post-1",
title: "title",
content: "content",
createdAt: .now())
await tryOrFail {
try setUpStorageAdapter(preCreating: [Post.self, Comment.self])
try saveMutationEvent(of: .delete, for: post)
try setUpDataStore()
try await startAmplifyAndWaitForSync()
}
do {
_ = try await Amplify.DataStore.save(post)
XCTFail("Should have caught error")
} catch {
XCTAssertNotNil(error)
}
let mutationEventVerified = expectation(description: "Verified mutation event")
let predicate = MutationEvent.keys.id == SyncEngineTestBase.mutationEventId(for: post)
storageAdapter.query(MutationEvent.self,
predicate: predicate) { result in
switch result {
case .failure(let dataStoreError):
XCTAssertNil(dataStoreError)
case .success(let mutationEvents):
guard let mutationEvent = mutationEvents.first else {
XCTFail("mutationEvents empty or nil")
return
}
XCTAssertEqual(mutationEvent.mutationType, GraphQLMutationType.delete.rawValue)
}
mutationEventVerified.fulfill()
}
wait(for: [mutationEventVerified], timeout: 1.0)
}
// test_<existing>_<candidate>
/// - Given: An existing MutationEvent of type .delete
/// - When:
/// - I submit a .update MutationEvent for the same object
/// - Then:
/// - I receive an error
/// - The mutation queue retains the original event
func test_delete_update() async {
let post = Post(id: "post-1",
title: "title",
content: "content",
createdAt: .now())
await tryOrFail {
try setUpStorageAdapter(preCreating: [Post.self, Comment.self])
try saveMutationEvent(of: .delete, for: post)
try savePost(post)
try setUpDataStore()
try await startAmplifyAndWaitForSync()
}
var mutatedPost = post
mutatedPost.content = "UPDATED CONTENT"
do {
_ = try await Amplify.DataStore.save(mutatedPost)
XCTFail("Should have caught error")
} catch {
XCTAssertNotNil(error)
}
let mutationEventVerified = expectation(description: "Verified mutation event")
let predicate = MutationEvent.keys.id == SyncEngineTestBase.mutationEventId(for: post)
storageAdapter.query(MutationEvent.self,
predicate: predicate) { result in
switch result {
case .failure(let dataStoreError):
XCTAssertNil(dataStoreError)
case .success(let mutationEvents):
guard let mutationEvent = mutationEvents.first else {
XCTFail("mutationEvents empty or nil")
return
}
XCTAssertEqual(mutationEvent.mutationType, GraphQLMutationType.delete.rawValue)
}
mutationEventVerified.fulfill()
}
wait(for: [mutationEventVerified], timeout: 1.0)
}
// MARK: - Empty queue tests
/// - Given: An empty mutation queue
/// - When:
/// - I perform a .create mutation
/// - Then:
/// - The update is saved to DataStore
/// - The mutation event is appended to the queue
func testCreateMutationAppendedToEmptyQueue() async throws {
let post = Post(id: "post-1",
title: "title",
content: "content",
createdAt: .now())
await tryOrFail {
try setUpStorageAdapter(preCreating: [Post.self, Comment.self])
try setUpDataStore()
try await startAmplifyAndWaitForSync()
}
let savedPost = try await Amplify.DataStore.save(post)
XCTAssertNotNil(savedPost)
let mutationEventVerified = expectation(description: "Verified mutation event")
storageAdapter.query(MutationEvent.self, predicate: nil) { result in
switch result {
case .failure(let dataStoreError):
XCTAssertNil(dataStoreError)
case .success(let mutationEvents):
guard let mutationEvent = mutationEvents.first else {
XCTFail("mutationEvents empty or nil")
return
}
XCTAssertEqual(mutationEvent.json, try? post.toJSON())
XCTAssertEqual(mutationEvent.mutationType, GraphQLMutationType.create.rawValue)
}
mutationEventVerified.fulfill()
}
wait(for: [mutationEventVerified], timeout: 1.0)
}
/// - Given: An empty mutation queue
/// - When:
/// - I perform a .update mutation
/// - Then:
/// - The update is saved to DataStore
/// - The mutation event is appended to the queue
func testUpdateMutationAppendedToEmptyQueue() async throws {
let post = Post(id: "post-1",
title: "title",
content: "content",
createdAt: .now())
await tryOrFail {
try setUpStorageAdapter(preCreating: [Post.self, Comment.self])
try savePost(post)
try setUpDataStore()
try await startAmplifyAndWaitForSync()
}
let savedPost = try await Amplify.DataStore.save(post)
XCTAssertNotNil(savedPost)
let mutationEventVerified = expectation(description: "Verified mutation event")
storageAdapter.query(MutationEvent.self, predicate: nil) { result in
switch result {
case .failure(let dataStoreError):
XCTAssertNil(dataStoreError)
case .success(let mutationEvents):
guard let mutationEvent = mutationEvents.first else {
XCTFail("mutationEvents empty or nil")
return
}
XCTAssertEqual(mutationEvent.json, try? post.toJSON())
XCTAssertEqual(mutationEvent.mutationType, GraphQLMutationType.update.rawValue)
}
mutationEventVerified.fulfill()
}
wait(for: [mutationEventVerified], timeout: 1.0)
}
/// - Given: An empty mutation queue
/// - When:
/// - I perform a .delete mutation
/// - Then:
/// - The update is saved to DataStore
/// - The mutation event is appended to the queue
func testDeleteMutationAppendedToEmptyQueue() async throws {
let post = Post(id: "post-1",
title: "title",
content: "content",
createdAt: .now())
await tryOrFail {
try setUpStorageAdapter(preCreating: [Post.self, Comment.self])
try savePost(post)
try setUpDataStore()
try await startAmplifyAndWaitForSync()
}
try await Amplify.DataStore.delete(post)
let mutationEventVerified = expectation(description: "Verified mutation event")
storageAdapter.query(MutationEvent.self, predicate: nil) { result in
switch result {
case .failure(let dataStoreError):
XCTAssertNil(dataStoreError)
case .success(let mutationEvents):
guard let mutationEvent = mutationEvents.first else {
XCTFail("mutationEvents empty or nil")
return
}
XCTAssertEqual(mutationEvent.modelId, post.id)
XCTAssertEqual(mutationEvent.mutationType, GraphQLMutationType.delete.rawValue)
}
mutationEventVerified.fulfill()
}
wait(for: [mutationEventVerified], timeout: 1.0)
}
// MARK: - In-process queue tests
/// - Given: A mutation queue with an in-process .create event
/// - When:
/// - I perform a .create mutation
/// - Then:
/// - The update is saved to DataStore
/// - The mutation event is appended to the queue, even though it would normally have thrown an error
func testCreateMutationAppendedToInProcessQueue() async throws {
let post = Post(id: "post-1",
title: "title",
content: "content",
createdAt: .now())
await tryOrFail {
try setUpStorageAdapter(preCreating: [Post.self, Comment.self])
try setUpDataStore()
try await startAmplifyAndWaitForSync()
try saveMutationEvent(of: .create, for: post, inProcess: true)
}
let savedPost = try await Amplify.DataStore.save(post)
XCTAssertNotNil(savedPost)
let mutationEventVerified = expectation(description: "Verified mutation event")
storageAdapter.query(MutationEvent.self, predicate: nil) { result in
switch result {
case .failure(let dataStoreError):
XCTAssertNil(dataStoreError)
case .success(let mutationEvents):
XCTAssertEqual(mutationEvents.count, 2)
XCTAssertEqual(mutationEvents[0].mutationType, GraphQLMutationType.create.rawValue)
XCTAssertEqual(mutationEvents[1].mutationType, GraphQLMutationType.create.rawValue)
}
mutationEventVerified.fulfill()
}
await waitForExpectations(timeout: 1.0)
}
/// - Given: A mutation queue with an in-process .create event
/// - When:
/// - I perform a .update mutation
/// - Then:
/// - The update is saved to DataStore
/// - The mutation event is appended to the queue, even though it would normally have overwritten the existing
/// create
func testUpdateMutationAppendedToInProcessQueue() async throws {
let post = Post(id: "post-1",
title: "title",
content: "content",
createdAt: .now())
await tryOrFail {
try setUpStorageAdapter(preCreating: [Post.self, Comment.self])
try setUpDataStore()
try await startAmplifyAndWaitForSync()
try savePost(post)
try saveMutationEvent(of: .create, for: post, inProcess: true)
}
var mutatedPost = post
mutatedPost.content = "UPDATED CONTENT"
let savedPost = try await Amplify.DataStore.save(mutatedPost)
XCTAssertEqual(savedPost.content, mutatedPost.content)
let mutationEventVerified = expectation(description: "Verified mutation event")
storageAdapter.query(MutationEvent.self, predicate: nil) { result in
switch result {
case .failure(let dataStoreError):
XCTAssertNil(dataStoreError)
case .success(let mutationEvents):
XCTAssertEqual(mutationEvents.count, 2)
XCTAssertEqual(mutationEvents[0].mutationType, GraphQLMutationType.create.rawValue)
XCTAssertEqual(mutationEvents[0].json, try? post.toJSON())
XCTAssertEqual(mutationEvents[1].mutationType, GraphQLMutationType.update.rawValue)
XCTAssertEqual(mutationEvents[1].json, try? mutatedPost.toJSON())
}
mutationEventVerified.fulfill()
}
await waitForExpectations(timeout: 1.0)
}
/// - Given: A mutation queue with an in-process .create event
/// - When:
/// - I perform a .delete mutation
/// - Then:
/// - The update is saved to DataStore
/// - The mutation event is appended to the queue, even though it would normally have thrown an error
func testDeleteMutationAppendedToInProcessQueue() async throws {
let post = Post(id: "post-1",
title: "title",
content: "content",
createdAt: .now())
await tryOrFail {
try setUpStorageAdapter(preCreating: [Post.self, Comment.self])
try setUpDataStore()
try await startAmplifyAndWaitForSync()
try savePost(post)
try saveMutationEvent(of: .create, for: post, inProcess: true)
}
try await Amplify.DataStore.delete(post)
let mutationEventVerified = expectation(description: "Verified mutation event")
storageAdapter.query(MutationEvent.self, predicate: nil) { result in
switch result {
case .failure(let dataStoreError):
XCTAssertNil(dataStoreError)
case .success(let mutationEvents):
XCTAssertEqual(mutationEvents.count, 2)
XCTAssertEqual(mutationEvents[0].mutationType, GraphQLMutationType.create.rawValue)
XCTAssertEqual(mutationEvents[1].mutationType, GraphQLMutationType.delete.rawValue)
}
mutationEventVerified.fulfill()
}
await waitForExpectations(timeout: 1.0)
}
}