diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index 61bfba1f3163cf119e610606f9d89ea67a122171..4092af826336f082c259e6f062ba40d86380852b 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -55,6 +55,7 @@ extension AppEnvironment { dbManager: dbManager, mainQueue: mainQueue, bgQueue: bgQueue, + now: Date.init, register: { RegisterEnvironment( messenger: messenger, diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift index faa5757c43a4e22a9c8c3b7dd7a99ee83e059a84..985ef3293ca3abdab70532f0d40f99676a1b29fb 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift @@ -6,8 +6,10 @@ import ContactsFeature import Foundation import RegisterFeature import UserSearchFeature +import XCTestDynamicOverlay import XXClient import XXMessengerClient +import XXModels public struct HomeState: Equatable { public init( @@ -47,6 +49,12 @@ public enum HomeAction: Equatable { case failure(NSError) } + public enum AuthCallbacks: Equatable { + case register + case unregister + case handle(XXClient.AuthCallbacks.Callback) + } + public enum NetworkMonitor: Equatable { case start case stop @@ -62,6 +70,7 @@ public enum HomeAction: Equatable { } case messenger(Messenger) + case authCallbacks(AuthCallbacks) case networkMonitor(NetworkMonitor) case deleteAccount(DeleteAccount) case didDismissAlert @@ -81,6 +90,7 @@ public struct HomeEnvironment { dbManager: DBManager, mainQueue: AnySchedulerOf<DispatchQueue>, bgQueue: AnySchedulerOf<DispatchQueue>, + now: @escaping () -> Date, register: @escaping () -> RegisterEnvironment, contacts: @escaping () -> ContactsEnvironment, userSearch: @escaping () -> UserSearchEnvironment @@ -89,6 +99,7 @@ public struct HomeEnvironment { self.dbManager = dbManager self.mainQueue = mainQueue self.bgQueue = bgQueue + self.now = now self.register = register self.contacts = contacts self.userSearch = userSearch @@ -98,6 +109,7 @@ public struct HomeEnvironment { public var dbManager: DBManager public var mainQueue: AnySchedulerOf<DispatchQueue> public var bgQueue: AnySchedulerOf<DispatchQueue> + public var now: () -> Date public var register: () -> RegisterEnvironment public var contacts: () -> ContactsEnvironment public var userSearch: () -> UserSearchEnvironment @@ -109,6 +121,7 @@ extension HomeEnvironment { dbManager: .unimplemented, mainQueue: .unimplemented, bgQueue: .unimplemented, + now: XCTUnimplemented("\(Self.self).now", placeholder: Date()), register: { .unimplemented }, contacts: { .unimplemented }, userSearch: { .unimplemented } @@ -119,10 +132,12 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> { state, action, env in enum NetworkHealthEffectId {} enum NetworkNodesEffectId {} + enum AuthCallbacksEffectId {} switch action { case .messenger(.start): return .merge( + Effect(value: .authCallbacks(.register)), Effect(value: .networkMonitor(.stop)), Effect.result { do { @@ -160,6 +175,59 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> state.failure = error.localizedDescription return .none + case .authCallbacks(.register): + return Effect.run { subscriber in + let handler = AuthCallbacks { callback in + subscriber.send(.authCallbacks(.handle(callback))) + } + let cancellable = env.messenger.registerAuthCallbacks(handler) + return AnyCancellable { cancellable.cancel() } + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + .cancellable(id: AuthCallbacksEffectId.self, cancelInFlight: true) + + case .authCallbacks(.unregister): + return .cancel(id: AuthCallbacksEffectId.self) + + case .authCallbacks(.handle(.request(let contact, _, _, _))): + return .fireAndForget { + let db = try env.dbManager.getDB() + let contactId = try contact.getId() + guard try db.fetchContacts(.init(id: [contactId])).isEmpty else { + return + } + var dbContact = XXModels.Contact(id: contactId) + dbContact.marshaled = contact.data + dbContact.username = try contact.getFact(.username)?.value + dbContact.email = try contact.getFact(.email)?.value + dbContact.phone = try contact.getFact(.phone)?.value + dbContact.authStatus = .verificationInProgress + dbContact.createdAt = env.now() + dbContact = try db.saveContact(dbContact) + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + + case .authCallbacks(.handle(.confirm(let contact, _, _, _))): + return .fireAndForget { + let db = try env.dbManager.getDB() + let contactId = try contact.getId() + guard var dbContact = try db.fetchContacts(.init(id: [contactId])).first else { + return + } + dbContact.authStatus = .friend + dbContact = try db.saveContact(dbContact) + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + + case .authCallbacks(.handle(.reset(let contact, _, _, _))): + return .none + case .networkMonitor(.start): return .merge( Effect.run { subscriber in diff --git a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift index 89aef4c9f00a7d887f3b1f6bfead0063ade5f263..40cd5a2a7e2fbf4cd586b667d5c2aa9e36392db2 100644 --- a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift +++ b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift @@ -21,6 +21,7 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate + store.environment.messenger.registerAuthCallbacks.run = { _ in Cancellable {} } store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) } store.environment.messenger.isConnected.run = { false } store.environment.messenger.connect.run = { messengerDidConnect += 1 } @@ -32,10 +33,13 @@ final class HomeFeatureTests: XCTestCase { XCTAssertNoDifference(messengerDidStartWithTimeout, [30_000]) XCTAssertNoDifference(messengerDidConnect, 1) + store.receive(.authCallbacks(.register)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.didStartUnregistered)) { $0.register = RegisterState() } + + store.send(.authCallbacks(.unregister)) } func testMessengerStartRegistered() { @@ -51,6 +55,7 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate + store.environment.messenger.registerAuthCallbacks.run = { _ in Cancellable {} } store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) } store.environment.messenger.isConnected.run = { false } store.environment.messenger.connect.run = { messengerDidConnect += 1 } @@ -73,11 +78,14 @@ final class HomeFeatureTests: XCTestCase { XCTAssertNoDifference(messengerDidConnect, 1) XCTAssertNoDifference(messengerDidLogIn, 1) + store.receive(.authCallbacks(.register)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.didStartRegistered)) store.receive(.networkMonitor(.start)) store.send(.networkMonitor(.stop)) + + store.send(.authCallbacks(.unregister)) } func testRegisterFinished() { @@ -94,6 +102,7 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate + store.environment.messenger.registerAuthCallbacks.run = { _ in Cancellable {} } store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) } store.environment.messenger.isConnected.run = { true } store.environment.messenger.isLoggedIn.run = { false } @@ -118,11 +127,14 @@ final class HomeFeatureTests: XCTestCase { XCTAssertNoDifference(messengerDidStartWithTimeout, [30_000]) XCTAssertNoDifference(messengerDidLogIn, 1) + store.receive(.authCallbacks(.register)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.didStartRegistered)) store.receive(.networkMonitor(.start)) store.send(.networkMonitor(.stop)) + + store.send(.authCallbacks(.unregister)) } func testMessengerStartFailure() { @@ -137,14 +149,18 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate + store.environment.messenger.registerAuthCallbacks.run = { _ in Cancellable {} } store.environment.messenger.start.run = { _ in throw error } store.send(.messenger(.start)) + store.receive(.authCallbacks(.register)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.failure(error as NSError))) { $0.failure = error.localizedDescription } + + store.send(.authCallbacks(.unregister)) } func testMessengerStartConnectFailure() { @@ -159,16 +175,20 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate + store.environment.messenger.registerAuthCallbacks.run = { _ in Cancellable {} } store.environment.messenger.start.run = { _ in } store.environment.messenger.isConnected.run = { false } store.environment.messenger.connect.run = { throw error } store.send(.messenger(.start)) + store.receive(.authCallbacks(.register)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.failure(error as NSError))) { $0.failure = error.localizedDescription } + + store.send(.authCallbacks(.unregister)) } func testMessengerStartIsRegisteredFailure() { @@ -183,6 +203,7 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate + store.environment.messenger.registerAuthCallbacks.run = { _ in Cancellable {} } store.environment.messenger.start.run = { _ in } store.environment.messenger.isConnected.run = { true } store.environment.messenger.isLoggedIn.run = { false } @@ -190,10 +211,13 @@ final class HomeFeatureTests: XCTestCase { store.send(.messenger(.start)) + store.receive(.authCallbacks(.register)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.failure(error as NSError))) { $0.failure = error.localizedDescription } + + store.send(.authCallbacks(.unregister)) } func testMessengerStartLogInFailure() { @@ -208,6 +232,7 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate + store.environment.messenger.registerAuthCallbacks.run = { _ in Cancellable {} } store.environment.messenger.start.run = { _ in } store.environment.messenger.isConnected.run = { true } store.environment.messenger.isLoggedIn.run = { false } @@ -216,10 +241,13 @@ final class HomeFeatureTests: XCTestCase { store.send(.messenger(.start)) + store.receive(.authCallbacks(.register)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.failure(error as NSError))) { $0.failure = error.localizedDescription } + + store.send(.authCallbacks(.unregister)) } func testNetworkMonitorStart() { @@ -491,4 +519,109 @@ final class HomeFeatureTests: XCTestCase { $0.contacts = nil } } + + func testAuthCallbacks() { + let store = TestStore( + initialState: HomeState(), + reducer: homeReducer, + environment: .unimplemented + ) + + let now = Date() + var didRegisterCallbacks: [AuthCallbacks] = [] + var didCancelAuthCallbacks = 0 + var dbContact: XXModels.Contact? + var dbDidSaveContact: [XXModels.Contact] = [] + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.now = { now } + store.environment.messenger.registerAuthCallbacks.run = { callbacks in + didRegisterCallbacks.append(callbacks) + return Cancellable { didCancelAuthCallbacks += 1 } + } + store.environment.dbManager.getDB.run = { + var db: Database = .failing + db.fetchContacts.run = { _ in [dbContact].compactMap { $0 } } + db.saveContact.run = { contact in + dbDidSaveContact.append(contact) + return contact + } + return db + } + + store.send(.authCallbacks(.register)) + + XCTAssertNoDifference(didRegisterCallbacks.count, 1) + + var contact = XXClient.Contact.unimplemented("data".data(using: .utf8)!) + contact.getIdFromContact.run = { _ in "id".data(using: .utf8)! } + contact.getFactsFromContact.run = { _ in + [ + Fact(type: .username, value: "username"), + Fact(type: .email, value: "email"), + Fact(type: .phone, value: "phone"), + ] + } + + dbContact = nil + dbDidSaveContact = [] + didRegisterCallbacks.first?.handle( + .request(contact: contact, receptionId: Data(), ephemeralId: 0, roundId: 0) + ) + + store.receive(.authCallbacks(.handle( + .request(contact: contact, receptionId: Data(), ephemeralId: 0, roundId: 0) + ))) + XCTAssertNoDifference(dbDidSaveContact, [.init( + id: "id".data(using: .utf8)!, + marshaled: "data".data(using: .utf8)!, + username: "username", + email: "email", + phone: "phone", + authStatus: .verificationInProgress, + createdAt: now + )]) + + dbContact = .init(id: "id".data(using: .utf8)!) + dbDidSaveContact = [] + didRegisterCallbacks.first?.handle( + .request(contact: contact, receptionId: Data(), ephemeralId: 0, roundId: 0) + ) + + store.receive(.authCallbacks(.handle( + .request(contact: contact, receptionId: Data(), ephemeralId: 0, roundId: 0) + ))) + XCTAssertNoDifference(dbDidSaveContact, []) + + dbContact = .init(id: "id".data(using: .utf8)!) + dbDidSaveContact = [] + didRegisterCallbacks.first?.handle( + .confirm(contact: contact, receptionId: Data(), ephemeralId: 0, roundId: 0) + ) + + store.receive(.authCallbacks(.handle( + .confirm(contact: contact, receptionId: Data(), ephemeralId: 0, roundId: 0) + ))) + XCTAssertNoDifference(dbDidSaveContact, [.init( + id: "id".data(using: .utf8)!, + authStatus: .friend, + createdAt: dbContact!.createdAt + )]) + + dbContact = nil + dbDidSaveContact = [] + didRegisterCallbacks.first?.handle( + .reset(contact: contact, receptionId: Data(), ephemeralId: 0, roundId: 0) + ) + + store.receive(.authCallbacks(.handle( + .reset(contact: contact, receptionId: Data(), ephemeralId: 0, roundId: 0) + ))) + XCTAssertNoDifference(dbDidSaveContact, []) + + store.send(.authCallbacks(.unregister)) + + XCTAssertNoDifference(didCancelAuthCallbacks, 1) + } }