Skip to content

Commit 258dbc6

Browse files
committed
Added Unit tests for OutgoingMutationQueue state and minor refactor
1 parent 742bfef commit 258dbc6

File tree

5 files changed

+245
-36
lines changed

5 files changed

+245
-36
lines changed

AmplifyPlugins/DataStore/AWSDataStoreCategoryPlugin/Sync/MutationSync/OutgoingMutationQueue.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,18 @@ final class OutgoingMutationQueue: OutgoingMutationQueueBehavior {
3030
private weak var api: APICategoryGraphQLBehavior?
3131
private var subscription: Subscription?
3232

33-
init() {
33+
init(_ stateMachine: StateMachine<State, Action>? = nil) {
3434
let operationQueue = OperationQueue()
3535
operationQueue.name = "com.amazonaws.OutgoingMutationOperationQueue"
3636
operationQueue.maxConcurrentOperationCount = 1
3737
operationQueue.isSuspended = true
3838

3939
self.operationQueue = operationQueue
4040

41-
self.stateMachine = StateMachine(initialState: .notInitialized,
42-
resolver: OutgoingMutationQueue.Resolver.resolve(currentState:action:))
41+
self.stateMachine = stateMachine ?? StateMachine(initialState: .notInitialized,
42+
resolver: OutgoingMutationQueue.Resolver.resolve(currentState:action:))
4343

44-
self.stateMachineSink = stateMachine
44+
self.stateMachineSink = self.stateMachine
4545
.$state
4646
.sink { [weak self] newState in
4747
guard let self = self else {
@@ -54,7 +54,7 @@ final class OutgoingMutationQueue: OutgoingMutationQueueBehavior {
5454
}
5555

5656
log.verbose("Initialized")
57-
stateMachine.notify(action: .initialized)
57+
self.stateMachine.notify(action: .initialized)
5858
}
5959

6060
// MARK: - Public API
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
//
2+
// Copyright 2018-2019 Amazon.com,
3+
// Inc. or its affiliates. All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
import Combine
10+
import XCTest
11+
12+
@testable import Amplify
13+
@testable import AmplifyTestCommon
14+
@testable import AWSPluginsCore
15+
@testable import AWSDataStoreCategoryPlugin
16+
17+
class OutgoingMutationQueueMockStateTest: XCTestCase {
18+
var mutationQueue: OutgoingMutationQueue!
19+
var stateMachine: MockStateMachine<OutgoingMutationQueue.State, OutgoingMutationQueue.Action>!
20+
var publisher: AWSMutationEventPublisher!
21+
var apiBehavior: MockAPICategoryPlugin!
22+
var eventSource: MockMutationEventSource!
23+
override func setUp() {
24+
do {
25+
try setUpWithAPI()
26+
} catch {
27+
XCTFail(String(describing: "Unable to setup API category for unit tests"))
28+
}
29+
ModelRegistry.register(modelType: Post.self)
30+
stateMachine = MockStateMachine(initialState: .notInitialized,
31+
resolver: OutgoingMutationQueue.Resolver.resolve(currentState:action:))
32+
mutationQueue = OutgoingMutationQueue(stateMachine)
33+
eventSource = MockMutationEventSource()
34+
publisher = AWSMutationEventPublisher(eventSource: eventSource)
35+
apiBehavior = MockAPICategoryPlugin()
36+
37+
}
38+
39+
func testInitialState() {
40+
let expect = expectation(description: "state initialized")
41+
stateMachine.pushExpectActionCriteria { action in
42+
XCTAssertEqual(action, OutgoingMutationQueue.Action.initialized)
43+
expect.fulfill()
44+
}
45+
46+
mutationQueue = OutgoingMutationQueue(stateMachine)
47+
waitForExpectations(timeout: 1)
48+
49+
XCTAssertEqual(stateMachine.state, OutgoingMutationQueue.State.notInitialized)
50+
}
51+
52+
func testStartingState() {
53+
let expect = expectation(description: "state receivedSubscription")
54+
stateMachine.pushExpectActionCriteria { action in
55+
XCTAssertEqual(action, OutgoingMutationQueue.Action.receivedSubscription)
56+
expect.fulfill()
57+
}
58+
59+
stateMachine.state = .starting(apiBehavior, publisher)
60+
waitForExpectations(timeout: 1)
61+
}
62+
63+
func testRequestingEvent_subscriptionSetup() {
64+
let semaphore = DispatchSemaphore(value: 0)
65+
stateMachine.pushExpectActionCriteria { action in
66+
XCTAssertEqual(action, OutgoingMutationQueue.Action.receivedSubscription)
67+
semaphore.signal()
68+
}
69+
stateMachine.state = .starting(apiBehavior, publisher)
70+
semaphore.wait()
71+
72+
let enqueueEvent = expectation(description: "state requestingEvent, enqueueEvent")
73+
let processEvent = expectation(description: "state requestingEvent, processedEvent")
74+
stateMachine.pushExpectActionCriteria { action in
75+
XCTAssertEqual(action, OutgoingMutationQueue.Action.enqueuedEvent)
76+
enqueueEvent.fulfill()
77+
}
78+
stateMachine.pushExpectActionCriteria { action in
79+
XCTAssertEqual(action, OutgoingMutationQueue.Action.processedEvent)
80+
processEvent.fulfill()
81+
}
82+
stateMachine.state = .requestingEvent
83+
84+
waitForExpectations(timeout: 1)
85+
}
86+
87+
func testRequestingEvent_nosubscription() {
88+
let expect = expectation(description: "state requestingEvent, no subscription")
89+
stateMachine.pushExpectActionCriteria { action in
90+
let error = DataStoreError.unknown("_", "", nil)
91+
XCTAssertEqual(action, OutgoingMutationQueue.Action.errored(error))
92+
expect.fulfill()
93+
}
94+
95+
stateMachine.state = .requestingEvent
96+
waitForExpectations(timeout: 1)
97+
}
98+
}
99+
100+
extension OutgoingMutationQueue.State: Equatable {
101+
public static func == (lhs: OutgoingMutationQueue.State, rhs: OutgoingMutationQueue.State) -> Bool {
102+
switch (lhs, rhs) {
103+
case (.notInitialized, notInitialized):
104+
return true
105+
case (.notStarted, .notStarted):
106+
return true
107+
case (.starting, .starting):
108+
return true
109+
case (.requestingEvent, .requestingEvent):
110+
return true
111+
case (.waitingForEventToProcess, .waitingForEventToProcess):
112+
return true
113+
case (.finished, .finished):
114+
return true
115+
case (.inError, .inError):
116+
return true
117+
default:
118+
return false
119+
}
120+
}
121+
}
122+
123+
extension OutgoingMutationQueue.Action: Equatable {
124+
public static func == (lhs: OutgoingMutationQueue.Action, rhs: OutgoingMutationQueue.Action) -> Bool {
125+
switch (lhs, rhs) {
126+
case (.initialized, .initialized):
127+
return true
128+
case (.receivedStart, .receivedStart):
129+
return true
130+
case (.receivedSubscription, .receivedSubscription):
131+
return true
132+
case (.enqueuedEvent, .enqueuedEvent):
133+
return true
134+
case (.processedEvent, .processedEvent):
135+
return true
136+
case (.receivedCancel, .receivedCancel):
137+
return true
138+
case (.errored, .errored):
139+
return true
140+
default:
141+
return false
142+
}
143+
}
144+
}
145+
146+
class MockMutationEventSource: MutationEventSource {
147+
148+
func getNextMutationEvent(completion: @escaping DataStoreCallback<MutationEvent>) {
149+
//TODO: Make generic to handle the error cases
150+
var mutationEvent = MutationEvent(modelId: "1",
151+
modelName: "Post",
152+
json: "{}",
153+
mutationType: MutationEvent.MutationType.create)
154+
completion(.success(mutationEvent))
155+
}
156+
}
157+
158+
extension OutgoingMutationQueueMockStateTest {
159+
private func setUpCore() throws -> AmplifyConfiguration {
160+
Amplify.reset()
161+
162+
let storageEngine = MockStorageEngineBehavior()
163+
let dataStorePublisher = DataStorePublisher()
164+
let dataStorePlugin = AWSDataStorePlugin(modelRegistration: TestModelRegistration(),
165+
storageEngine: storageEngine,
166+
dataStorePublisher: dataStorePublisher)
167+
try Amplify.add(plugin: dataStorePlugin)
168+
let dataStoreConfig = DataStoreCategoryConfiguration(plugins: [
169+
"awsDataStorePlugin": true
170+
])
171+
let amplifyConfig = AmplifyConfiguration(dataStore: dataStoreConfig)
172+
return amplifyConfig
173+
}
174+
private func setUpAPICategory(config: AmplifyConfiguration) throws -> AmplifyConfiguration {
175+
let apiPlugin = MockAPICategoryPlugin()
176+
try Amplify.add(plugin: apiPlugin)
177+
178+
let apiConfig = APICategoryConfiguration(plugins: [
179+
"MockAPICategoryPlugin": true
180+
])
181+
let amplifyConfig = AmplifyConfiguration(api: apiConfig, dataStore: config.dataStore)
182+
return amplifyConfig
183+
}
184+
private func setUpWithAPI() throws {
185+
let configWithoutAPI = try setUpCore()
186+
let configWithAPI = try setUpAPICategory(config: configWithoutAPI)
187+
try Amplify.configure(configWithAPI)
188+
}
189+
}

AmplifyPlugins/DataStore/AWSDataStoreCategoryPluginTests/Sync/ReconcileAndLocalSaveOperationTests.swift

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase {
2121
var anyPostDeletedMutationSync: MutationSync<AnyModel>!
2222

2323
var operation: ReconcileAndLocalSaveOperation!
24-
var stateMachine: MockStateMachine!
24+
var stateMachine: MockStateMachine<ReconcileAndLocalSaveOperation.State, ReconcileAndLocalSaveOperation.Action>!
2525

2626
override func setUp() {
2727
do {
@@ -65,7 +65,7 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase {
6565
func testQuerying() throws {
6666
let expect = expectation(description: "action .queried notified")
6767
storageAdapter.returnOnQueryMutationSync(mutationSync: anyPostMutationSync)
68-
stateMachine.setExpectActionCriteria { action in
68+
stateMachine.pushExpectActionCriteria { action in
6969
XCTAssertEqual(action,
7070
ReconcileAndLocalSaveOperation.Action.queried(self.anyPostMutationSync,
7171
self.anyPostMutationSync))
@@ -93,7 +93,7 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase {
9393
*/
9494
func testQueryingWithInvalidStorageAdapter_error() throws {
9595
let expect = expectation(description: "action .errored nil storage adapter")
96-
stateMachine.setExpectActionCriteria { action in
96+
stateMachine.pushExpectActionCriteria { action in
9797
XCTAssertEqual(action,
9898
ReconcileAndLocalSaveOperation.Action.errored(DataStoreError.nilStorageAdapter()))
9999
expect.fulfill()
@@ -109,7 +109,7 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase {
109109
let expect = expectation(description: "action .errored notified")
110110
let error = DataStoreError.invalidModelName("invalidModelName")
111111
storageAdapter.throwOnQueryMutationSync(error: error)
112-
stateMachine.setExpectActionCriteria { action in
112+
stateMachine.pushExpectActionCriteria { action in
113113
XCTAssertEqual(action, ReconcileAndLocalSaveOperation.Action.errored(error))
114114
expect.fulfill()
115115
}
@@ -122,7 +122,7 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase {
122122
func testQueryingWithEmptyLocalStore() throws {
123123
let expect = expectation(description: "action .queried notified with local data == nil")
124124
storageAdapter.returnOnQueryMutationSync(mutationSync: nil)
125-
stateMachine.setExpectActionCriteria { action in
125+
stateMachine.pushExpectActionCriteria { action in
126126
XCTAssertEqual(action, ReconcileAndLocalSaveOperation.Action.queried(self.anyPostMutationSync, nil))
127127
expect.fulfill()
128128
}
@@ -135,7 +135,7 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase {
135135
func testReconcilingWithoutLocalModel() throws {
136136
let expect = expectation(description: "action .reconciled notified")
137137
let expectedDisposition = RemoteSyncReconciler.Disposition.applyRemoteModel(anyPostMutationSync)
138-
stateMachine.setExpectActionCriteria { action in
138+
stateMachine.pushExpectActionCriteria { action in
139139
XCTAssertEqual(action, ReconcileAndLocalSaveOperation.Action.reconciled(expectedDisposition))
140140
expect.fulfill()
141141
}
@@ -148,7 +148,7 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase {
148148
let expect = expectation(description: "action .execute applyRemoteModel")
149149
let disposition = RemoteSyncReconciler.Disposition.applyRemoteModel(anyPostMutationSync)
150150
storageAdapter.returnOnSave(dataStoreResult: .success(anyPostMutationSync.model))
151-
stateMachine.setExpectActionCriteria { action in
151+
stateMachine.pushExpectActionCriteria { action in
152152
XCTAssertEqual(action, ReconcileAndLocalSaveOperation.Action.applied(self.anyPostMutationSync))
153153
expect.fulfill()
154154
}
@@ -162,7 +162,7 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase {
162162
let disposition = RemoteSyncReconciler.Disposition.applyRemoteModel(anyPostMutationSync)
163163
let error = DataStoreError.invalidModelName("invModelName")
164164
storageAdapter.returnOnSave(dataStoreResult: .failure(error))
165-
stateMachine.setExpectActionCriteria { action in
165+
stateMachine.pushExpectActionCriteria { action in
166166
XCTAssertEqual(action, ReconcileAndLocalSaveOperation.Action.errored(error))
167167
expect.fulfill()
168168
}
@@ -178,7 +178,7 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase {
178178
let error = DataStoreError.invalidModelName("forceError")
179179
storageAdapter.returnOnSave(dataStoreResult: .success(anyPostMutationSync.model))
180180
storageAdapter.shouldReturnErrorOnSaveMetadata = true
181-
stateMachine.setExpectActionCriteria { action in
181+
stateMachine.pushExpectActionCriteria { action in
182182
XCTAssertEqual(action, ReconcileAndLocalSaveOperation.Action.errored(error))
183183
expect.fulfill()
184184
}
@@ -191,7 +191,7 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase {
191191
let expect = expectation(description: "action .execute applyRemoteModel delete success case")
192192
let disposition = RemoteSyncReconciler.Disposition.applyRemoteModel(anyPostDeletedMutationSync)
193193
storageAdapter.returnOnSave(dataStoreResult: .success(anyPostDeletedMutationSync.model))
194-
stateMachine.setExpectActionCriteria { action in
194+
stateMachine.pushExpectActionCriteria { action in
195195
XCTAssertEqual(action, ReconcileAndLocalSaveOperation.Action.applied(self.anyPostDeletedMutationSync))
196196
expect.fulfill()
197197
}
@@ -205,7 +205,7 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase {
205205
let disposition = RemoteSyncReconciler.Disposition.applyRemoteModel(anyPostDeletedMutationSync)
206206
let error = DataStoreError.invalidModelName("DelMutate")
207207
storageAdapter.shouldReturnErrorOnDeleteMutation = true
208-
stateMachine.setExpectActionCriteria { action in
208+
stateMachine.pushExpectActionCriteria { action in
209209
XCTAssertEqual(action, ReconcileAndLocalSaveOperation.Action.errored(error))
210210
expect.fulfill()
211211
}
@@ -221,7 +221,7 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase {
221221
let error = DataStoreError.invalidModelName("forceError")
222222
storageAdapter.shouldReturnErrorOnSaveMetadata = true
223223
storageAdapter.returnOnSave(dataStoreResult: .failure(error))
224-
stateMachine.setExpectActionCriteria { action in
224+
stateMachine.pushExpectActionCriteria { action in
225225
XCTAssertEqual(action, ReconcileAndLocalSaveOperation.Action.errored(error))
226226
expect.fulfill()
227227
}
@@ -233,7 +233,7 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase {
233233
func testExecuteDropRemoteModel() throws {
234234
let expect = expectation(description: "action .execute dropRemoteModel")
235235
let disposition = RemoteSyncReconciler.Disposition.dropRemoteModel
236-
stateMachine.setExpectActionCriteria { action in
236+
stateMachine.pushExpectActionCriteria { action in
237237
XCTAssertEqual(action, ReconcileAndLocalSaveOperation.Action.dropped)
238238
expect.fulfill()
239239
}
@@ -247,7 +247,7 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase {
247247
let expect = expectation(description: "action .execute error")
248248
let error = DataStoreError.invalidModelName("invModelName")
249249
let disposition = RemoteSyncReconciler.Disposition.error(error)
250-
stateMachine.setExpectActionCriteria { action in
250+
stateMachine.pushExpectActionCriteria { action in
251251
XCTAssertEqual(action, ReconcileAndLocalSaveOperation.Action.errored(error))
252252
expect.fulfill()
253253
}
@@ -264,7 +264,7 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase {
264264
XCTAssertEqual(payload.eventName, "DataStore.syncReceived")
265265
hubExpect.fulfill()
266266
}
267-
stateMachine.setExpectActionCriteria { action in
267+
stateMachine.pushExpectActionCriteria { action in
268268
XCTAssertEqual(action, ReconcileAndLocalSaveOperation.Action.notified)
269269
notifyExpect.fulfill()
270270
}
@@ -275,22 +275,6 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase {
275275
Amplify.Hub.removeListener(hubListener)
276276
}
277277
}
278-
279-
class MockStateMachine: StateMachine<ReconcileAndLocalSaveOperation.State, ReconcileAndLocalSaveOperation.Action> {
280-
typealias ExpectActionCriteria = (_ action: ReconcileAndLocalSaveOperation.Action) -> Void
281-
var expectActionCriteria: ExpectActionCriteria?
282-
283-
override func notify(action: ReconcileAndLocalSaveOperation.Action) {
284-
if let expectActionCriteria = expectActionCriteria {
285-
expectActionCriteria(action)
286-
}
287-
}
288-
289-
func setExpectActionCriteria(expectActionCriteria: @escaping ExpectActionCriteria) {
290-
self.expectActionCriteria = expectActionCriteria
291-
}
292-
}
293-
294278
extension ReconcileAndLocalSaveOperation.State: Equatable {
295279
public static func == (lhs: ReconcileAndLocalSaveOperation.State,
296280
rhs: ReconcileAndLocalSaveOperation.State) -> Bool {

0 commit comments

Comments
 (0)