amplify-swift/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/LocalSubscriptionWithJSONMo...

423 lines
17 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 AWSPluginsCore
@testable import AmplifyTestCommon
@testable import AWSDataStorePlugin
/// Tests behavior of local DataStore subscriptions (as opposed to remote API subscription behaviors)
/// using serialized JSON models
class LocalSubscriptionWithJSONModelTests: XCTestCase {
var dataStorePlugin: AWSDataStorePlugin!
override func setUp() async throws {
try await super.setUp()
await Amplify.reset()
Amplify.Logging.logLevel = .warn
let storageAdapter: SQLiteStorageEngineAdapter
let storageEngine: StorageEngine
var stateMachine: MockStateMachine<RemoteSyncEngine.State, RemoteSyncEngine.Action>!
let validAPIPluginKey = "MockAPICategoryPlugin"
let validAuthPluginKey = "MockAuthCategoryPlugin"
do {
let connection = try Connection(.inMemory)
storageAdapter = try SQLiteStorageEngineAdapter(connection: connection)
try storageAdapter.setUp(modelSchemas: StorageEngine.systemModelSchemas)
let outgoingMutationQueue = NoOpMutationQueue()
let mutationDatabaseAdapter = try AWSMutationDatabaseAdapter(storageAdapter: storageAdapter)
let awsMutationEventPublisher = AWSMutationEventPublisher(eventSource: mutationDatabaseAdapter)
stateMachine = MockStateMachine(initialState: .notStarted,
resolver: RemoteSyncEngine.Resolver.resolve(currentState:action:))
let syncEngine = RemoteSyncEngine(
storageAdapter: storageAdapter,
dataStoreConfiguration: .default,
authModeStrategy: AWSDefaultAuthModeStrategy(),
outgoingMutationQueue: outgoingMutationQueue,
mutationEventIngester: mutationDatabaseAdapter,
mutationEventPublisher: awsMutationEventPublisher,
initialSyncOrchestratorFactory: NoOpInitialSyncOrchestrator.factory,
reconciliationQueueFactory: MockAWSIncomingEventReconciliationQueue.factory,
stateMachine: stateMachine,
networkReachabilityPublisher: nil,
requestRetryablePolicy: MockRequestRetryablePolicy()
)
storageEngine = StorageEngine(storageAdapter: storageAdapter,
dataStoreConfiguration: .default,
syncEngine: syncEngine,
validAPIPluginKey: validAPIPluginKey,
validAuthPluginKey: validAuthPluginKey)
} catch {
XCTFail(String(describing: error))
return
}
let storageEngineBehaviorFactory: StorageEngineBehaviorFactory = {_, _, _, _, _, _ throws in
return storageEngine
}
let dataStorePublisher = DataStorePublisher()
dataStorePlugin = AWSDataStorePlugin(modelRegistration: TestJsonModelRegistration(),
storageEngineBehaviorFactory: storageEngineBehaviorFactory,
dataStorePublisher: dataStorePublisher,
validAPIPluginKey: validAPIPluginKey,
validAuthPluginKey: validAuthPluginKey)
let dataStoreConfig = DataStoreCategoryConfiguration(plugins: [
"awsDataStorePlugin": true
])
// Since these tests use syncable models, we have to set up an API category also
let apiConfig = APICategoryConfiguration(plugins: ["MockAPICategoryPlugin": true])
let apiPlugin = MockAPICategoryPlugin()
let amplifyConfig = AmplifyConfiguration(api: apiConfig, dataStore: dataStoreConfig)
do {
try Amplify.add(plugin: apiPlugin)
try Amplify.add(plugin: dataStorePlugin)
try Amplify.configure(amplifyConfig)
} catch {
XCTFail(String(describing: error))
return
}
}
/// - Given: A configured Amplify system on iOS 13 or higher
/// - When:
/// - I get a publisher observing a model
/// - Then:
/// - I receive notifications for updates to that model
func testPublisher() {
let receivedMutationEvent = expectation(description: "Received mutation event")
let subscription = dataStorePlugin.publisher(for: "Post").sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
XCTFail("Unexpected error: \(error)")
case .finished:
break
}
}, receiveValue: { _ in
receivedMutationEvent.fulfill()
})
// insert a post
let title = "a title"
let content = "some content"
let createdAt = Temporal.DateTime.now().iso8601String
let post = ["title": .string(title),
"content": .string(content),
"createdAt": .string(createdAt)] as [String: JSONValue]
let model = DynamicModel(values: post)
let postSchema = ModelRegistry.modelSchema(from: "Post")!
dataStorePlugin.save(model, modelSchema: postSchema) { _ in }
wait(for: [receivedMutationEvent], timeout: 1.0)
subscription.cancel()
}
/// - Given: A configured Amplify system on iOS 13 or higher
/// - When:
/// - I get a publisher observing all models
/// - I perform mutation for Post and Comment
/// - Then:
/// - I receive notifications for updates to both Post and Comment
func testPublisherWithMultipleCreate() {
let receivedPostMutationEvent = expectation(description: "Received post mutation event")
let receivedCommentMutationEvent = expectation(description: "Received Comment mutation event")
let subscription = dataStorePlugin.publisher.sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
XCTFail("Unexpected error: \(error)")
case .finished:
break
}
}, receiveValue: { (event: MutationEvent) in
switch event.modelName {
case "Post":
receivedPostMutationEvent.fulfill()
case "Comment":
receivedCommentMutationEvent.fulfill()
default:
print("Ignore")
}
})
// insert a post
let title = "a title"
let content = "some content"
let createdAt = Temporal.DateTime.now().iso8601String
let post = ["title": .string(title),
"content": .string(content),
"createdAt": .string(createdAt)] as [String: JSONValue]
let model = DynamicModel(values: post)
let postSchema = ModelRegistry.modelSchema(from: "Post")!
dataStorePlugin.save(model, modelSchema: postSchema) { _ in }
// insert a comment
let commentContent = "some content"
let comment = ["content": .string(commentContent),
"createdAt": .string(createdAt),
"post": .object(model.values)] as [String: JSONValue]
let commentModel = DynamicModel(values: comment)
let commentSchema = ModelRegistry.modelSchema(from: "Comment")!
dataStorePlugin.save(commentModel, modelSchema: commentSchema) { result in
switch result {
case .failure(let error):
print(error)
case .success(let model):
print(model)
}
}
wait(for: [receivedPostMutationEvent, receivedCommentMutationEvent], timeout: 3.0)
subscription.cancel()
}
/// - Given: A configured DataStore
/// - When:
/// - I subscribe to model events
/// - Then:
/// - I am notified of `create` mutations
func testCreate() {
let receivedMutationEvent = expectation(description: "Received mutation event")
let subscription = dataStorePlugin.publisher(for: "Post").sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
XCTFail("Unexpected error: \(error)")
case .finished:
break
}
}, receiveValue: { mutationEvent in
if mutationEvent.mutationType == MutationEvent.MutationType.create.rawValue {
receivedMutationEvent.fulfill()
}
})
let title = "a title"
let content = "some content"
let createdAt = Temporal.DateTime.now().iso8601String
let post = ["title": .string(title),
"content": .string(content),
"createdAt": .string(createdAt)] as [String: JSONValue]
let model = DynamicModel(values: post)
let postSchema = ModelRegistry.modelSchema(from: "Post")!
dataStorePlugin.save(model, modelSchema: postSchema) { _ in }
wait(for: [receivedMutationEvent], timeout: 1.0)
subscription.cancel()
}
/// - Given: A configured DataStore
/// - When:
/// - I subscribe to model events
/// - Then:
/// - I am notified of `update` mutations
func testUpdate() {
let originalContent = "Content as of \(Date())"
let title = "a title"
let createdAt = Temporal.DateTime.now().iso8601String
let post = ["title": .string(title),
"content": .string(originalContent),
"createdAt": .string(createdAt)] as [String: JSONValue]
let model = DynamicModel(values: post)
let postSchema = ModelRegistry.modelSchema(from: "Post")!
let saveCompleted = expectation(description: "Save complete")
dataStorePlugin.save(model, modelSchema: postSchema) { _ in
saveCompleted.fulfill()
}
wait(for: [saveCompleted], timeout: 5.0)
let newContent = "Updated content as of \(Date())"
var newModel = model
newModel.values["content"] = JSONValue.string(newContent)
newModel.values["createdAt"] = JSONValue.string(Temporal.DateTime.now().iso8601String)
let receivedMutationEvent = expectation(description: "Received mutation event")
let subscription = dataStorePlugin.publisher(for: "Post").sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
XCTFail("Unexpected error: \(error)")
case .finished:
break
}
}, receiveValue: { mutationEvent in
if mutationEvent.mutationType == MutationEvent.MutationType.update.rawValue {
receivedMutationEvent.fulfill()
}
})
dataStorePlugin.save(newModel, modelSchema: postSchema) { _ in }
wait(for: [receivedMutationEvent], timeout: 1.0)
subscription.cancel()
}
/// - Given: A configured DataStore
/// - When:
/// - I subscribe to model events
/// - Then:
/// - I am notified of `delete` mutations
func testDelete() {
let receivedMutationEvent = expectation(description: "Received mutation event")
let subscription = dataStorePlugin.publisher(for: "Post").sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
XCTFail("Unexpected error: \(error)")
case .finished:
break
}
}, receiveValue: { mutationEvent in
if mutationEvent.mutationType == MutationEvent.MutationType.delete.rawValue {
receivedMutationEvent.fulfill()
}
})
let title = "a title"
let content = "some content"
let createdAt = Temporal.DateTime.now().iso8601String
let post = ["title": .string(title),
"content": .string(content),
"createdAt": .string(createdAt)] as [String: JSONValue]
let model = DynamicModel(values: post)
let postSchema = ModelRegistry.modelSchema(from: "Post")!
dataStorePlugin.save(model, modelSchema: postSchema) { _ in }
dataStorePlugin.delete(model, modelSchema: postSchema) { _ in }
wait(for: [receivedMutationEvent], timeout: 1.0)
subscription.cancel()
}
/// - Given: A configured DataStore, with post and comment
/// - When:
/// - I subscribe to model events
/// - Delete the post. This will cascade delete the comments as well
/// - Then:
/// - I am notified of `delete` mutations of the post and comments deleted
func testDeletePostShouldDeleteComments() {
let receivedPostMutationEvent = expectation(description: "Received post delete mutation event")
let subscriptionPost = dataStorePlugin.publisher(for: "Post").sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
XCTFail("Unexpected error: \(error)")
case .finished:
break
}
}, receiveValue: { mutationEvent in
if mutationEvent.mutationType == MutationEvent.MutationType.delete.rawValue {
receivedPostMutationEvent.fulfill()
}
})
let title = "a title"
let content = "some content"
let createdAt = Temporal.DateTime.now().iso8601String
let post = ["title": .string(title),
"content": .string(content),
"createdAt": .string(createdAt)] as [String: JSONValue]
let model = DynamicModel(values: post)
let postSchema = ModelRegistry.modelSchema(from: "Post")!
let savedPost = expectation(description: "post saved")
dataStorePlugin.save(model, modelSchema: postSchema) { result in
switch result {
case .failure(let error):
XCTFail("\(error)")
case .success(let model):
print(model)
savedPost.fulfill()
}
}
wait(for: [savedPost], timeout: 1.0)
let commentContent = "some content"
let comment = ["content": .string(commentContent),
"createdAt": .string(createdAt),
"post": .object(model.values)] as [String: JSONValue]
let commentModel = DynamicModel(values: comment)
let commentSchema = ModelRegistry.modelSchema(from: "Comment")!
let savedComment = expectation(description: "comment saved")
dataStorePlugin.save(commentModel, modelSchema: commentSchema) { result in
switch result {
case .failure(let error):
XCTFail("\(error)")
case .success(let model):
print(model)
savedComment.fulfill()
}
}
wait(for: [savedComment], timeout: 1.0)
let queryCommentSuccess = expectation(description: "querying for comment should exist")
dataStorePlugin.query(DynamicModel.self,
modelSchema: commentSchema,
where: DynamicModel.keys.id == commentModel.id) { result in
switch result {
case .success(let comments):
XCTAssertEqual(comments.count, 1)
queryCommentSuccess.fulfill()
case .failure(let error):
XCTFail("\(error)")
}
}
wait(for: [queryCommentSuccess], timeout: 10.0)
let deletePostSuccess = expectation(description: "deleted post successfully")
dataStorePlugin.delete(model, modelSchema: postSchema) { result in
switch result {
case .success:
deletePostSuccess.fulfill()
case .failure(let error):
XCTFail("\(error)")
}
}
wait(for: [receivedPostMutationEvent, deletePostSuccess], timeout: 10.0)
subscriptionPost.cancel()
let queryCommentEmpty = expectation(description: "querying for comment should be empty")
dataStorePlugin.query(DynamicModel.self,
modelSchema: commentSchema,
where: DynamicModel.keys.id == commentModel.id) { result in
switch result {
case .success(let comments):
XCTAssertEqual(comments.count, 0)
queryCommentEmpty.fulfill()
case .failure(let error):
XCTFail("\(error)")
}
}
wait(for: [queryCommentEmpty], timeout: 10.0)
}
}