From bf618cfd9020fac7043ed467f44c9b22357352b6 Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Thu, 22 Sep 2022 23:37:00 +0200
Subject: [PATCH] Use AuthHandler and MessageListener in AppFeature

---
 .../AppFeature/AppEnvironment+Live.swift      |   5 +
 .../Sources/AppFeature/AppFeature.swift       |  44 ++-
 .../AppFeatureTests/AppFeatureTests.swift     | 303 ++++++++++++++----
 3 files changed, 270 insertions(+), 82 deletions(-)

diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift
index e00d9d82..77da4aec 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 43cede69..9424cbf2 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 fc58fe4e..93d71903 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
 }
-- 
GitLab