|
| 1 | +// |
| 2 | +// Copyright Amazon.com Inc. or its affiliates. |
| 3 | +// All Rights Reserved. |
| 4 | +// |
| 5 | +// SPDX-License-Identifier: Apache-2.0 |
| 6 | +// |
| 7 | + |
| 8 | +import Foundation |
| 9 | +import XCTest |
| 10 | +import Combine |
| 11 | + |
| 12 | +import AmplifyPlugins |
| 13 | +import AWSDataStoreCategoryPlugin |
| 14 | +import AWSPluginsCore |
| 15 | + |
| 16 | +@testable import Amplify |
| 17 | +@testable import AmplifyTestCommon |
| 18 | + |
| 19 | +class AWSDataStoreMultiAuthBaseTest: XCTestCase { |
| 20 | + var requests: Set<AnyCancellable> = [] |
| 21 | + |
| 22 | + var user1: TestUser? |
| 23 | + var user2: TestUser? |
| 24 | + |
| 25 | + static let amplifyConfigurationFile = "AWSDataStoreCategoryPluginMultiAuthIntegrationTests-amplifyconfiguration" |
| 26 | + static let credentialsFile = "AWSDataStoreCategoryPluginMultiAuthIntegrationTests-credentials" |
| 27 | + |
| 28 | + var authRecorderInterceptor: AuthRecorderInterceptor = AuthRecorderInterceptor() |
| 29 | + |
| 30 | + override func setUp() { |
| 31 | + Amplify.Logging.logLevel = .verbose |
| 32 | + |
| 33 | + do { |
| 34 | + let credentials = try TestConfigHelper.retrieveCredentials(forResource: Self.credentialsFile) |
| 35 | + |
| 36 | + guard let user1 = credentials["user1"], |
| 37 | + let user2 = credentials["user2"], |
| 38 | + let passwordUser1 = credentials["passwordUser1"], |
| 39 | + let passwordUser2 = credentials["passwordUser2"] else { |
| 40 | + XCTFail("Invalid \(Self.credentialsFile).json data") |
| 41 | + return |
| 42 | + } |
| 43 | + |
| 44 | + self.user1 = TestUser(username: user1, password: passwordUser1) |
| 45 | + self.user2 = TestUser(username: user2, password: passwordUser2) |
| 46 | + |
| 47 | + authRecorderInterceptor.reset() |
| 48 | + |
| 49 | + } catch { |
| 50 | + XCTFail("Error during setup: \(error)") |
| 51 | + } |
| 52 | + } |
| 53 | + |
| 54 | + override func tearDownWithError() throws { |
| 55 | + sleep(10) |
| 56 | + Amplify.reset() |
| 57 | + } |
| 58 | + |
| 59 | + // MARK: - Test Helpers |
| 60 | + func makeExpectations() -> MultiAuthTestExpectations { |
| 61 | + MultiAuthTestExpectations( |
| 62 | + subscriptionsEstablished: expectation(description: "Subscription established"), |
| 63 | + modelsSynced: expectation(description: "Model synced"), |
| 64 | + |
| 65 | + query: expectation(description: "Query success"), |
| 66 | + |
| 67 | + mutationSave: expectation(description: "Mutation save success"), |
| 68 | + mutationSaveProcessed: expectation(description: "Mutation save processed"), |
| 69 | + |
| 70 | + mutationDelete: expectation(description: "Mutation delete success"), |
| 71 | + mutationDeleteProcessed: expectation(description: "Mutation delete processed"), |
| 72 | + |
| 73 | + ready: expectation(description: "Ready")) |
| 74 | + } |
| 75 | + |
| 76 | + /// Setup DataStore with given models |
| 77 | + /// - Parameter models: DataStore models |
| 78 | + func setup(withModels models: AmplifyModelRegistration) { |
| 79 | + do { |
| 80 | + let datastoreConfig = DataStoreConfiguration.custom(authModeStrategy: .multiAuth) |
| 81 | + |
| 82 | + try Amplify.add(plugin: AWSDataStorePlugin(modelRegistration: models, |
| 83 | + configuration: datastoreConfig)) |
| 84 | + |
| 85 | + let apiPlugin = AWSAPIPlugin() |
| 86 | + |
| 87 | + try Amplify.add(plugin: apiPlugin) |
| 88 | + try Amplify.add(plugin: AWSCognitoAuthPlugin()) |
| 89 | + |
| 90 | + let amplifyConfig = try TestConfigHelper.retrieveAmplifyConfiguration(forResource: Self.amplifyConfigurationFile) |
| 91 | + try Amplify.configure(amplifyConfig) |
| 92 | + |
| 93 | + // register auth recorder interceptor |
| 94 | + try apiPlugin.add(interceptor: authRecorderInterceptor, for: "datastoreintegtestmu") |
| 95 | + |
| 96 | + signOut() |
| 97 | + clearDataStore() |
| 98 | + } catch { |
| 99 | + XCTFail("Error during setup: \(error)") |
| 100 | + } |
| 101 | + } |
| 102 | + |
| 103 | + func clearDataStore() { |
| 104 | + let semaphore = DispatchSemaphore(value: 0) |
| 105 | + Amplify.DataStore.clear { |
| 106 | + if case let .failure(error) = $0 { |
| 107 | + XCTFail("DataStore clear failed \(error)") |
| 108 | + } |
| 109 | + semaphore.signal() |
| 110 | + } |
| 111 | + semaphore.wait() |
| 112 | + } |
| 113 | +} |
| 114 | + |
| 115 | +// MARK: - Auth helpers |
| 116 | +extension AWSDataStoreMultiAuthBaseTest { |
| 117 | + /// Signin given user |
| 118 | + /// - Parameter user |
| 119 | + func signIn(user: TestUser?) { |
| 120 | + guard let user = user else { |
| 121 | + XCTFail("Invalid user") |
| 122 | + return |
| 123 | + } |
| 124 | + let signInInvoked = expectation(description: "sign in completed") |
| 125 | + _ = Amplify.Auth.signIn(username: user.username, |
| 126 | + password: user.password, options: nil) { result in |
| 127 | + switch result { |
| 128 | + case .failure(let error): |
| 129 | + XCTFail("Signin failure \(error)") |
| 130 | + signInInvoked.fulfill() // won't count as pass |
| 131 | + case .success: |
| 132 | + signInInvoked.fulfill() |
| 133 | + } |
| 134 | + } |
| 135 | + wait(for: [signInInvoked], timeout: TestCommonConstants.networkTimeout) |
| 136 | + } |
| 137 | + |
| 138 | + /// Signout current signed-in user |
| 139 | + func signOut() { |
| 140 | + let signoutInvoked = expectation(description: "sign out completed") |
| 141 | + _ = Amplify.Auth.signOut { result in |
| 142 | + switch result { |
| 143 | + case .failure(let error): |
| 144 | + XCTFail("Signout failure \(error)") |
| 145 | + signoutInvoked.fulfill() // won't count as pass |
| 146 | + |
| 147 | + case .success: |
| 148 | + signoutInvoked.fulfill() |
| 149 | + } |
| 150 | + } |
| 151 | + wait(for: [signoutInvoked], timeout: TestCommonConstants.networkTimeout) |
| 152 | + } |
| 153 | +} |
| 154 | + |
| 155 | +// MARK: - DataStore behavior assert helpers |
| 156 | +extension AWSDataStoreMultiAuthBaseTest { |
| 157 | + /// Asserts that query with given `Model` succeeds |
| 158 | + /// - Parameters: |
| 159 | + /// - modelType: model type |
| 160 | + /// - expectation: success XCTestExpectation |
| 161 | + /// - onFailure: on failure callback |
| 162 | + func assertQuerySuccess<M: Model>(modelType: M.Type, |
| 163 | + _ expectations: MultiAuthTestExpectations, |
| 164 | + onFailure: @escaping (_ error: DataStoreError) -> Void) { |
| 165 | + Amplify.DataStore.query(modelType).sink { |
| 166 | + if case let .failure(error) = $0 { |
| 167 | + onFailure(error) |
| 168 | + } |
| 169 | + } |
| 170 | + receiveValue: { posts in |
| 171 | + XCTAssertNotNil(posts) |
| 172 | + expectations.query.fulfill() |
| 173 | + }.store(in: &requests) |
| 174 | + wait(for: [expectations.query], |
| 175 | + timeout: 60) |
| 176 | + } |
| 177 | + |
| 178 | + /// Asserts that DataStore is in a ready state and subscriptions are established |
| 179 | + /// - Parameter events: DataStore Hub events |
| 180 | + func assertDataStoreReady(_ expectations: MultiAuthTestExpectations, |
| 181 | + expectedModelSynced: Int = 1) { |
| 182 | + var modelSyncedCount = 0 |
| 183 | + let dataStoreEvents = HubPayload.EventName.DataStore.self |
| 184 | + _ = Amplify.Hub.listen(to: .dataStore) { event in |
| 185 | + // subscription fulfilled |
| 186 | + if event.eventName == dataStoreEvents.subscriptionsEstablished { |
| 187 | + expectations.subscriptionsEstablished.fulfill() |
| 188 | + } |
| 189 | + |
| 190 | + // syncQueryReady fulfilled |
| 191 | + if event.eventName == dataStoreEvents.modelSynced { |
| 192 | + modelSyncedCount += 1 |
| 193 | + if modelSyncedCount == expectedModelSynced { |
| 194 | + expectations.modelsSynced.fulfill() |
| 195 | + } |
| 196 | + } |
| 197 | + |
| 198 | + if event.eventName == dataStoreEvents.ready { |
| 199 | + expectations.ready.fulfill() |
| 200 | + } |
| 201 | + } |
| 202 | + Amplify.DataStore.start { _ in } |
| 203 | + wait(for: [expectations.subscriptionsEstablished, |
| 204 | + expectations.modelsSynced, |
| 205 | + expectations.ready], |
| 206 | + timeout: 60) |
| 207 | + } |
| 208 | + |
| 209 | + /// Assert that a save and a delete mutation complete successfully. |
| 210 | + /// - Parameters: |
| 211 | + /// - model: model instance saved and then deleted |
| 212 | + /// - expectations: test expectatinos |
| 213 | + /// - onFailure: failure callback |
| 214 | + func assertMutations<M: Model>(model: M, |
| 215 | + _ expectations: MultiAuthTestExpectations, |
| 216 | + onFailure: @escaping (_ error: DataStoreError) -> Void) { |
| 217 | + _ = Amplify.Hub.listen(to: .dataStore, eventName: HubPayload.EventName.DataStore.syncReceived) { payload in |
| 218 | + guard let mutationEvent = payload.data as? MutationEvent, |
| 219 | + mutationEvent.modelId == model.id else { |
| 220 | + return |
| 221 | + } |
| 222 | + |
| 223 | + if mutationEvent.mutationType == GraphQLMutationType.create.rawValue { |
| 224 | + expectations.mutationSaveProcessed.fulfill() |
| 225 | + return |
| 226 | + } |
| 227 | + |
| 228 | + if mutationEvent.mutationType == GraphQLMutationType.delete.rawValue { |
| 229 | + expectations.mutationDeleteProcessed.fulfill() |
| 230 | + return |
| 231 | + } |
| 232 | + } |
| 233 | + |
| 234 | + Amplify.DataStore.save(model).sink { |
| 235 | + if case let .failure(error) = $0 { |
| 236 | + onFailure(error) |
| 237 | + } |
| 238 | + } |
| 239 | + receiveValue: { posts in |
| 240 | + XCTAssertNotNil(posts) |
| 241 | + expectations.mutationSave.fulfill() |
| 242 | + }.store(in: &requests) |
| 243 | + |
| 244 | + wait(for: [expectations.mutationSave, expectations.mutationSaveProcessed], timeout: 60) |
| 245 | + |
| 246 | + Amplify.DataStore.delete(M.self, withId: model.id).sink { |
| 247 | + if case let .failure(error) = $0 { |
| 248 | + onFailure(error) |
| 249 | + } |
| 250 | + } |
| 251 | + receiveValue: { posts in |
| 252 | + XCTAssertNotNil(posts) |
| 253 | + expectations.mutationDelete.fulfill() |
| 254 | + }.store(in: &requests) |
| 255 | + |
| 256 | + wait(for: [expectations.mutationDelete, expectations.mutationDeleteProcessed], timeout: 60) |
| 257 | + } |
| 258 | + |
| 259 | + /// Asserts that save mutation with given `Model` succeeds |
| 260 | + /// - Parameters: |
| 261 | + /// - model: model type |
| 262 | + /// - expectation: success XCTestExpectation |
| 263 | + /// - onFailure: on failure callback |
| 264 | + func assertMutations<M: Model>(model: M, |
| 265 | + _ expectation: XCTestExpectation, |
| 266 | + onFailure: @escaping (_ error: DataStoreError) -> Void) { |
| 267 | + Amplify.DataStore.save(model).sink { |
| 268 | + if case let .failure(error) = $0 { |
| 269 | + onFailure(error) |
| 270 | + } |
| 271 | + } |
| 272 | + receiveValue: { posts in |
| 273 | + XCTAssertNotNil(posts) |
| 274 | + expectation.fulfill() |
| 275 | + }.store(in: &requests) |
| 276 | + } |
| 277 | + |
| 278 | + func assertUsedAuthTypes(_ authTypes: [AWSAuthorizationType], |
| 279 | + file: StaticString = #file, |
| 280 | + line: UInt = #line) { |
| 281 | + XCTAssertEqual(authRecorderInterceptor.consumedAuthTypes, |
| 282 | + Set(authTypes), |
| 283 | + file: file, |
| 284 | + line: line) |
| 285 | + } |
| 286 | +} |
| 287 | + |
| 288 | +// MARK: - Expectations |
| 289 | +extension AWSDataStoreMultiAuthBaseTest { |
| 290 | + struct MultiAuthTestExpectations { |
| 291 | + var subscriptionsEstablished: XCTestExpectation |
| 292 | + var modelsSynced: XCTestExpectation |
| 293 | + var query: XCTestExpectation |
| 294 | + var mutationSave: XCTestExpectation |
| 295 | + var mutationSaveProcessed: XCTestExpectation |
| 296 | + var mutationDelete: XCTestExpectation |
| 297 | + var mutationDeleteProcessed: XCTestExpectation |
| 298 | + var ready: XCTestExpectation |
| 299 | + var expectations: [XCTestExpectation] { |
| 300 | + return [subscriptionsEstablished, |
| 301 | + modelsSynced, |
| 302 | + query, |
| 303 | + mutationSave, |
| 304 | + mutationSaveProcessed |
| 305 | + ] |
| 306 | + } |
| 307 | + } |
| 308 | +} |
0 commit comments