diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index e00d9d8231fd35509eabc64d79c22f2de715646b..77da4aecdd7c8563f71351e385660104b93b9405 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -85,6 +85,11 @@ extension AppEnvironment { return AppEnvironment( dbManager: dbManager, messenger: messenger, + authHandler: authHandler, + messageListener: .live( + messenger: messenger, + db: dbManager.getDB + ), mainQueue: mainQueue, bgQueue: bgQueue, welcome: { diff --git a/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift b/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift index 43cede697423359094d9b0186350abbc01e79d2e..9424cbf2991248fa6369fb44a2a23982ff6fe8d8 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift @@ -6,6 +6,7 @@ import Foundation import HomeFeature import RestoreFeature import WelcomeFeature +import XXClient import XXMessengerClient struct AppState: Equatable { @@ -37,6 +38,7 @@ extension AppState.Screen { enum AppAction: Equatable, BindableAction { case start + case stop case binding(BindingAction<AppState>) case welcome(WelcomeAction) case restore(RestoreAction) @@ -46,6 +48,8 @@ enum AppAction: Equatable, BindableAction { struct AppEnvironment { var dbManager: DBManager var messenger: Messenger + var authHandler: AuthCallbackHandler + var messageListener: MessageListenerHandler var mainQueue: AnySchedulerOf<DispatchQueue> var bgQueue: AnySchedulerOf<DispatchQueue> var welcome: () -> WelcomeEnvironment @@ -57,6 +61,8 @@ extension AppEnvironment { static let unimplemented = AppEnvironment( dbManager: .unimplemented, messenger: .unimplemented, + authHandler: .unimplemented, + messageListener: .unimplemented, mainQueue: .unimplemented, bgQueue: .unimplemented, welcome: { .unimplemented }, @@ -67,34 +73,50 @@ extension AppEnvironment { let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, env in + enum EffectId {} + switch action { case .start, .welcome(.finished), .restore(.finished), .home(.deleteAccount(.success)): state.screen = .loading - return .run { subscriber in + return Effect.run { subscriber in + var cancellables: [XXClient.Cancellable] = [] + do { if env.dbManager.hasDB() == false { try env.dbManager.makeDB() } - if env.messenger.isLoaded() == false { - if env.messenger.isCreated() == false { - subscriber.send(.set(\.$screen, .welcome(WelcomeState()))) - subscriber.send(completion: .finished) - return AnyCancellable {} - } + cancellables.append(env.authHandler(onError: { error in + // TODO: handle error + })) + cancellables.append(env.messageListener(onError: { error in + // TODO: handle error + })) + + let isLoaded = env.messenger.isLoaded() + let isCreated = env.messenger.isCreated() + + if !isLoaded, !isCreated { + subscriber.send(.set(\.$screen, .welcome(WelcomeState()))) + } else if !isLoaded { try env.messenger.load() + subscriber.send(.set(\.$screen, .home(HomeState()))) + } else { + subscriber.send(.set(\.$screen, .home(HomeState()))) } - - subscriber.send(.set(\.$screen, .home(HomeState()))) } catch { subscriber.send(.set(\.$screen, .failure(error.localizedDescription))) } - subscriber.send(completion: .finished) - return AnyCancellable {} + + return AnyCancellable { cancellables.forEach { $0.cancel() } } } .subscribe(on: env.bgQueue) .receive(on: env.mainQueue) .eraseToEffect() + .cancellable(id: EffectId.self, cancelInFlight: true) + + case .stop: + return .cancel(id: EffectId.self) case .welcome(.restoreTapped): state.screen = .restore(RestoreState()) diff --git a/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift b/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift index fc58fe4eaf2ec4fa8be79df08205638c640d656b..93d719030e6bd5554465078f957f1e0412c3da76 100644 --- a/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift +++ b/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift @@ -1,78 +1,103 @@ +import AppCore import ComposableArchitecture import CustomDump import HomeFeature import RestoreFeature import WelcomeFeature import XCTest +import XXClient @testable import AppFeature final class AppFeatureTests: XCTestCase { func testStartWithoutMessengerCreated() { + var actions: [Action] = [] + let store = TestStore( initialState: AppState(), reducer: appReducer, environment: .unimplemented ) - let mainQueue = DispatchQueue.test - let bgQueue = DispatchQueue.test - var didMakeDB = 0 - - store.environment.mainQueue = mainQueue.eraseToAnyScheduler() - store.environment.bgQueue = bgQueue.eraseToAnyScheduler() + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate store.environment.dbManager.hasDB.run = { false } - store.environment.dbManager.makeDB.run = { didMakeDB += 1 } store.environment.messenger.isLoaded.run = { false } store.environment.messenger.isCreated.run = { false } + store.environment.dbManager.makeDB.run = { + actions.append(.didMakeDB) + } + store.environment.authHandler.run = { _ in + actions.append(.didStartAuthHandler) + return Cancellable {} + } + store.environment.messageListener.run = { _ in + actions.append(.didStartMessageListener) + return Cancellable {} + } store.send(.start) - bgQueue.advance() - - XCTAssertNoDifference(didMakeDB, 1) - - mainQueue.advance() - store.receive(.set(\.$screen, .welcome(WelcomeState()))) { $0.screen = .welcome(WelcomeState()) } + + XCTAssertNoDifference(actions, [ + .didMakeDB, + .didStartAuthHandler, + .didStartMessageListener, + ]) + + store.send(.stop) } func testStartWithMessengerCreated() { + var actions: [Action] = [] + let store = TestStore( initialState: AppState(), reducer: appReducer, environment: .unimplemented ) - let mainQueue = DispatchQueue.test - let bgQueue = DispatchQueue.test - var didMakeDB = 0 - var messengerDidLoad = 0 - - store.environment.mainQueue = mainQueue.eraseToAnyScheduler() - store.environment.bgQueue = bgQueue.eraseToAnyScheduler() + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate store.environment.dbManager.hasDB.run = { false } - store.environment.dbManager.makeDB.run = { didMakeDB += 1 } store.environment.messenger.isLoaded.run = { false } store.environment.messenger.isCreated.run = { true } - store.environment.messenger.load.run = { messengerDidLoad += 1 } + store.environment.dbManager.makeDB.run = { + actions.append(.didMakeDB) + } + store.environment.messenger.load.run = { + actions.append(.didLoadMessenger) + } + store.environment.authHandler.run = { _ in + actions.append(.didStartAuthHandler) + return Cancellable {} + } + store.environment.messageListener.run = { _ in + actions.append(.didStartMessageListener) + return Cancellable {} + } store.send(.start) - bgQueue.advance() - - XCTAssertNoDifference(didMakeDB, 1) - XCTAssertNoDifference(messengerDidLoad, 1) - - mainQueue.advance() - store.receive(.set(\.$screen, .home(HomeState()))) { $0.screen = .home(HomeState()) } + + XCTAssertNoDifference(actions, [ + .didMakeDB, + .didStartAuthHandler, + .didStartMessageListener, + .didLoadMessenger, + ]) + + store.send(.stop) } func testWelcomeFinished() { + var actions: [Action] = [] + let store = TestStore( initialState: AppState( screen: .welcome(WelcomeState()) @@ -81,33 +106,43 @@ final class AppFeatureTests: XCTestCase { environment: .unimplemented ) - let mainQueue = DispatchQueue.test - let bgQueue = DispatchQueue.test - var messengerDidLoad = 0 - - store.environment.mainQueue = mainQueue.eraseToAnyScheduler() - store.environment.bgQueue = bgQueue.eraseToAnyScheduler() + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate store.environment.dbManager.hasDB.run = { true } store.environment.messenger.isLoaded.run = { false } store.environment.messenger.isCreated.run = { true } - store.environment.messenger.load.run = { messengerDidLoad += 1 } + store.environment.messenger.load.run = { + actions.append(.didLoadMessenger) + } + store.environment.authHandler.run = { _ in + actions.append(.didStartAuthHandler) + return Cancellable {} + } + store.environment.messageListener.run = { _ in + actions.append(.didStartMessageListener) + return Cancellable {} + } store.send(.welcome(.finished)) { $0.screen = .loading } - bgQueue.advance() - - XCTAssertNoDifference(messengerDidLoad, 1) - - mainQueue.advance() - store.receive(.set(\.$screen, .home(HomeState()))) { $0.screen = .home(HomeState()) } + + XCTAssertNoDifference(actions, [ + .didStartAuthHandler, + .didStartMessageListener, + .didLoadMessenger, + ]) + + store.send(.stop) } func testRestoreFinished() { + var actions: [Action] = [] + let store = TestStore( initialState: AppState( screen: .restore(RestoreState()) @@ -116,33 +151,43 @@ final class AppFeatureTests: XCTestCase { environment: .unimplemented ) - let mainQueue = DispatchQueue.test - let bgQueue = DispatchQueue.test - var messengerDidLoad = 0 - - store.environment.mainQueue = mainQueue.eraseToAnyScheduler() - store.environment.bgQueue = bgQueue.eraseToAnyScheduler() + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate store.environment.dbManager.hasDB.run = { true } store.environment.messenger.isLoaded.run = { false } store.environment.messenger.isCreated.run = { true } - store.environment.messenger.load.run = { messengerDidLoad += 1 } + store.environment.messenger.load.run = { + actions.append(.didLoadMessenger) + } + store.environment.authHandler.run = { _ in + actions.append(.didStartAuthHandler) + return Cancellable {} + } + store.environment.messageListener.run = { _ in + actions.append(.didStartMessageListener) + return Cancellable {} + } store.send(.restore(.finished)) { $0.screen = .loading } - bgQueue.advance() - - XCTAssertNoDifference(messengerDidLoad, 1) - - mainQueue.advance() - store.receive(.set(\.$screen, .home(HomeState()))) { $0.screen = .home(HomeState()) } + + XCTAssertNoDifference(actions, [ + .didStartAuthHandler, + .didStartMessageListener, + .didLoadMessenger, + ]) + + store.send(.stop) } func testHomeDidDeleteAccount() { + var actions: [Action] = [] + let store = TestStore( initialState: AppState( screen: .home(HomeState()) @@ -151,25 +196,34 @@ final class AppFeatureTests: XCTestCase { environment: .unimplemented ) - let mainQueue = DispatchQueue.test - let bgQueue = DispatchQueue.test - - store.environment.mainQueue = mainQueue.eraseToAnyScheduler() - store.environment.bgQueue = bgQueue.eraseToAnyScheduler() + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate store.environment.dbManager.hasDB.run = { true } store.environment.messenger.isLoaded.run = { false } store.environment.messenger.isCreated.run = { false } + store.environment.authHandler.run = { _ in + actions.append(.didStartAuthHandler) + return Cancellable {} + } + store.environment.messageListener.run = { _ in + actions.append(.didStartMessageListener) + return Cancellable {} + } store.send(.home(.deleteAccount(.success))) { $0.screen = .loading } - bgQueue.advance() - mainQueue.advance() - store.receive(.set(\.$screen, .welcome(WelcomeState()))) { $0.screen = .welcome(WelcomeState()) } + + XCTAssertNoDifference(actions, [ + .didStartAuthHandler, + .didStartMessageListener, + ]) + + store.send(.stop) } func testWelcomeRestoreTapped() { @@ -187,6 +241,8 @@ final class AppFeatureTests: XCTestCase { } func testWelcomeFailed() { + let failure = "Something went wrong" + let store = TestStore( initialState: AppState( screen: .welcome(WelcomeState()) @@ -195,23 +251,21 @@ final class AppFeatureTests: XCTestCase { environment: .unimplemented ) - let failure = "Something went wrong" - store.send(.welcome(.failed(failure))) { $0.screen = .failure(failure) } } func testStartDatabaseMakeFailure() { + struct Failure: Error {} + let error = Failure() + let store = TestStore( initialState: AppState(), reducer: appReducer, environment: .unimplemented ) - struct Failure: Error {} - let error = Failure() - store.environment.mainQueue = .immediate store.environment.bgQueue = .immediate store.environment.dbManager.hasDB.run = { false } @@ -222,29 +276,136 @@ final class AppFeatureTests: XCTestCase { store.receive(.set(\.$screen, .failure(error.localizedDescription))) { $0.screen = .failure(error.localizedDescription) } + + store.send(.stop) } func testStartMessengerLoadFailure() { + struct Failure: Error {} + let error = Failure() + + var actions: [Action] = [] + let store = TestStore( initialState: AppState(), reducer: appReducer, environment: .unimplemented ) - struct Failure: Error {} - let error = Failure() - store.environment.mainQueue = .immediate store.environment.bgQueue = .immediate store.environment.dbManager.hasDB.run = { true } store.environment.messenger.isLoaded.run = { false } store.environment.messenger.isCreated.run = { true } store.environment.messenger.load.run = { throw error } + store.environment.authHandler.run = { _ in + actions.append(.didStartAuthHandler) + return Cancellable {} + } + store.environment.messageListener.run = { _ in + actions.append(.didStartMessageListener) + return Cancellable {} + } store.send(.start) store.receive(.set(\.$screen, .failure(error.localizedDescription))) { $0.screen = .failure(error.localizedDescription) } + + XCTAssertNoDifference(actions, [ + .didStartAuthHandler, + .didStartMessageListener, + ]) + + store.send(.stop) } + + func testStartHandlersAndListeners() { + var actions: [Action] = [] + var authHandlerOnError: [AuthCallbackHandler.OnError] = [] + var messageListenerOnError: [MessageListenerHandler.OnError] = [] + + let store = TestStore( + initialState: AppState(), + reducer: appReducer, + environment: .unimplemented + ) + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.dbManager.hasDB.run = { true } + store.environment.messenger.isLoaded.run = { true } + store.environment.messenger.isCreated.run = { true } + store.environment.authHandler.run = { onError in + authHandlerOnError.append(onError) + actions.append(.didStartAuthHandler) + return Cancellable { + actions.append(.didCancelAuthHandler) + } + } + store.environment.messageListener.run = { onError in + messageListenerOnError.append(onError) + actions.append(.didStartMessageListener) + return Cancellable { + actions.append(.didCancelMessageListener) + } + } + + store.send(.start) + + store.receive(.set(\.$screen, .home(HomeState()))) { + $0.screen = .home(HomeState()) + } + + XCTAssertNoDifference(actions, [ + .didStartAuthHandler, + .didStartMessageListener, + ]) + actions = [] + + store.send(.start) { + $0.screen = .loading + } + + store.receive(.set(\.$screen, .home(HomeState()))) { + $0.screen = .home(HomeState()) + } + + XCTAssertNoDifference(actions, [ + .didCancelAuthHandler, + .didCancelMessageListener, + .didStartAuthHandler, + .didStartMessageListener, + ]) + actions = [] + + struct AuthError: Error {} + authHandlerOnError.first?(AuthError()) + + XCTAssertNoDifference(actions, []) + actions = [] + + struct MessageError: Error {} + messageListenerOnError.first?(MessageError()) + + XCTAssertNoDifference(actions, []) + actions = [] + + store.send(.stop) + + XCTAssertNoDifference(actions, [ + .didCancelAuthHandler, + .didCancelMessageListener, + ]) + } +} + +private enum Action: Equatable { + case didMakeDB + case didStartAuthHandler + case didStartMessageListener + case didLoadMessenger + case didCancelAuthHandler + case didCancelMessageListener }