From 8ab5b3b57abe9de0d03bc6b655cd4b85f636e14c Mon Sep 17 00:00:00 2001
From: Dariusz Rybicki <dariusz@elixxir.io>
Date: Mon, 6 Jun 2022 13:14:46 +0200
Subject: [PATCH] Start & stop network follower from SessionView

---
 Example/example-app/Package.swift             |   5 +
 .../example-app/Sources/AppFeature/App.swift  |   4 +-
 .../SessionFeature/SessionFeature.swift       |  72 ++++++++++-
 .../Sources/SessionFeature/SessionView.swift  |  47 ++++++-
 .../SessionFeatureTests.swift                 | 118 +++++++++++++++++-
 5 files changed, 235 insertions(+), 11 deletions(-)

diff --git a/Example/example-app/Package.swift b/Example/example-app/Package.swift
index 6277f619..55d54626 100644
--- a/Example/example-app/Package.swift
+++ b/Example/example-app/Package.swift
@@ -135,10 +135,15 @@ let package = Package(
     .target(
       name: "SessionFeature",
       dependencies: [
+        .target(name: "ErrorFeature"),
         .product(
           name: "ComposableArchitecture",
           package: "swift-composable-architecture"
         ),
+        .product(
+          name: "ComposablePresentation",
+          package: "swift-composable-presentation"
+        ),
         .product(
           name: "ElixxirDAppsSDK",
           package: "elixxir-dapps-sdk-swift"
diff --git a/Example/example-app/Sources/AppFeature/App.swift b/Example/example-app/Sources/AppFeature/App.swift
index ed136fb1..341cb46c 100644
--- a/Example/example-app/Sources/AppFeature/App.swift
+++ b/Example/example-app/Sources/AppFeature/App.swift
@@ -42,7 +42,9 @@ extension AppEnvironment {
         error: ErrorEnvironment()
       ),
       session: SessionEnvironment(
-        getClient: { clientSubject.value }
+        getClient: { clientSubject.value },
+        bgScheduler: bgScheduler,
+        mainScheduler: mainScheduler
       )
     )
   }
diff --git a/Example/example-app/Sources/SessionFeature/SessionFeature.swift b/Example/example-app/Sources/SessionFeature/SessionFeature.swift
index 2e0d9c96..b07a14cc 100644
--- a/Example/example-app/Sources/SessionFeature/SessionFeature.swift
+++ b/Example/example-app/Sources/SessionFeature/SessionFeature.swift
@@ -1,34 +1,98 @@
+import Combine
 import ComposableArchitecture
 import ElixxirDAppsSDK
+import ErrorFeature
 
 public struct SessionState: Equatable {
   public init(
-    id: UUID
+    id: UUID,
+    networkFollowerStatus: NetworkFollowerStatus? = nil,
+    error: ErrorState? = nil
   ) {
     self.id = id
+    self.networkFollowerStatus = networkFollowerStatus
+    self.error = error
   }
 
   public var id: UUID
+  public var networkFollowerStatus: NetworkFollowerStatus?
+  public var error: ErrorState?
 }
 
 public enum SessionAction: Equatable {
   case viewDidLoad
+  case updateNetworkFollowerStatus
+  case didUpdateNetworkFollowerStatus(NetworkFollowerStatus?)
+  case runNetworkFollower(Bool)
+  case networkFollowerDidFail(NSError)
+  case error(ErrorAction)
+  case didDismissError
 }
 
 public struct SessionEnvironment {
   public init(
-    getClient: @escaping () -> Client?
+    getClient: @escaping () -> Client?,
+    bgScheduler: AnySchedulerOf<DispatchQueue>,
+    mainScheduler: AnySchedulerOf<DispatchQueue>
   ) {
     self.getClient = getClient
+    self.bgScheduler = bgScheduler
+    self.mainScheduler = mainScheduler
   }
 
   public var getClient: () -> Client?
+  public var bgScheduler: AnySchedulerOf<DispatchQueue>
+  public var mainScheduler: AnySchedulerOf<DispatchQueue>
 }
 
 public let sessionReducer = Reducer<SessionState, SessionAction, SessionEnvironment>
 { state, action, env in
   switch action {
   case .viewDidLoad:
+    return .merge([
+      .init(value: .updateNetworkFollowerStatus),
+    ])
+
+  case .updateNetworkFollowerStatus:
+    return Effect.future { fulfill in
+      let status = env.getClient()?.networkFollower.status()
+      fulfill(.success(.didUpdateNetworkFollowerStatus(status)))
+    }
+    .subscribe(on: env.bgScheduler)
+    .receive(on: env.mainScheduler)
+    .eraseToEffect()
+
+  case .didUpdateNetworkFollowerStatus(let status):
+    state.networkFollowerStatus = status
+    return .none
+
+  case .runNetworkFollower(let start):
+    state.networkFollowerStatus = start ? .starting : .stopping
+    return Effect.run { subscriber in
+      do {
+        if start {
+          try env.getClient()?.networkFollower.start(timeoutMS: 30_000)
+        } else {
+          try env.getClient()?.networkFollower.stop()
+        }
+      } catch {
+        subscriber.send(.networkFollowerDidFail(error as NSError))
+      }
+      let status = env.getClient()?.networkFollower.status()
+      subscriber.send(.didUpdateNetworkFollowerStatus(status))
+      subscriber.send(completion: .finished)
+      return AnyCancellable {}
+    }
+    .subscribe(on: env.bgScheduler)
+    .receive(on: env.mainScheduler)
+    .eraseToEffect()
+
+  case .networkFollowerDidFail(let error):
+    state.error = ErrorState(error: error)
+    return .none
+
+  case .didDismissError:
+    state.error = nil
     return .none
   }
 }
@@ -36,7 +100,9 @@ public let sessionReducer = Reducer<SessionState, SessionAction, SessionEnvironm
 #if DEBUG
 extension SessionEnvironment {
   public static let failing = SessionEnvironment(
-    getClient: { .failing }
+    getClient: { .failing },
+    bgScheduler: .failing,
+    mainScheduler: .failing
   )
 }
 #endif
diff --git a/Example/example-app/Sources/SessionFeature/SessionView.swift b/Example/example-app/Sources/SessionFeature/SessionView.swift
index cbf8934e..74b4d130 100644
--- a/Example/example-app/Sources/SessionFeature/SessionView.swift
+++ b/Example/example-app/Sources/SessionFeature/SessionView.swift
@@ -1,4 +1,7 @@
 import ComposableArchitecture
+import ComposablePresentation
+import ElixxirDAppsSDK
+import ErrorFeature
 import SwiftUI
 
 public struct SessionView: View {
@@ -9,16 +12,50 @@ public struct SessionView: View {
   let store: Store<SessionState, SessionAction>
 
   struct ViewState: Equatable {
-    init(state: SessionState) {}
+    let networkFollowerStatus: NetworkFollowerStatus?
+
+    init(state: SessionState) {
+      networkFollowerStatus = state.networkFollowerStatus
+    }
   }
 
   public var body: some View {
     WithViewStore(store.scope(state: ViewState.init)) { viewStore in
-      Text("SessionView")
-        .navigationTitle("Session")
-        .task {
-          viewStore.send(.viewDidLoad)
+      Form {
+        Section {
+          NetworkFollowerStatusView(status: viewStore.networkFollowerStatus)
+
+          Button {
+            viewStore.send(.runNetworkFollower(true))
+          } label: {
+            Text("Start")
+          }
+          .disabled(viewStore.networkFollowerStatus != .stopped)
+
+          Button {
+            viewStore.send(.runNetworkFollower(false))
+          } label: {
+            Text("Stop")
+          }
+          .disabled(viewStore.networkFollowerStatus != .running)
+        } header: {
+          Text("Network follower")
         }
+      }
+      .navigationTitle("Session")
+      .task {
+        viewStore.send(.viewDidLoad)
+      }
+      .sheet(
+        store.scope(
+          state: \.error,
+          action: SessionAction.error
+        ),
+        onDismiss: {
+          viewStore.send(.didDismissError)
+        },
+        content: ErrorView.init(store:)
+      )
     }
   }
 }
diff --git a/Example/example-app/Tests/SessionFeatureTests/SessionFeatureTests.swift b/Example/example-app/Tests/SessionFeatureTests/SessionFeatureTests.swift
index 1e13e0d1..092573ed 100644
--- a/Example/example-app/Tests/SessionFeatureTests/SessionFeatureTests.swift
+++ b/Example/example-app/Tests/SessionFeatureTests/SessionFeatureTests.swift
@@ -1,15 +1,129 @@
 import ComposableArchitecture
+import ElixxirDAppsSDK
+import ErrorFeature
 import XCTest
 @testable import SessionFeature
 
 final class SessionFeatureTests: XCTestCase {
-  func testViewDidLoad() throws {
+  func testViewDidLoad() {
+    var networkFollowerStatus: NetworkFollowerStatus!
+    let bgScheduler = DispatchQueue.test
+    let mainScheduler = DispatchQueue.test
+
+    var env = SessionEnvironment.failing
+    env.getClient = {
+      var client = Client.failing
+      client.networkFollower.status.status = { networkFollowerStatus }
+      return client
+    }
+    env.bgScheduler = bgScheduler.eraseToAnyScheduler()
+    env.mainScheduler = mainScheduler.eraseToAnyScheduler()
+
     let store = TestStore(
       initialState: SessionState(id: UUID()),
       reducer: sessionReducer,
-      environment: .failing
+      environment: env
     )
 
     store.send(.viewDidLoad)
+
+    store.receive(.updateNetworkFollowerStatus)
+
+    networkFollowerStatus = .stopped
+    bgScheduler.advance()
+    mainScheduler.advance()
+
+    store.receive(.didUpdateNetworkFollowerStatus(.stopped)) {
+      $0.networkFollowerStatus = .stopped
+    }
+  }
+
+  func testStartStopNetworkFollower() {
+    var networkFollowerStatus: NetworkFollowerStatus!
+    var didStartNetworkFollowerWithTimeout = [Int]()
+    var didStopNetworkFollower = 0
+    var networkFollowerStartError: NSError?
+    let bgScheduler = DispatchQueue.test
+    let mainScheduler = DispatchQueue.test
+
+    var env = SessionEnvironment.failing
+    env.getClient = {
+      var client = Client.failing
+      client.networkFollower.status.status = {
+        networkFollowerStatus
+      }
+      client.networkFollower.start.start = {
+        didStartNetworkFollowerWithTimeout.append($0)
+        if let error = networkFollowerStartError {
+          throw error
+        }
+      }
+      client.networkFollower.stop.stop = {
+        didStopNetworkFollower += 1
+      }
+      return client
+    }
+    env.bgScheduler = bgScheduler.eraseToAnyScheduler()
+    env.mainScheduler = mainScheduler.eraseToAnyScheduler()
+
+    let store = TestStore(
+      initialState: SessionState(id: UUID()),
+      reducer: sessionReducer,
+      environment: env
+    )
+
+    store.send(.runNetworkFollower(true)) {
+      $0.networkFollowerStatus = .starting
+    }
+
+    networkFollowerStatus = .running
+    bgScheduler.advance()
+    mainScheduler.advance()
+
+    XCTAssertEqual(didStartNetworkFollowerWithTimeout, [30_000])
+    XCTAssertEqual(didStopNetworkFollower, 0)
+
+    store.receive(.didUpdateNetworkFollowerStatus(.running)) {
+      $0.networkFollowerStatus = .running
+    }
+
+    store.send(.runNetworkFollower(false)) {
+      $0.networkFollowerStatus = .stopping
+    }
+
+    networkFollowerStatus = .stopped
+    bgScheduler.advance()
+    mainScheduler.advance()
+
+    XCTAssertEqual(didStartNetworkFollowerWithTimeout, [30_000])
+    XCTAssertEqual(didStopNetworkFollower, 1)
+
+    store.receive(.didUpdateNetworkFollowerStatus(.stopped)) {
+      $0.networkFollowerStatus = .stopped
+    }
+
+    store.send(.runNetworkFollower(true)) {
+      $0.networkFollowerStatus = .starting
+    }
+
+    networkFollowerStartError = NSError(domain: "test", code: 1234)
+    networkFollowerStatus = .stopped
+    bgScheduler.advance()
+    mainScheduler.advance()
+
+    XCTAssertEqual(didStartNetworkFollowerWithTimeout, [30_000, 30_000])
+    XCTAssertEqual(didStopNetworkFollower, 1)
+
+    store.receive(.networkFollowerDidFail(networkFollowerStartError!)) {
+      $0.error = ErrorState(error: networkFollowerStartError!)
+    }
+
+    store.receive(.didUpdateNetworkFollowerStatus(.stopped)) {
+      $0.networkFollowerStatus = .stopped
+    }
+
+    store.send(.didDismissError) {
+      $0.error = nil
+    }
   }
 }
-- 
GitLab