amplify-swift/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Storage/CascadeDeleteOperationTests...

808 lines
39 KiB
Swift

//
// Copyright Amazon.com Inc. or its affiliates.
// All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//
import XCTest
import SQLite
import Combine
@testable import Amplify
@testable import AmplifyTestCommon
@testable import AWSDataStorePlugin
// swiftlint:disable type_body_length
class CascadeDeleteOperationTests: StorageEngineTestsBase {
override func setUp() {
super.setUp()
Amplify.Logging.logLevel = .warn
let validAPIPluginKey = "MockAPICategoryPlugin"
let validAuthPluginKey = "MockAuthCategoryPlugin"
do {
connection = try Connection(.inMemory)
storageAdapter = try SQLiteStorageEngineAdapter(connection: connection)
try storageAdapter.setUp(modelSchemas: StorageEngine.systemModelSchemas)
syncEngine = MockRemoteSyncEngine()
storageEngine = StorageEngine(storageAdapter: storageAdapter,
dataStoreConfiguration: .default,
syncEngine: syncEngine,
validAPIPluginKey: validAPIPluginKey,
validAuthPluginKey: validAuthPluginKey)
ModelRegistry.register(modelType: Restaurant.self)
ModelRegistry.register(modelType: Menu.self)
ModelRegistry.register(modelType: Dish.self)
ModelRegistry.register(modelType: ModelCompositePk.self)
ModelRegistry.register(modelType: PostWithCompositeKey.self)
ModelRegistry.register(modelType: CommentWithCompositeKey.self)
do {
try storageEngine.setUp(modelSchemas: [Restaurant.schema])
try storageEngine.setUp(modelSchemas: [Menu.schema])
try storageEngine.setUp(modelSchemas: [Dish.schema])
try storageEngine.setUp(modelSchemas: [ModelCompositePk.schema])
try storageEngine.setUp(modelSchemas: [PostWithCompositeKey.schema])
try storageEngine.setUp(modelSchemas: [CommentWithCompositeKey.schema])
} catch {
XCTFail("Failed to setup storage engine")
}
} catch {
XCTFail(String(describing: error))
return
}
}
// MARK: - Query and delete (No Sync)
func testWithId() async {
let restaurant = Restaurant(restaurantName: "restaurant1")
guard case .success = await saveModelSynchronous(model: restaurant) else {
XCTFail("Failed to save")
return
}
let predicate: QueryPredicate = Restaurant.keys.id == restaurant.id
guard case .success(let queriedRestaurants) = await queryModelSynchronous(modelType: Restaurant.self,
predicate: predicate) else {
XCTFail("Failed to query")
return
}
XCTAssertEqual(queriedRestaurants.count, 1)
let completed = expectation(description: "operation completed")
let identifier = DefaultModelIdentifier<Restaurant>.makeDefault(id: restaurant.id)
let operation = CascadeDeleteOperation(storageAdapter: storageAdapter,
syncEngine: nil,
modelType: Restaurant.self,
modelSchema: Restaurant.schema,
withIdentifier: identifier) { result in
switch result {
case .success(let restaurant):
XCTAssertNotNil(restaurant)
completed.fulfill()
case .failure(let error):
XCTFail("\(error)")
}
}
operation.start()
await waitForExpectations(timeout: 1)
guard case .success(let queriedRestaurants) = await queryModelSynchronous(modelType: Restaurant.self,
predicate: predicate) else {
XCTFail("Failed to query")
return
}
XCTAssertEqual(queriedRestaurants.count, 0)
}
func testWithCompositeIdentifier() {
let modelId = "model-id"
let modelDob = Temporal.DateTime.now()
let model = ModelCompositePk(id: modelId, dob: modelDob, name: "name")
guard case .success = saveModelSynchronous(model: model) else {
XCTFail("Failed to save")
return
}
let predicate: QueryPredicate = ModelCompositePk.keys.id == modelId
&& ModelCompositePk.keys.dob == modelDob
guard case .success(let queriedModel) = queryModelSynchronous(modelType: ModelCompositePk.self,
predicate: predicate) else {
XCTFail("Failed to query")
return
}
XCTAssertEqual(queriedModel.count, 1)
XCTAssertEqual(queriedModel[0].id, model.id)
XCTAssertEqual(queriedModel[0].name, model.name)
XCTAssertEqual(queriedModel[0].dob, model.dob)
let completed = expectation(description: "operation completed")
let identifier = ModelCompositePk.IdentifierProtocol.identifier(id: modelId, dob: modelDob)
let operation = CascadeDeleteOperation(storageAdapter: storageAdapter,
syncEngine: nil,
modelType: ModelCompositePk.self,
modelSchema: ModelCompositePk.schema,
withIdentifier: identifier) { result in
switch result {
case .success(let model):
XCTAssertNotNil(model)
completed.fulfill()
case .failure(let error):
XCTFail("\(error)")
}
}
operation.start()
wait(for: [completed], timeout: 1)
guard case .success(let queriedModel) = queryModelSynchronous(modelType: ModelCompositePk.self,
predicate: predicate) else {
XCTFail("Failed to query")
return
}
XCTAssertEqual(queriedModel.count, 0)
}
func testWithIdAndCondition() {
let restaurantName = UUID().uuidString
let restaurant = Restaurant(restaurantName: restaurantName)
guard case .success = saveModelSynchronous(model: restaurant) else {
XCTFail("Failed to save")
return
}
let predicate: QueryPredicate = Restaurant.keys.id == restaurant.id &&
Restaurant.keys.restaurantName == restaurantName
guard case .success(let queriedRestaurants) = queryModelSynchronous(modelType: Restaurant.self,
predicate: predicate) else {
XCTFail("Failed to query")
return
}
XCTAssertEqual(queriedRestaurants.count, 1)
XCTAssertEqual(queriedRestaurants.first!.restaurantName, restaurantName)
let completed = expectation(description: "operation completed")
let identifier = DefaultModelIdentifier<Restaurant>.makeDefault(id: restaurant.id)
let operation = CascadeDeleteOperation(storageAdapter: storageAdapter,
syncEngine: nil,
modelType: Restaurant.self,
modelSchema: Restaurant.schema,
withIdentifier: identifier,
condition: Restaurant.keys.restaurantName.eq(restaurantName)) { result in
switch result {
case .success(let restaurant):
XCTAssertNotNil(restaurant)
completed.fulfill()
case .failure(let error):
XCTFail("\(error)")
}
}
operation.start()
wait(for: [completed], timeout: 1)
guard case .success(let queriedRestaurants) = queryModelSynchronous(modelType: Restaurant.self,
predicate: predicate) else {
XCTFail("Failed to query")
return
}
XCTAssertEqual(queriedRestaurants.count, 0)
}
func testWithIdAndCondition_InvalidCondition() {
let restaurantName = UUID().uuidString
let restaurant = Restaurant(restaurantName: restaurantName)
guard case .success = saveModelSynchronous(model: restaurant) else {
XCTFail("Failed to save")
return
}
let predicate: QueryPredicate = Restaurant.keys.id == restaurant.id &&
Restaurant.keys.restaurantName == restaurantName
guard case .success(let queriedRestaurants) = queryModelSynchronous(modelType: Restaurant.self,
predicate: predicate) else {
XCTFail("Failed to query")
return
}
XCTAssertEqual(queriedRestaurants.count, 1)
XCTAssertEqual(queriedRestaurants.first!.restaurantName, restaurantName)
let completed = expectation(description: "operation completed")
let identifier = DefaultModelIdentifier<Restaurant>.makeDefault(id: restaurant.id)
let operation = CascadeDeleteOperation(storageAdapter: storageAdapter,
syncEngine: nil,
modelType: Restaurant.self,
modelSchema: Restaurant.schema,
withIdentifier: identifier,
condition: Restaurant.keys.restaurantName.ne(restaurantName)) { result in
switch result {
case .success:
XCTFail("Should have been invalid condition error")
case .failure(let error):
guard case .invalidCondition = error else {
XCTFail("\(error)")
return
}
completed.fulfill()
}
}
operation.start()
wait(for: [completed], timeout: 1)
guard case .success(let queriedRestaurants) = queryModelSynchronous(modelType: Restaurant.self,
predicate: predicate) else {
XCTFail("Failed to query")
return
}
XCTAssertEqual(queriedRestaurants.count, 1)
XCTAssertEqual(queriedRestaurants[0].restaurantName, restaurantName)
}
func testWithFilter() {
let restaurantName = UUID().uuidString
let restaurant = Restaurant(restaurantName: restaurantName)
guard case .success = saveModelSynchronous(model: restaurant) else {
XCTFail("Failed to save")
return
}
let predicate: QueryPredicate = Restaurant.keys.id == restaurant.id &&
Restaurant.keys.restaurantName == restaurantName
guard case .success(let queriedRestaurants) = queryModelSynchronous(modelType: Restaurant.self,
predicate: predicate) else {
XCTFail("Failed to query")
return
}
XCTAssertEqual(queriedRestaurants.count, 1)
XCTAssertEqual(queriedRestaurants.first!.restaurantName, restaurantName)
let completed = expectation(description: "operation completed")
let operation = CascadeDeleteOperation(storageAdapter: storageAdapter,
syncEngine: nil,
modelType: Restaurant.self,
modelSchema: Restaurant.schema,
filter: Restaurant.keys.restaurantName.eq(restaurantName)) { result in
switch result {
case .success(let restaurants):
XCTAssertEqual(restaurants.count, 1)
XCTAssertEqual(restaurants.first!.restaurantName, restaurantName)
completed.fulfill()
case .failure(let error):
XCTFail("\(error)")
}
}
operation.start()
wait(for: [completed], timeout: 1)
guard case .success(let queriedRestaurants) = queryModelSynchronous(modelType: Restaurant.self,
predicate: predicate) else {
XCTFail("Failed to query")
return
}
XCTAssertEqual(queriedRestaurants.count, 0)
}
func testWithFilter_NoneMatching() {
let restaurantName = UUID().uuidString
let restaurant = Restaurant(restaurantName: restaurantName)
guard case .success = saveModelSynchronous(model: restaurant) else {
XCTFail("Failed to save")
return
}
let predicate: QueryPredicate = Restaurant.keys.id == restaurant.id &&
Restaurant.keys.restaurantName == restaurantName
guard case .success(let queriedRestaurants) = queryModelSynchronous(modelType: Restaurant.self,
predicate: predicate) else {
XCTFail("Failed to query")
return
}
XCTAssertEqual(queriedRestaurants.count, 1)
XCTAssertEqual(queriedRestaurants.first!.restaurantName, restaurantName)
let completed = expectation(description: "operation completed")
let operation = CascadeDeleteOperation(storageAdapter: storageAdapter,
syncEngine: nil,
modelType: Restaurant.self,
modelSchema: Restaurant.schema,
filter: Restaurant.keys.restaurantName.ne(restaurantName)) { result in
switch result {
case .success(let restaurants):
XCTAssertEqual(restaurants.count, 0)
completed.fulfill()
case .failure(let error):
XCTFail("\(error)")
}
}
operation.start()
wait(for: [completed], timeout: 1)
guard case .success(let queriedRestaurants) = queryModelSynchronous(modelType: Restaurant.self,
predicate: predicate) else {
XCTFail("Failed to query")
return
}
XCTAssertEqual(queriedRestaurants.count, 1)
XCTAssertEqual(queriedRestaurants[0].restaurantName, restaurantName)
}
// MARK: - Query and delete (With Sync)
func testWithId_WithSync() async {
let restaurant = Restaurant(restaurantName: "restaurant1")
guard case .success = await saveModelSynchronous(model: restaurant) else {
XCTFail("Failed to save")
return
}
let predicate: QueryPredicate = Restaurant.keys.id == restaurant.id
guard case .success(let queriedRestaurants) = await queryModelSynchronous(modelType: Restaurant.self,
predicate: predicate) else {
XCTFail("Failed to query")
return
}
XCTAssertEqual(queriedRestaurants.count, 1)
let receivedMutationEvent = expectation(description: "Mutation Events submitted to sync engine")
receivedMutationEvent.expectedFulfillmentCount = 1
let expectedFailures = expectation(description: "Simulated failure on mutation event submitted to sync engine")
expectedFailures.isInverted = true
let expectedSuccess = expectation(description: "Simulated success on mutation event submitted to sync engine")
expectedSuccess.expectedFulfillmentCount = 1
syncEngine.setCallbackOnSubmit { submittedMutationEvent, completion in
receivedMutationEvent.fulfill()
if submittedMutationEvent.modelId == restaurant.id {
expectedSuccess.fulfill()
completion(.success(submittedMutationEvent))
} else {
expectedFailures.fulfill()
completion(.failure(.internalOperation("mockError", "", nil)))
}
}
let completed = expectation(description: "operation completed")
let identifier = DefaultModelIdentifier<Restaurant>.makeDefault(id: restaurant.id)
let operation = CascadeDeleteOperation(storageAdapter: storageAdapter,
syncEngine: syncEngine,
modelType: Restaurant.self,
modelSchema: Restaurant.schema,
withIdentifier: identifier) { result in
switch result {
case .success(let restaurant):
XCTAssertNotNil(restaurant)
completed.fulfill()
case .failure(let error):
XCTFail("\(error)")
}
}
operation.start()
await waitForExpectations(timeout: 1)
guard case .success(let queriedRestaurants) = await queryModelSynchronous(modelType: Restaurant.self,
predicate: predicate) else {
XCTFail("Failed to query")
return
}
XCTAssertEqual(queriedRestaurants.count, 0)
}
func testWithIdentifier_WithSync() async {
let modelId = "model-id"
let modelDob = Temporal.DateTime.now()
let model = ModelCompositePk(id: modelId, dob: modelDob, name: "name")
guard case .success = await saveModelSynchronous(model: model) else {
XCTFail("Failed to save")
return
}
let predicate: QueryPredicate = ModelCompositePk.keys.id == modelId
&& ModelCompositePk.keys.dob == modelDob
guard case .success(let queriedModels) = await queryModelSynchronous(modelType: ModelCompositePk.self,
predicate: predicate) else {
XCTFail("Failed to query")
return
}
XCTAssertEqual(queriedModels.count, 1)
let receivedMutationEvent = expectation(description: "Mutation Events submitted to sync engine")
receivedMutationEvent.expectedFulfillmentCount = 1
let expectedFailures = expectation(description: "Simulated failure on mutation event submitted to sync engine")
expectedFailures.isInverted = true
let expectedSuccess = expectation(description: "Simulated success on mutation event submitted to sync engine")
expectedSuccess.expectedFulfillmentCount = 1
syncEngine.setCallbackOnSubmit { submittedMutationEvent, completion in
receivedMutationEvent.fulfill()
if submittedMutationEvent.modelId == model.identifier {
expectedSuccess.fulfill()
completion(.success(submittedMutationEvent))
} else {
expectedFailures.fulfill()
completion(.failure(.internalOperation("mockError", "", nil)))
}
}
let completed = expectation(description: "operation completed")
let identifier = ModelCompositePk.IdentifierProtocol.identifier(id: modelId, dob: modelDob)
let operation = CascadeDeleteOperation(storageAdapter: storageAdapter,
syncEngine: syncEngine,
modelType: ModelCompositePk.self,
modelSchema: ModelCompositePk.schema,
withIdentifier: identifier) { result in
switch result {
case .success(let restaurant):
XCTAssertNotNil(restaurant)
completed.fulfill()
case .failure(let error):
XCTFail("\(error)")
}
}
operation.start()
await waitForExpectations(timeout: 1)
guard case .success(let queriedModels) = await queryModelSynchronous(modelType: ModelCompositePk.self,
predicate: predicate) else {
XCTFail("Failed to query")
return
}
XCTAssertEqual(queriedModels.count, 0)
}
func testWithIdAndCondition_WithSync() {
let restaurantName = UUID().uuidString
let restaurant = Restaurant(restaurantName: restaurantName)
guard case .success = saveModelSynchronous(model: restaurant) else {
XCTFail("Failed to save")
return
}
let predicate: QueryPredicate = Restaurant.keys.id == restaurant.id &&
Restaurant.keys.restaurantName == restaurantName
guard case .success(let queriedRestaurants) = queryModelSynchronous(modelType: Restaurant.self,
predicate: predicate) else {
XCTFail("Failed to query")
return
}
XCTAssertEqual(queriedRestaurants.count, 1)
XCTAssertEqual(queriedRestaurants.first!.restaurantName, restaurantName)
let receivedMutationEvent = expectation(description: "Mutation Events submitted to sync engine")
receivedMutationEvent.expectedFulfillmentCount = 1
let expectedFailures = expectation(description: "Simulated failure on mutation event submitted to sync engine")
expectedFailures.isInverted = true
let expectedSuccess = expectation(description: "Simulated success on mutation event submitted to sync engine")
expectedSuccess.expectedFulfillmentCount = 1
syncEngine.setCallbackOnSubmit { submittedMutationEvent, completion in
receivedMutationEvent.fulfill()
if submittedMutationEvent.modelId == restaurant.id {
expectedSuccess.fulfill()
completion(.success(submittedMutationEvent))
} else {
expectedFailures.fulfill()
completion(.failure(.internalOperation("mockError", "", nil)))
}
}
let completed = expectation(description: "operation completed")
let identifier = DefaultModelIdentifier<Restaurant>.makeDefault(id: restaurant.id)
let operation = CascadeDeleteOperation(storageAdapter: storageAdapter,
syncEngine: syncEngine,
modelType: Restaurant.self,
modelSchema: Restaurant.schema,
withIdentifier: identifier,
condition: Restaurant.keys.restaurantName.eq(restaurantName)) { result in
switch result {
case .success(let restaurant):
XCTAssertNotNil(restaurant)
completed.fulfill()
case .failure(let error):
XCTFail("\(error)")
}
}
operation.start()
wait(for: [completed, receivedMutationEvent, expectedFailures, expectedSuccess], timeout: 1)
guard case .success(let queriedRestaurants) = queryModelSynchronous(modelType: Restaurant.self,
predicate: predicate) else {
XCTFail("Failed to query")
return
}
XCTAssertEqual(queriedRestaurants.count, 0)
}
func testWithFilter_WithSync() {
let restaurantName = UUID().uuidString
let restaurant = Restaurant(restaurantName: restaurantName)
guard case .success = saveModelSynchronous(model: restaurant) else {
XCTFail("Failed to save")
return
}
let predicate: QueryPredicate = Restaurant.keys.id == restaurant.id &&
Restaurant.keys.restaurantName == restaurantName
guard case .success(let queriedRestaurants) = queryModelSynchronous(modelType: Restaurant.self,
predicate: predicate) else {
XCTFail("Failed to query")
return
}
XCTAssertEqual(queriedRestaurants.count, 1)
XCTAssertEqual(queriedRestaurants.first!.restaurantName, restaurantName)
let receivedMutationEvent = expectation(description: "Mutation Events submitted to sync engine")
receivedMutationEvent.expectedFulfillmentCount = 1
let expectedFailures = expectation(description: "Simulated failure on mutation event submitted to sync engine")
expectedFailures.isInverted = true
let expectedSuccess = expectation(description: "Simulated success on mutation event submitted to sync engine")
expectedSuccess.expectedFulfillmentCount = 1
syncEngine.setCallbackOnSubmit { submittedMutationEvent, completion in
receivedMutationEvent.fulfill()
if submittedMutationEvent.modelId == restaurant.id {
expectedSuccess.fulfill()
completion(.success(submittedMutationEvent))
} else {
expectedFailures.fulfill()
completion(.failure(.internalOperation("mockError", "", nil)))
}
}
let completed = expectation(description: "operation completed")
let operation = CascadeDeleteOperation(storageAdapter: storageAdapter,
syncEngine: syncEngine,
modelType: Restaurant.self,
modelSchema: Restaurant.schema,
filter: Restaurant.keys.restaurantName.eq(restaurantName)) { result in
switch result {
case .success(let restaurants):
XCTAssertEqual(restaurants.count, 1)
XCTAssertEqual(restaurants.first!.restaurantName, restaurantName)
completed.fulfill()
case .failure(let error):
XCTFail("\(error)")
}
}
operation.start()
wait(for: [completed, receivedMutationEvent, expectedFailures, expectedSuccess], timeout: 1)
guard case .success(let queriedRestaurants) = queryModelSynchronous(modelType: Restaurant.self,
predicate: predicate) else {
XCTFail("Failed to query")
return
}
XCTAssertEqual(queriedRestaurants.count, 0)
}
// MARK: - Internal testing
func testSingle() async {
let restaurant = Restaurant(restaurantName: "restaurant1")
guard case .success = await saveModelSynchronous(model: restaurant) else {
XCTFail("Failed to save")
return
}
let identifier = DefaultModelIdentifier<Restaurant>.makeDefault(id: restaurant.id)
let operation = CascadeDeleteOperation(storageAdapter: storageAdapter,
syncEngine: syncEngine,
modelType: Restaurant.self,
modelSchema: Restaurant.schema,
withIdentifier: identifier) { _ in }
let result = await operation.queryAndDeleteTransaction()
switch result {
case .success(let queryAndDeleteResult):
XCTAssertEqual(queryAndDeleteResult.deletedModels.count, 1)
XCTAssertEqual(queryAndDeleteResult.associatedModels.count, 0)
case .failure(let error):
XCTFail("\(error)")
}
let receivedMutationEvent = expectation(description: "Mutation Events submitted to sync engine")
receivedMutationEvent.expectedFulfillmentCount = 1
let expectedFailures = expectation(description: "Simulated failure on mutation event submitted to sync engine")
expectedFailures.isInverted = true
let expectedSuccess = expectation(description: "Simulated success on mutation event submitted to sync engine")
expectedSuccess.expectedFulfillmentCount = 1
syncEngine.setCallbackOnSubmit { submittedMutationEvent, completion in
receivedMutationEvent.fulfill()
if submittedMutationEvent.modelId == restaurant.id {
expectedSuccess.fulfill()
completion(.success(submittedMutationEvent))
} else {
expectedFailures.fulfill()
completion(.failure(.internalOperation("mockError", "", nil)))
}
}
operation.syncIfNeededAndFinish(result)
wait(for: [receivedMutationEvent, expectedFailures, expectedSuccess], timeout: 1)
}
func testDeleteWithAssociatedModels() async {
let restaurant = Restaurant(restaurantName: "restaurant1")
let lunchStandardMenu = Menu(name: "Standard", menuType: .lunch, restaurant: restaurant)
let oysters = Dish(dishName: "Fried oysters", menu: lunchStandardMenu)
guard case .success = await saveModelSynchronous(model: restaurant),
case .success = await saveModelSynchronous(model: lunchStandardMenu),
case .success = await saveModelSynchronous(model: oysters) else {
XCTFail("Failed to save hierarchy")
return
}
let completed = expectation(description: "operation completed")
let identifier = DefaultModelIdentifier<Restaurant>.makeDefault(id: restaurant.id)
let operation = CascadeDeleteOperation(storageAdapter: storageAdapter,
syncEngine: syncEngine,
modelType: Restaurant.self,
modelSchema: Restaurant.schema,
withIdentifier: identifier) { result in
switch result {
case .success:
completed.fulfill()
case .failure(let error):
XCTFail("\(error)")
}
}
let result = await operation.queryAndDeleteTransaction()
switch result {
case .success(let queryAndDeleteResult):
XCTAssertEqual(queryAndDeleteResult.deletedModels.count, 1)
XCTAssertEqual(queryAndDeleteResult.associatedModels.count, 2)
// The associated models are retrieved in order (Restaurant to Menu to Dish)
XCTAssertEqual(queryAndDeleteResult.associatedModels[0].0, Menu.modelName)
XCTAssertEqual(queryAndDeleteResult.associatedModels[1].0, Dish.modelName)
case .failure(let error):
XCTFail("\(error)")
}
let receivedMutationEvent = expectation(description: "Mutation Events submitted to sync engine")
receivedMutationEvent.expectedFulfillmentCount = 3
let expectedFailures = expectation(description: "Simulated failure on mutation event submitted to sync engine")
expectedFailures.isInverted = true
let expectedSuccess = expectation(description: "Simulated success on mutation event submitted to sync engine")
expectedSuccess.expectedFulfillmentCount = 3
var submittedEvents = [MutationEvent]()
syncEngine.setCallbackOnSubmit { submittedMutationEvent, completion in
submittedEvents.append(submittedMutationEvent)
receivedMutationEvent.fulfill()
if submittedMutationEvent.modelId == restaurant.id ||
submittedMutationEvent.modelId == lunchStandardMenu.id ||
submittedMutationEvent.modelId == oysters.id {
expectedSuccess.fulfill()
completion(.success(submittedMutationEvent))
} else {
expectedFailures.fulfill()
completion(.failure(.internalOperation("mockError", "", nil)))
}
}
operation.syncIfNeededAndFinish(result)
wait(for: [completed, receivedMutationEvent, expectedFailures, expectedSuccess], timeout: 1)
XCTAssertEqual(submittedEvents.count, 3)
// The delete mutations should be synced in reverse order (children to parent)
XCTAssertEqual(submittedEvents[0].modelName, Dish.modelName)
XCTAssertEqual(submittedEvents[1].modelName, Menu.modelName)
XCTAssertEqual(submittedEvents[2].modelName, Restaurant.modelName)
}
func testDeleteWithAssociatedModelsAndCompositePK() async {
let post = PostWithCompositeKey(id: "post-id", title: "title")
let comment = CommentWithCompositeKey(id: "comment-id", content: "comment-content", post: post)
if case .failure(let error) = await saveModelSynchronous(model: post) {
XCTFail("Failed to save post with error \(error)")
}
if case .failure(let error) = await saveModelSynchronous(model: comment) {
XCTFail("Failed to save comment with error \(error)")
}
let completed = expectation(description: "operation completed")
let identifier = PostWithCompositeKey.IdentifierProtocol.identifier(id: post.id, title: post.title)
let operation = CascadeDeleteOperation(storageAdapter: storageAdapter,
syncEngine: syncEngine,
modelType: PostWithCompositeKey.self,
modelSchema: PostWithCompositeKey.schema,
withIdentifier: identifier) { result in
switch result {
case .success:
completed.fulfill()
case .failure(let error):
XCTFail("\(error)")
}
}
let result = await operation.queryAndDeleteTransaction()
switch result {
case .success(let queryAndDeleteResult):
XCTAssertEqual(queryAndDeleteResult.deletedModels.count, 1)
XCTAssertEqual(queryAndDeleteResult.associatedModels.count, 1)
// The associated models are retrieved in order (Restaurant to Menu to Dish)
XCTAssertEqual(queryAndDeleteResult.associatedModels[0].0, CommentWithCompositeKey.modelName)
case .failure(let error):
XCTFail("\(error)")
}
let receivedMutationEvent = expectation(description: "Mutation Events submitted to sync engine")
receivedMutationEvent.expectedFulfillmentCount = 2
let expectedFailures = expectation(description: "Simulated failure on mutation event submitted to sync engine")
expectedFailures.isInverted = true
let expectedSuccess = expectation(description: "Simulated success on mutation event submitted to sync engine")
expectedSuccess.expectedFulfillmentCount = 2
var submittedEvents = [MutationEvent]()
syncEngine.setCallbackOnSubmit { submittedMutationEvent, completion in
submittedEvents.append(submittedMutationEvent)
receivedMutationEvent.fulfill()
if submittedMutationEvent.modelId == post.identifier ||
submittedMutationEvent.modelId == comment.identifier {
expectedSuccess.fulfill()
completion(.success(submittedMutationEvent))
} else {
expectedFailures.fulfill()
completion(.failure(.internalOperation("mockError", "", nil)))
}
}
operation.syncIfNeededAndFinish(result)
await waitForExpectations(timeout: 1)
XCTAssertEqual(submittedEvents.count, 2)
// The delete mutations should be synced in reverse order (children to parent)
XCTAssertEqual(submittedEvents[0].modelName, CommentWithCompositeKey.modelName)
XCTAssertEqual(submittedEvents[1].modelName, PostWithCompositeKey.modelName)
}
func testDeleteWithAssociatedModels_SingleFailure() async {
let restaurant = Restaurant(restaurantName: "restaurant1")
let lunchStandardMenu = Menu(name: "Standard", menuType: .lunch, restaurant: restaurant)
let oysters = Dish(dishName: "Fried oysters", menu: lunchStandardMenu)
guard case .success = await saveModelSynchronous(model: restaurant),
case .success = await saveModelSynchronous(model: lunchStandardMenu),
case .success = await saveModelSynchronous(model: oysters) else {
XCTFail("Failed to save hierarchy")
return
}
let completed = expectation(description: "operation completed")
let identifier = DefaultModelIdentifier<Restaurant>.makeDefault(id: restaurant.id)
let operation = CascadeDeleteOperation(storageAdapter: storageAdapter,
syncEngine: syncEngine,
modelType: Restaurant.self,
modelSchema: Restaurant.schema,
withIdentifier: identifier) { result in
switch result {
case .success:
XCTFail("Should have failed")
case .failure:
completed.fulfill()
}
}
let result = await operation.queryAndDeleteTransaction()
switch result {
case .success(let queryAndDeleteResult):
XCTAssertEqual(queryAndDeleteResult.deletedModels.count, 1)
XCTAssertEqual(queryAndDeleteResult.associatedModels.count, 2)
// The associated models are retrieved in order (Restaurant to Menu to Dish)
XCTAssertEqual(queryAndDeleteResult.associatedModels[0].0, Menu.modelName)
XCTAssertEqual(queryAndDeleteResult.associatedModels[1].0, Dish.modelName)
case .failure(let error):
XCTFail("\(error)")
}
let receivedMutationEvent = expectation(description: "Mutation Events submitted to sync engine")
receivedMutationEvent.expectedFulfillmentCount = 3
let expectedFailures = expectation(description: "Simulated failure on mutation event submitted to sync engine")
expectedFailures.expectedFulfillmentCount = 1
let expectedSuccess = expectation(description: "Simulated success on mutation event submitted to sync engine")
expectedSuccess.expectedFulfillmentCount = 2
var submittedEvents = [MutationEvent]()
syncEngine.setCallbackOnSubmit { submittedMutationEvent, completion in
submittedEvents.append(submittedMutationEvent)
receivedMutationEvent.fulfill()
if submittedMutationEvent.modelId == restaurant.id ||
submittedMutationEvent.modelId == oysters.id {
expectedSuccess.fulfill()
completion(.success(submittedMutationEvent))
} else {
expectedFailures.fulfill()
completion(.failure(.internalOperation("mockError", "", nil)))
}
}
operation.syncIfNeededAndFinish(result)
await waitForExpectations(timeout: 1)
XCTAssertEqual(submittedEvents.count, 3)
// The delete mutations should be synced in reverse order (children to parent)
XCTAssertEqual(submittedEvents[0].modelName, Dish.modelName)
XCTAssertEqual(submittedEvents[1].modelName, Menu.modelName)
XCTAssertEqual(submittedEvents[2].modelName, Restaurant.modelName)
}
}