808 lines
39 KiB
Swift
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)
|
|
}
|
|
}
|