diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index c0a4f9054a911c82a801544c2ff17e4629c0ecfb..34d2f4657375e1a4baf78a7811a96a64d2490571 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -219,6 +219,7 @@ let package = Package( .target(name: "UserSearchFeature"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .product(name: "ComposablePresentation", package: "swift-composable-presentation"), + .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), ], swiftSettings: swiftSettings @@ -273,6 +274,7 @@ let package = Package( name: "RestoreFeature", dependencies: [ .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), ], swiftSettings: swiftSettings ), diff --git a/Examples/xx-messenger/Sources/AppCore/DBManager/DBManager.swift b/Examples/xx-messenger/Sources/AppCore/DBManager/DBManager.swift index f591dc1c2d37c3f14cf1a47905ccee2e3379fad1..beec52f86eeb3574b5da410923bc10ee3a9405a9 100644 --- a/Examples/xx-messenger/Sources/AppCore/DBManager/DBManager.swift +++ b/Examples/xx-messenger/Sources/AppCore/DBManager/DBManager.swift @@ -1,3 +1,4 @@ +import Foundation import XXModels public struct DBManager { @@ -8,7 +9,12 @@ public struct DBManager { } extension DBManager { - public static func live() -> DBManager { + public static func live( + url: URL = FileManager.default + .urls(for: .applicationSupportDirectory, in: .userDomainMask) + .first! + .appendingPathComponent("database") + ) -> DBManager { class Container { var db: Database? } @@ -17,9 +23,9 @@ extension DBManager { return DBManager( hasDB: .init { container.db != nil }, - makeDB: .live(setDB: { container.db = $0 }), + makeDB: .live(url: url, setDB: { container.db = $0 }), getDB: .live(getDB: { container.db }), - removeDB: .live(getDB: { container.db }, unsetDB: { container.db = nil }) + removeDB: .live(url: url, getDB: { container.db }, unsetDB: { container.db = nil }) ) } } diff --git a/Examples/xx-messenger/Sources/AppCore/DBManager/DBManagerMakeDB.swift b/Examples/xx-messenger/Sources/AppCore/DBManager/DBManagerMakeDB.swift index a3f018b1d617ad402a4c5f8914b07a2c622150ac..d7e5e93db530327715ce72cc7cae602fb1802c8c 100644 --- a/Examples/xx-messenger/Sources/AppCore/DBManager/DBManagerMakeDB.swift +++ b/Examples/xx-messenger/Sources/AppCore/DBManager/DBManagerMakeDB.swift @@ -13,18 +13,14 @@ public struct DBManagerMakeDB { extension DBManagerMakeDB { public static func live( + url: URL, setDB: @escaping (Database) -> Void ) -> DBManagerMakeDB { DBManagerMakeDB { - let dbDirectoryURL = FileManager.default - .urls(for: .applicationSupportDirectory, in: .userDomainMask) - .first! - .appendingPathComponent("database") - try? FileManager.default - .createDirectory(at: dbDirectoryURL, withIntermediateDirectories: true) + .createDirectory(at: url, withIntermediateDirectories: true) - let dbFilePath = dbDirectoryURL + let dbFilePath = url .appendingPathComponent("db") .appendingPathExtension("sqlite") .path diff --git a/Examples/xx-messenger/Sources/AppCore/DBManager/DBManagerRemoveDB.swift b/Examples/xx-messenger/Sources/AppCore/DBManager/DBManagerRemoveDB.swift index 69ab6e020d1ae782b048c82d75fb667eb3d7b985..557a1a53cfaa5902f6a51e2db5189285bc65ec4a 100644 --- a/Examples/xx-messenger/Sources/AppCore/DBManager/DBManagerRemoveDB.swift +++ b/Examples/xx-messenger/Sources/AppCore/DBManager/DBManagerRemoveDB.swift @@ -13,12 +13,18 @@ public struct DBManagerRemoveDB { extension DBManagerRemoveDB { public static func live( + url: URL, getDB: @escaping () -> Database?, unsetDB: @escaping () -> Void ) -> DBManagerRemoveDB { DBManagerRemoveDB { - try getDB()?.drop() + let db = getDB() unsetDB() + try db?.drop() + let fm = FileManager.default + if fm.fileExists(atPath: url.path) { + try fm.removeItem(atPath: url.path) + } } } } diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index d824d6d400b402270d341bb43fbd5f363c84a879..3c75f40afb41be86846967f43d2b8fb0f8462b8c 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: { @@ -95,17 +100,17 @@ extension AppEnvironment { ) }, restore: { - RestoreEnvironment() + RestoreEnvironment( + messenger: messenger, + loadData: .live, + mainQueue: mainQueue, + bgQueue: bgQueue + ) }, home: { HomeEnvironment( messenger: messenger, dbManager: dbManager, - authHandler: authHandler, - messageListener: .live( - messenger: messenger, - db: dbManager.getDB - ), mainQueue: mainQueue, bgQueue: bgQueue, register: { 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/Sources/HomeFeature/HomeFeature.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift index 894c5aca32ca1170d0acd47ae89a8242f2e73cb8..068cdb4dd77598fede025166b1a5e7505f1a7ba7 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift @@ -14,8 +14,6 @@ import XXModels public struct HomeState: Equatable { public init( failure: String? = nil, - authFailure: String? = nil, - messageListenerFailure: String? = nil, isNetworkHealthy: Bool? = nil, networkNodesReport: NodeRegistrationReport? = nil, isDeletingAccount: Bool = false, @@ -25,8 +23,6 @@ public struct HomeState: Equatable { userSearch: UserSearchState? = nil ) { self.failure = failure - self.authFailure = authFailure - self.messageListenerFailure = messageListenerFailure self.isNetworkHealthy = isNetworkHealthy self.isDeletingAccount = isDeletingAccount self.alert = alert @@ -36,8 +32,6 @@ public struct HomeState: Equatable { } public var failure: String? - public var authFailure: String? - public var messageListenerFailure: String? public var isNetworkHealthy: Bool? public var networkNodesReport: NodeRegistrationReport? public var isDeletingAccount: Bool @@ -55,20 +49,6 @@ public enum HomeAction: Equatable { case failure(NSError) } - public enum AuthHandler: Equatable { - case start - case stop - case failure(NSError) - case failureDismissed - } - - public enum MessageListener: Equatable { - case start - case stop - case failure(NSError) - case failureDismissed - } - public enum NetworkMonitor: Equatable { case start case stop @@ -84,8 +64,6 @@ public enum HomeAction: Equatable { } case messenger(Messenger) - case authHandler(AuthHandler) - case messageListener(MessageListener) case networkMonitor(NetworkMonitor) case deleteAccount(DeleteAccount) case didDismissAlert @@ -103,8 +81,6 @@ public struct HomeEnvironment { public init( messenger: Messenger, dbManager: DBManager, - authHandler: AuthCallbackHandler, - messageListener: MessageListenerHandler, mainQueue: AnySchedulerOf<DispatchQueue>, bgQueue: AnySchedulerOf<DispatchQueue>, register: @escaping () -> RegisterEnvironment, @@ -113,8 +89,6 @@ public struct HomeEnvironment { ) { self.messenger = messenger self.dbManager = dbManager - self.authHandler = authHandler - self.messageListener = messageListener self.mainQueue = mainQueue self.bgQueue = bgQueue self.register = register @@ -124,8 +98,6 @@ public struct HomeEnvironment { public var messenger: Messenger public var dbManager: DBManager - public var authHandler: AuthCallbackHandler - public var messageListener: MessageListenerHandler public var mainQueue: AnySchedulerOf<DispatchQueue> public var bgQueue: AnySchedulerOf<DispatchQueue> public var register: () -> RegisterEnvironment @@ -137,8 +109,6 @@ extension HomeEnvironment { public static let unimplemented = HomeEnvironment( messenger: .unimplemented, dbManager: .unimplemented, - authHandler: .unimplemented, - messageListener: .unimplemented, mainQueue: .unimplemented, bgQueue: .unimplemented, register: { .unimplemented }, @@ -151,14 +121,10 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> { state, action, env in enum NetworkHealthEffectId {} enum NetworkNodesEffectId {} - enum AuthCallbacksEffectId {} - enum MessageListenerEffectId {} switch action { case .messenger(.start): return .merge( - Effect(value: .authHandler(.start)), - Effect(value: .messageListener(.start)), Effect(value: .networkMonitor(.stop)), Effect.result { do { @@ -166,6 +132,9 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> if env.messenger.isConnected() == false { try env.messenger.connect() + } + + if env.messenger.isListeningForMessages() == false { try env.messenger.listenForMessages() } @@ -197,52 +166,6 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> state.failure = error.localizedDescription return .none - case .authHandler(.start): - return Effect.run { subscriber in - let cancellable = env.authHandler(onError: { error in - subscriber.send(.authHandler(.failure(error as NSError))) - }) - return AnyCancellable { cancellable.cancel() } - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - .cancellable(id: AuthCallbacksEffectId.self, cancelInFlight: true) - - case .authHandler(.stop): - return .cancel(id: AuthCallbacksEffectId.self) - - case .authHandler(.failure(let error)): - state.authFailure = error.localizedDescription - return .none - - case .authHandler(.failureDismissed): - state.authFailure = nil - return .none - - case .messageListener(.start): - return Effect.run { subscriber in - let cancellable = env.messageListener(onError: { error in - subscriber.send(.messageListener(.failure(error as NSError))) - }) - return AnyCancellable { cancellable.cancel() } - } - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - .cancellable(id: MessageListenerEffectId.self, cancelInFlight: true) - - case .messageListener(.stop): - return .cancel(id: MessageListenerEffectId.self) - - case .messageListener(.failure(let error)): - state.messageListenerFailure = error.localizedDescription - return .none - - case .messageListener(.failureDismissed): - state.messageListenerFailure = nil - return .none - case .networkMonitor(.start): return .merge( Effect.run { subscriber in diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift index 8cd7259b6c57a7054b1bae658df8772b0a7efa3d..03907b18d08fa6a5e39502060ec3f63b44773404 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift @@ -15,16 +15,12 @@ public struct HomeView: View { struct ViewState: Equatable { var failure: String? - var authFailure: String? - var messageListenerFailure: String? var isNetworkHealthy: Bool? var networkNodesReport: NodeRegistrationReport? var isDeletingAccount: Bool init(state: HomeState) { failure = state.failure - authFailure = state.authFailure - messageListenerFailure = state.messageListenerFailure isNetworkHealthy = state.isNetworkHealthy isDeletingAccount = state.isDeletingAccount networkNodesReport = state.networkNodesReport @@ -48,32 +44,6 @@ public struct HomeView: View { } } - if let authFailure = viewStore.authFailure { - Section { - Text(authFailure) - Button { - viewStore.send(.authHandler(.failureDismissed)) - } label: { - Text("Dismiss") - } - } header: { - Text("Auth Error") - } - } - - if let messageListenerFailure = viewStore.messageListenerFailure { - Section { - Text(messageListenerFailure) - Button { - viewStore.send(.messageListener(.failureDismissed)) - } label: { - Text("Dismiss") - } - } header: { - Text("Message Listener Error") - } - } - Section { HStack { Text("Health") diff --git a/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift b/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift index 6ce31e5d67acfde939a1bc9ba03e28226f4e0761..5339c89bbd3dba39edaf2cc8f0181e673b396f4b 100644 --- a/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift +++ b/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift @@ -1,19 +1,147 @@ +import Combine import ComposableArchitecture +import Foundation +import XCTestDynamicOverlay +import XXMessengerClient public struct RestoreState: Equatable { - public init() {} + public enum Field: String, Hashable { + case passphrase + } + + public struct File: Equatable { + public init(name: String, data: Data) { + self.name = name + self.data = data + } + + public var name: String + public var data: Data + } + + public init( + file: File? = nil, + fileImportFailure: String? = nil, + restoreFailure: String? = nil, + focusedField: Field? = nil, + isImportingFile: Bool = false, + passphrase: String = "", + isRestoring: Bool = false + ) { + self.file = file + self.fileImportFailure = fileImportFailure + self.restoreFailure = restoreFailure + self.focusedField = focusedField + self.isImportingFile = isImportingFile + self.passphrase = passphrase + self.isRestoring = isRestoring + } + + public var file: File? + public var fileImportFailure: String? + public var restoreFailure: String? + @BindableState public var focusedField: Field? + @BindableState public var isImportingFile: Bool + @BindableState public var passphrase: String + @BindableState public var isRestoring: Bool } -public enum RestoreAction: Equatable { +public enum RestoreAction: Equatable, BindableAction { + case importFileTapped + case fileImport(Result<URL, NSError>) + case restoreTapped case finished + case failed(NSError) + case binding(BindingAction<RestoreState>) } public struct RestoreEnvironment { - public init() {} + public init( + messenger: Messenger, + loadData: URLDataLoader, + mainQueue: AnySchedulerOf<DispatchQueue>, + bgQueue: AnySchedulerOf<DispatchQueue> + ) { + self.messenger = messenger + self.loadData = loadData + self.mainQueue = mainQueue + self.bgQueue = bgQueue + } + + public var messenger: Messenger + public var loadData: URLDataLoader + public var mainQueue: AnySchedulerOf<DispatchQueue> + public var bgQueue: AnySchedulerOf<DispatchQueue> } extension RestoreEnvironment { - public static let unimplemented = RestoreEnvironment() + public static let unimplemented = RestoreEnvironment( + messenger: .unimplemented, + loadData: .unimplemented, + mainQueue: .unimplemented, + bgQueue: .unimplemented + ) } -public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironment>.empty +public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironment> +{ state, action, env in + switch action { + case .importFileTapped: + state.isImportingFile = true + state.fileImportFailure = nil + return .none + + case .fileImport(.success(let url)): + state.isImportingFile = false + do { + state.file = .init( + name: url.lastPathComponent, + data: try env.loadData(url) + ) + state.fileImportFailure = nil + } catch { + state.file = nil + state.fileImportFailure = error.localizedDescription + } + return .none + + case .fileImport(.failure(let error)): + state.isImportingFile = false + state.file = nil + state.fileImportFailure = error.localizedDescription + return .none + + case .restoreTapped: + guard let backupData = state.file?.data, backupData.count > 0 else { return .none } + let backupPassphrase = state.passphrase + state.isRestoring = true + state.restoreFailure = nil + return Effect.result { + do { + _ = try env.messenger.restoreBackup( + backupData: backupData, + backupPassphrase: backupPassphrase + ) + return .success(.finished) + } catch { + return .success(.failed(error as NSError)) + } + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + + case .finished: + state.isRestoring = false + return .none + + case .failed(let error): + state.isRestoring = false + state.restoreFailure = error.localizedDescription + return .none + + case .binding(_): + return .none + } +} +.binding() diff --git a/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift b/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift index b2cf3e86cdaa2db59c2ce37eb3c8b33f0da38868..06f7b91515725dcd81157dd876b3bcba2783a8e1 100644 --- a/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift +++ b/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift @@ -7,9 +7,31 @@ public struct RestoreView: View { } let store: Store<RestoreState, RestoreAction> + @FocusState var focusedField: RestoreState.Field? struct ViewState: Equatable { - init(state: RestoreState) {} + struct File: Equatable { + var name: String + var size: Int + } + + var file: File? + var isImportingFile: Bool + var passphrase: String + var isRestoring: Bool + var focusedField: RestoreState.Field? + var fileImportFailure: String? + var restoreFailure: String? + + init(state: RestoreState) { + file = state.file.map { .init(name: $0.name, size: $0.data.count) } + isImportingFile = state.isImportingFile + passphrase = state.passphrase + isRestoring = state.isRestoring + focusedField = state.focusedField + fileImportFailure = state.fileImportFailure + restoreFailure = state.restoreFailure + } } public var body: some View { @@ -17,23 +39,94 @@ public struct RestoreView: View { NavigationView { Form { Section { - Text("Not implemented") + if let file = viewStore.file { + HStack(alignment: .bottom) { + Text(file.name) + Spacer() + Text(format(byteCount: file.size)) + } + } + + Button { + viewStore.send(.importFileTapped) + } label: { + Text("Import backup file") + } + .fileImporter( + isPresented: viewStore.binding( + get: \.isImportingFile, + send: { .set(\.$isImportingFile, $0) } + ), + allowedContentTypes: [.data], + onCompletion: { result in + viewStore.send(.fileImport(result.mapError { $0 as NSError })) + } + ) + + if let failure = viewStore.fileImportFailure { + Text("Error: \(failure)") + } + } header: { + Text("File") } + .disabled(viewStore.isRestoring) - Section { + if viewStore.file != nil { + Section { + SecureField("Passphrase", text: viewStore.binding( + get: \.passphrase, + send: { .set(\.$passphrase, $0) } + )) + .textContentType(.password) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .focused($focusedField, equals: .passphrase) + + Button { + viewStore.send(.restoreTapped) + } label: { + HStack { + Text("Restore") + Spacer() + if viewStore.isRestoring { + ProgressView() + } + } + } + + if let failure = viewStore.restoreFailure { + Text("Error: \(failure)") + } + } header: { + Text("Backup") + } + .disabled(viewStore.isRestoring) + } + } + .toolbar { + ToolbarItem(placement: .cancellationAction) { Button { viewStore.send(.finished) } label: { - Text("OK") - .frame(maxWidth: .infinity) + Text("Cancel") } + .disabled(viewStore.isRestoring) } } .navigationTitle("Restore") + .onChange(of: viewStore.focusedField) { focusedField = $0 } + .onChange(of: focusedField) { viewStore.send(.set(\.$focusedField, $0)) } } .navigationViewStyle(.stack) } } + + func format(byteCount: Int) -> String { + let formatter = ByteCountFormatter() + formatter.allowedUnits = [.useMB, .useKB, .useBytes] + formatter.countStyle = .binary + return formatter.string(fromByteCount: Int64(byteCount)) + } } #if DEBUG diff --git a/Examples/xx-messenger/Sources/RestoreFeature/URLDataLoader.swift b/Examples/xx-messenger/Sources/RestoreFeature/URLDataLoader.swift new file mode 100644 index 0000000000000000000000000000000000000000..bca96e58f59d9880aca1f38d100192fdf9c97f47 --- /dev/null +++ b/Examples/xx-messenger/Sources/RestoreFeature/URLDataLoader.swift @@ -0,0 +1,22 @@ +import Foundation +import XCTestDynamicOverlay + +public struct URLDataLoader { + public var load: (URL) throws -> Data + + public func callAsFunction(_ url: URL) throws -> Data { + try load(url) + } +} + +extension URLDataLoader { + public static let live = URLDataLoader { url in + try Data(contentsOf: url) + } +} + +extension URLDataLoader { + public static let unimplemented = URLDataLoader( + load: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeFeature.swift b/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeFeature.swift index 6c6f2e22aa36e73ff0fc4acc441eb63b91f56e66..5bfe814829ab4e85ff12ed43ec4b01fa91d9dc45 100644 --- a/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeFeature.swift +++ b/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeFeature.swift @@ -4,12 +4,15 @@ import XXMessengerClient public struct WelcomeState: Equatable { public init( - isCreatingCMix: Bool = false + isCreatingCMix: Bool = false, + failure: String? = nil ) { self.isCreatingAccount = isCreatingCMix + self.failure = failure } public var isCreatingAccount: Bool + public var failure: String? } public enum WelcomeAction: Equatable { @@ -48,6 +51,7 @@ public let welcomeReducer = Reducer<WelcomeState, WelcomeAction, WelcomeEnvironm switch action { case .newAccountTapped: state.isCreatingAccount = true + state.failure = nil return .future { fulfill in do { try env.messenger.create() @@ -66,10 +70,12 @@ public let welcomeReducer = Reducer<WelcomeState, WelcomeAction, WelcomeEnvironm case .finished: state.isCreatingAccount = false + state.failure = nil return .none - case .failed(_): + case .failed(let failure): state.isCreatingAccount = false + state.failure = failure return .none } } diff --git a/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeView.swift b/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeView.swift index 32312386644b0331d069a386e337f17b4003d41c..0050a572f08f565a88ef3581ddc464503151c260 100644 --- a/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeView.swift +++ b/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeView.swift @@ -11,9 +11,11 @@ public struct WelcomeView: View { struct ViewState: Equatable { init(_ state: WelcomeState) { isCreatingAccount = state.isCreatingAccount + failure = state.failure } var isCreatingAccount: Bool + var failure: String? } public var body: some View { @@ -24,6 +26,14 @@ public struct WelcomeView: View { Text("xx messenger") } + if let failure = viewStore.failure { + Section { + Text(failure) + } header: { + Text("Error") + } + } + Section { Button { viewStore.send(.newAccountTapped) 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 } diff --git a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift index d922d4f9f9e141ddaef8b577cdaccf6a0e24e922..a40f432cf7f02e207010f1a161baefb4082e73da 100644 --- a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift +++ b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift @@ -24,11 +24,10 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate - store.environment.authHandler.run = { _ in Cancellable {} } - store.environment.messageListener.run = { _ in Cancellable {} } store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) } store.environment.messenger.isConnected.run = { false } store.environment.messenger.connect.run = { messengerDidConnect += 1 } + store.environment.messenger.isListeningForMessages.run = { false } store.environment.messenger.listenForMessages.run = { messengerDidListenForMessages += 1 } store.environment.messenger.isLoggedIn.run = { false } store.environment.messenger.isRegistered.run = { false } @@ -39,15 +38,10 @@ final class HomeFeatureTests: XCTestCase { XCTAssertNoDifference(messengerDidConnect, 1) XCTAssertNoDifference(messengerDidListenForMessages, 1) - store.receive(.authHandler(.start)) - store.receive(.messageListener(.start)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.didStartUnregistered)) { $0.register = RegisterState() } - - store.send(.authHandler(.stop)) - store.send(.messageListener(.stop)) } func testMessengerStartRegistered() { @@ -64,11 +58,10 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate - store.environment.authHandler.run = { _ in Cancellable {} } - store.environment.messageListener.run = { _ in Cancellable {} } store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) } store.environment.messenger.isConnected.run = { false } store.environment.messenger.connect.run = { messengerDidConnect += 1 } + store.environment.messenger.isListeningForMessages.run = { false } store.environment.messenger.listenForMessages.run = { messengerDidListenForMessages += 1 } store.environment.messenger.isLoggedIn.run = { false } store.environment.messenger.isRegistered.run = { true } @@ -90,15 +83,11 @@ final class HomeFeatureTests: XCTestCase { XCTAssertNoDifference(messengerDidListenForMessages, 1) XCTAssertNoDifference(messengerDidLogIn, 1) - store.receive(.authHandler(.start)) - store.receive(.messageListener(.start)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.didStartRegistered)) store.receive(.networkMonitor(.start)) store.send(.networkMonitor(.stop)) - store.send(.authHandler(.stop)) - store.send(.messageListener(.stop)) } func testRegisterFinished() { @@ -115,10 +104,9 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate - store.environment.authHandler.run = { _ in Cancellable {} } - store.environment.messageListener.run = { _ in Cancellable {} } store.environment.messenger.start.run = { messengerDidStartWithTimeout.append($0) } store.environment.messenger.isConnected.run = { true } + store.environment.messenger.isListeningForMessages.run = { true } store.environment.messenger.isLoggedIn.run = { false } store.environment.messenger.isRegistered.run = { true } store.environment.messenger.logIn.run = { messengerDidLogIn += 1 } @@ -141,15 +129,11 @@ final class HomeFeatureTests: XCTestCase { XCTAssertNoDifference(messengerDidStartWithTimeout, [30_000]) XCTAssertNoDifference(messengerDidLogIn, 1) - store.receive(.authHandler(.start)) - store.receive(.messageListener(.start)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.didStartRegistered)) store.receive(.networkMonitor(.start)) store.send(.networkMonitor(.stop)) - store.send(.authHandler(.stop)) - store.send(.messageListener(.stop)) } func testMessengerStartFailure() { @@ -164,21 +148,14 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate - store.environment.authHandler.run = { _ in Cancellable {} } - store.environment.messageListener.run = { _ in Cancellable {} } store.environment.messenger.start.run = { _ in throw error } store.send(.messenger(.start)) - store.receive(.authHandler(.start)) - store.receive(.messageListener(.start)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.failure(error as NSError))) { $0.failure = error.localizedDescription } - - store.send(.authHandler(.stop)) - store.send(.messageListener(.stop)) } func testMessengerStartConnectFailure() { @@ -193,23 +170,16 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate - store.environment.authHandler.run = { _ in Cancellable {} } - store.environment.messageListener.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(.authHandler(.start)) - store.receive(.messageListener(.start)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.failure(error as NSError))) { $0.failure = error.localizedDescription } - - store.send(.authHandler(.stop)) - store.send(.messageListener(.stop)) } func testMessengerStartIsRegisteredFailure() { @@ -224,24 +194,18 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate - store.environment.authHandler.run = { _ in Cancellable {} } - store.environment.messageListener.run = { _ in Cancellable {} } store.environment.messenger.start.run = { _ in } store.environment.messenger.isConnected.run = { true } + store.environment.messenger.isListeningForMessages.run = { true } store.environment.messenger.isLoggedIn.run = { false } store.environment.messenger.isRegistered.run = { throw error } store.send(.messenger(.start)) - store.receive(.authHandler(.start)) - store.receive(.messageListener(.start)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.failure(error as NSError))) { $0.failure = error.localizedDescription } - - store.send(.authHandler(.stop)) - store.send(.messageListener(.stop)) } func testMessengerStartLogInFailure() { @@ -256,25 +220,19 @@ final class HomeFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate - store.environment.authHandler.run = { _ in Cancellable {} } - store.environment.messageListener.run = { _ in Cancellable {} } store.environment.messenger.start.run = { _ in } store.environment.messenger.isConnected.run = { true } + store.environment.messenger.isListeningForMessages.run = { true } store.environment.messenger.isLoggedIn.run = { false } store.environment.messenger.isRegistered.run = { true } store.environment.messenger.logIn.run = { throw error } store.send(.messenger(.start)) - store.receive(.authHandler(.start)) - store.receive(.messageListener(.start)) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.failure(error as NSError))) { $0.failure = error.localizedDescription } - - store.send(.authHandler(.stop)) - store.send(.messageListener(.stop)) } func testNetworkMonitorStart() { @@ -546,86 +504,4 @@ final class HomeFeatureTests: XCTestCase { $0.contacts = nil } } - - func testAuthCallbacks() { - let store = TestStore( - initialState: HomeState(), - reducer: homeReducer, - environment: .unimplemented - ) - - var didRunAuthHandler = 0 - var didCancelAuthHandler = 0 - var authHandlerOnError: [AuthCallbackHandler.OnError] = [] - - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.authHandler.run = { onError in - didRunAuthHandler += 1 - authHandlerOnError.append(onError) - return Cancellable { didCancelAuthHandler += 1 } - } - - store.send(.authHandler(.start)) - - XCTAssertNoDifference(didRunAuthHandler, 1) - - struct AuthHandlerError: Error { var id: Int } - authHandlerOnError.first?(AuthHandlerError(id: 1)) - - store.receive(.authHandler(.failure(AuthHandlerError(id: 1) as NSError))) { - $0.authFailure = AuthHandlerError(id: 1).localizedDescription - } - - store.send(.authHandler(.failureDismissed)) { - $0.authFailure = nil - } - - store.send(.authHandler(.stop)) - - XCTAssertNoDifference(didCancelAuthHandler, 1) - - authHandlerOnError.first?(AuthHandlerError(id: 2)) - } - - func testMessageListener() { - let store = TestStore( - initialState: HomeState(), - reducer: homeReducer, - environment: .unimplemented - ) - - var didRunMessageListener = 0 - var didCancelMessageListener = 0 - var messageListenerOnError: [MessageListenerHandler.OnError] = [] - - store.environment.mainQueue = .immediate - store.environment.bgQueue = .immediate - store.environment.messageListener.run = { onError in - didRunMessageListener += 1 - messageListenerOnError.append(onError) - return Cancellable { didCancelMessageListener += 1 } - } - - store.send(.messageListener(.start)) - - XCTAssertNoDifference(didRunMessageListener, 1) - - struct MessageListenerError: Error { var id: Int } - messageListenerOnError.first?(MessageListenerError(id: 1)) - - store.receive(.messageListener(.failure(MessageListenerError(id: 1) as NSError))) { - $0.messageListenerFailure = MessageListenerError(id: 1).localizedDescription - } - - store.send(.messageListener(.failureDismissed)) { - $0.messageListenerFailure = nil - } - - store.send(.messageListener(.stop)) - - XCTAssertNoDifference(didCancelMessageListener, 1) - - messageListenerOnError.first?(MessageListenerError(id: 2)) - } } diff --git a/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift b/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift index c3e77edbf64de2d8df6e3cfb518b610055928397..9a3cde3227357eb11d878c8c8f7fd7cc76f00fb5 100644 --- a/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift +++ b/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift @@ -1,15 +1,161 @@ +import CustomDump import ComposableArchitecture import XCTest @testable import RestoreFeature +import XXMessengerClient final class RestoreFeatureTests: XCTestCase { - func testFinish() { + func testFileImport() { + let fileURL = URL(string: "file-url")! + var didLoadDataFromURL: [URL] = [] + let dataFromURL = "data-from-url".data(using: .utf8)! + let store = TestStore( initialState: RestoreState(), reducer: restoreReducer, environment: .unimplemented ) - store.send(.finished) + store.environment.loadData.load = { url in + didLoadDataFromURL.append(url) + return dataFromURL + } + + store.send(.importFileTapped) { + $0.isImportingFile = true + } + + store.send(.fileImport(.success(fileURL))) { + $0.isImportingFile = false + $0.file = .init(name: fileURL.lastPathComponent, data: dataFromURL) + $0.fileImportFailure = nil + } + + XCTAssertNoDifference(didLoadDataFromURL, [fileURL]) + } + + func testFileImportFailure() { + struct Failure: Error {} + let failure = Failure() + + let store = TestStore( + initialState: RestoreState( + isImportingFile: true + ), + reducer: restoreReducer, + environment: .unimplemented + ) + + store.send(.fileImport(.failure(failure as NSError))) { + $0.isImportingFile = false + $0.file = nil + $0.fileImportFailure = failure.localizedDescription + } + } + + func testFileImportLoadingFailure() { + struct Failure: Error {} + let failure = Failure() + + let store = TestStore( + initialState: RestoreState( + isImportingFile: true + ), + reducer: restoreReducer, + environment: .unimplemented + ) + + store.environment.loadData.load = { _ in throw failure } + + store.send(.fileImport(.success(URL(string: "test")!))) { + $0.isImportingFile = false + $0.file = nil + $0.fileImportFailure = failure.localizedDescription + } + } + + func testRestore() { + let backupData = "backup-data".data(using: .utf8)! + let backupPassphrase = "backup-passphrase" + let restoreResult = MessengerRestoreBackup.Result( + restoredParams: BackupParams.init( + username: "", + email: nil, + phone: nil + ), + restoredContacts: [] + ) + + var didRestoreWithData: [Data] = [] + var didRestoreWithPassphrase: [String] = [] + + let store = TestStore( + initialState: RestoreState( + file: .init(name: "file-name", data: backupData) + ), + reducer: restoreReducer, + environment: .unimplemented + ) + + store.environment.bgQueue = .immediate + store.environment.mainQueue = .immediate + store.environment.messenger.restoreBackup.run = { data, passphrase in + didRestoreWithData.append(data) + didRestoreWithPassphrase.append(passphrase) + return restoreResult + } + + store.send(.set(\.$passphrase, backupPassphrase)) { + $0.passphrase = backupPassphrase + } + + store.send(.restoreTapped) { + $0.isRestoring = true + } + + XCTAssertNoDifference(didRestoreWithData, [backupData]) + XCTAssertNoDifference(didRestoreWithPassphrase, [backupPassphrase]) + + store.receive(.finished) { + $0.isRestoring = false + } + } + + func testRestoreWithoutFile() { + let store = TestStore( + initialState: RestoreState( + file: nil + ), + reducer: restoreReducer, + environment: .unimplemented + ) + + store.send(.restoreTapped) + } + + func testRestoreFailure() { + struct Failure: Error {} + let failure = Failure() + + let store = TestStore( + initialState: RestoreState( + file: .init(name: "name", data: "data".data(using: .utf8)!) + ), + reducer: restoreReducer, + environment: .unimplemented + ) + + store.environment.bgQueue = .immediate + store.environment.mainQueue = .immediate + store.environment.messenger.restoreBackup.run = { _, _ in throw failure } + + store.send(.restoreTapped) { + $0.isRestoring = true + } + + store.receive(.failed(failure as NSError)) { + $0.isRestoring = false + $0.restoreFailure = failure.localizedDescription + } } } diff --git a/Examples/xx-messenger/Tests/WelcomeFeatureTests/WelcomeFeatureTests.swift b/Examples/xx-messenger/Tests/WelcomeFeatureTests/WelcomeFeatureTests.swift index eb6b08566af0f4193c10b5167271bd86b2897330..c6f23b7a6c885a0b8728796f6e3fd41f3a018aa1 100644 --- a/Examples/xx-messenger/Tests/WelcomeFeatureTests/WelcomeFeatureTests.swift +++ b/Examples/xx-messenger/Tests/WelcomeFeatureTests/WelcomeFeatureTests.swift @@ -5,60 +5,65 @@ import XCTest @MainActor final class WelcomeFeatureTests: XCTestCase { func testNewAccountTapped() { + let mainQueue = DispatchQueue.test + let bgQueue = DispatchQueue.test + + var didCreateMessenger = 0 + let store = TestStore( initialState: WelcomeState(), reducer: welcomeReducer, environment: .unimplemented ) - let mainQueue = DispatchQueue.test - let bgQueue = DispatchQueue.test - var messengerDidCreate = false - store.environment.mainQueue = mainQueue.eraseToAnyScheduler() store.environment.bgQueue = bgQueue.eraseToAnyScheduler() - store.environment.messenger.create.run = { messengerDidCreate = true } + store.environment.messenger.create.run = { didCreateMessenger += 1 } store.send(.newAccountTapped) { $0.isCreatingAccount = true + $0.failure = nil } bgQueue.advance() - XCTAssertTrue(messengerDidCreate) + XCTAssertNoDifference(didCreateMessenger, 1) mainQueue.advance() store.receive(.finished) { $0.isCreatingAccount = false + $0.failure = nil } } func testNewAccountTappedMessengerCreateFailure() { + struct Failure: Error {} + let failure = Failure() + let mainQueue = DispatchQueue.test + let bgQueue = DispatchQueue.test + let store = TestStore( initialState: WelcomeState(), reducer: welcomeReducer, environment: .unimplemented ) - let mainQueue = DispatchQueue.test - let bgQueue = DispatchQueue.test - struct Error: Swift.Error, Equatable {} - let error = Error() - store.environment.mainQueue = mainQueue.eraseToAnyScheduler() store.environment.bgQueue = bgQueue.eraseToAnyScheduler() - store.environment.messenger.create.run = { throw error } + store.environment.messenger.create.run = { throw failure } store.send(.newAccountTapped) { $0.isCreatingAccount = true + $0.failure = nil } bgQueue.advance() mainQueue.advance() - store.receive(.failed(error.localizedDescription)) { + store.receive(.failed(failure.localizedDescription)) { $0.isCreatingAccount = false + $0.failure = failure.localizedDescription } } diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerConnect.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerConnect.swift index efca553a512e4ab3745eb292fd54e59232fe03f4..d4bc0fa90b6e4bfe72e1cd292ca3990875a35d0d 100644 --- a/Sources/XXMessengerClient/Messenger/Functions/MessengerConnect.swift +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerConnect.swift @@ -25,6 +25,7 @@ extension MessengerConnect { identity: try cMix.makeReceptionIdentity(legacy: true), e2eParamsJSON: env.getE2EParams() )) + env.isListeningForMessages.set(false) } } } diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerDestroy.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerDestroy.swift index 65a718dffa7c33c246de7a8b371c749433f2898b..7d6932badafbae5128694b414df70a32c405fd02 100644 --- a/Sources/XXMessengerClient/Messenger/Functions/MessengerDestroy.swift +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerDestroy.swift @@ -22,6 +22,7 @@ extension MessengerDestroy { env.ud.set(nil) env.e2e.set(nil) env.cMix.set(nil) + env.isListeningForMessages.set(false) try env.fileManager.removeDirectory(env.storageDir) try env.passwordStorage.remove() } diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerIsListeningForMessages.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerIsListeningForMessages.swift new file mode 100644 index 0000000000000000000000000000000000000000..a5cbd390456dc8063345655e007daca8bb4693fc --- /dev/null +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerIsListeningForMessages.swift @@ -0,0 +1,21 @@ +import XCTestDynamicOverlay + +public struct MessengerIsListeningForMessages { + public var run: () -> Bool + + public func callAsFunction() -> Bool { + run() + } +} + +extension MessengerIsListeningForMessages { + public static func live(_ env: MessengerEnvironment) -> MessengerIsListeningForMessages { + MessengerIsListeningForMessages(run: env.isListeningForMessages.get) + } +} + +extension MessengerIsListeningForMessages { + public static let unimplemented = MessengerIsListeningForMessages( + run: XCTUnimplemented("\(Self.self)", placeholder: false) + ) +} diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerListenForMessages.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerListenForMessages.swift index 90fa02dab3cf84d85e50e3d696fe0a5a226d4884..62b9a600a58eb7c279a518aff0b23d7110aaaaf5 100644 --- a/Sources/XXMessengerClient/Messenger/Functions/MessengerListenForMessages.swift +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerListenForMessages.swift @@ -16,14 +16,20 @@ public struct MessengerListenForMessages { extension MessengerListenForMessages { public static func live(_ env: MessengerEnvironment) -> MessengerListenForMessages { MessengerListenForMessages { - guard let e2e = env.e2e() else { - throw Error.notConnected + do { + guard let e2e = env.e2e() else { + throw Error.notConnected + } + try e2e.registerListener( + senderId: nil, + messageType: 2, + callback: env.messageListeners.registered() + ) + env.isListeningForMessages.set(true) + } catch { + env.isListeningForMessages.set(false) + throw error } - try e2e.registerListener( - senderId: nil, - messageType: 2, - callback: env.messageListeners.registered() - ) } } } diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerRestoreBackup.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerRestoreBackup.swift new file mode 100644 index 0000000000000000000000000000000000000000..9f0d591b44e0820c6f2f6af6611055c72f74be06 --- /dev/null +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerRestoreBackup.swift @@ -0,0 +1,93 @@ +import Foundation +import XXClient +import XCTestDynamicOverlay + +public struct MessengerRestoreBackup { + public struct Result: Equatable { + public init( + restoredParams: BackupParams, + restoredContacts: [Data] + ) { + self.restoredParams = restoredParams + self.restoredContacts = restoredContacts + } + + public var restoredParams: BackupParams + public var restoredContacts: [Data] + } + + public var run: (Data, String) throws -> Result + + public func callAsFunction( + backupData: Data, + backupPassphrase: String + ) throws -> Result { + try run(backupData, backupPassphrase) + } +} + +extension MessengerRestoreBackup { + public static func live(_ env: MessengerEnvironment) -> MessengerRestoreBackup { + MessengerRestoreBackup { backupData, backupPassphrase in + let storageDir = env.storageDir + do { + let ndfData = try env.downloadNDF(env.ndfEnvironment) + let password = env.generateSecret() + try env.passwordStorage.save(password) + try env.fileManager.removeDirectory(storageDir) + try env.fileManager.createDirectory(storageDir) + let report = try env.newCMixFromBackup( + ndfJSON: String(data: ndfData, encoding: .utf8)!, + storageDir: storageDir, + backupPassphrase: backupPassphrase, + sessionPassword: password, + backupFileContents: backupData + ) + let cMix = try env.loadCMix( + storageDir: storageDir, + password: password, + cMixParamsJSON: env.getCMixParams() + ) + try cMix.startNetworkFollower(timeoutMS: 30_000) + let e2e = try env.login( + cMixId: cMix.getId(), + authCallbacks: env.authCallbacks.registered(), + identity: try cMix.makeReceptionIdentity(legacy: true), + e2eParamsJSON: env.getE2EParams() + ) + let decoder = JSONDecoder() + let paramsData = report.params.data(using: .utf8)! + let params = try decoder.decode(BackupParams.self, from: paramsData) + let ud = try env.newUdManagerFromBackup( + params: NewUdManagerFromBackup.Params( + e2eId: e2e.getId(), + username: Fact(type: .username, value: params.username), + email: params.email.map { Fact(type: .email, value: $0) }, + phone: params.phone.map { Fact(type: .phone, value: $0) }, + cert: env.udCert ?? e2e.getUdCertFromNdf(), + contact: env.udContact ?? (try e2e.getUdContactFromNdf()), + address: env.udAddress ?? e2e.getUdAddressFromNdf() + ), + follower: UdNetworkStatus { cMix.networkFollowerStatus() } + ) + env.cMix.set(cMix) + env.e2e.set(e2e) + env.ud.set(ud) + env.isListeningForMessages.set(false) + return Result( + restoredParams: params, + restoredContacts: report.restoredContacts + ) + } catch { + try? env.fileManager.removeDirectory(storageDir) + throw error + } + } + } +} + +extension MessengerRestoreBackup { + public static let unimplemented = MessengerRestoreBackup( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/XXMessengerClient/Messenger/Messenger.swift b/Sources/XXMessengerClient/Messenger/Messenger.swift index 77f7f1e52f85af6d083f3b26916d8432643ab6a4..ea85852c6752e30af60987264851d57af910e811 100644 --- a/Sources/XXMessengerClient/Messenger/Messenger.swift +++ b/Sources/XXMessengerClient/Messenger/Messenger.swift @@ -6,6 +6,7 @@ public struct Messenger { public var ud: Stored<UserDiscovery?> public var isCreated: MessengerIsCreated public var create: MessengerCreate + public var restoreBackup: MessengerRestoreBackup public var isLoaded: MessengerIsLoaded public var load: MessengerLoad public var registerAuthCallbacks: MessengerRegisterAuthCallbacks @@ -13,6 +14,7 @@ public struct Messenger { public var start: MessengerStart public var isConnected: MessengerIsConnected public var connect: MessengerConnect + public var isListeningForMessages: MessengerIsListeningForMessages public var listenForMessages: MessengerListenForMessages public var isRegistered: MessengerIsRegistered public var register: MessengerRegister @@ -37,6 +39,7 @@ extension Messenger { ud: env.ud, isCreated: .live(env), create: .live(env), + restoreBackup: .live(env), isLoaded: .live(env), load: .live(env), registerAuthCallbacks: .live(env), @@ -44,6 +47,7 @@ extension Messenger { start: .live(env), isConnected: .live(env), connect: .live(env), + isListeningForMessages: .live(env), listenForMessages: .live(env), isRegistered: .live(env), register: .live(env), @@ -69,6 +73,7 @@ extension Messenger { ud: .unimplemented(), isCreated: .unimplemented, create: .unimplemented, + restoreBackup: .unimplemented, isLoaded: .unimplemented, load: .unimplemented, registerAuthCallbacks: .unimplemented, @@ -76,6 +81,7 @@ extension Messenger { start: .unimplemented, isConnected: .unimplemented, connect: .unimplemented, + isListeningForMessages: .unimplemented, listenForMessages: .unimplemented, isRegistered: .unimplemented, register: .unimplemented, diff --git a/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift b/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift index f125c1ef3ae705a1dde5d8e9ff1dc4467e25044c..641a8062d3f9ae136192af38b2814fe14cafccb9 100644 --- a/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift +++ b/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift @@ -12,6 +12,7 @@ public struct MessengerEnvironment { public var getCMixParams: GetCMixParams public var getE2EParams: GetE2EParams public var getSingleUseParams: GetSingleUseParams + public var isListeningForMessages: Stored<Bool> public var isRegisteredWithUD: IsRegisteredWithUD public var loadCMix: LoadCMix public var login: Login @@ -20,7 +21,9 @@ public struct MessengerEnvironment { public var multiLookupUD: MultiLookupUD public var ndfEnvironment: NDFEnvironment public var newCMix: NewCMix + public var newCMixFromBackup: NewCMixFromBackup public var newOrLoadUd: NewOrLoadUd + public var newUdManagerFromBackup: NewUdManagerFromBackup public var passwordStorage: PasswordStorage public var registerForNotifications: RegisterForNotifications public var searchUD: SearchUD @@ -50,6 +53,7 @@ extension MessengerEnvironment { getCMixParams: .liveDefault, getE2EParams: .liveDefault, getSingleUseParams: .liveDefault, + isListeningForMessages: .inMemory(false), isRegisteredWithUD: .live, loadCMix: .live, login: .live, @@ -58,7 +62,9 @@ extension MessengerEnvironment { multiLookupUD: .live(), ndfEnvironment: .mainnet, newCMix: .live, + newCMixFromBackup: .live, newOrLoadUd: .live, + newUdManagerFromBackup: .live, passwordStorage: .keychain, registerForNotifications: .live, searchUD: .live, @@ -83,6 +89,7 @@ extension MessengerEnvironment { getCMixParams: .unimplemented, getE2EParams: .unimplemented, getSingleUseParams: .unimplemented, + isListeningForMessages: .unimplemented(placeholder: false), isRegisteredWithUD: .unimplemented, loadCMix: .unimplemented, login: .unimplemented, @@ -91,7 +98,9 @@ extension MessengerEnvironment { multiLookupUD: .unimplemented, ndfEnvironment: .unimplemented, newCMix: .unimplemented, + newCMixFromBackup: .unimplemented, newOrLoadUd: .unimplemented, + newUdManagerFromBackup: .unimplemented, passwordStorage: .unimplemented, registerForNotifications: .unimplemented, searchUD: .unimplemented, diff --git a/Sources/XXMessengerClient/Utils/BackupParams.swift b/Sources/XXMessengerClient/Utils/BackupParams.swift new file mode 100644 index 0000000000000000000000000000000000000000..02fdc595461cbcc14e617560b6f7b91b6b398fc2 --- /dev/null +++ b/Sources/XXMessengerClient/Utils/BackupParams.swift @@ -0,0 +1,17 @@ +import Foundation + +public struct BackupParams: Equatable, Codable { + public init( + username: String, + email: String?, + phone: String? + ) { + self.username = username + self.email = email + self.phone = phone + } + + public var username: String + public var email: String? + public var phone: String? +} diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerConnectTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerConnectTests.swift index 1f69797f10465a6c4d4ddd027d368cdc7dc69c7a..e17d6028ed3fd2eb59577f2b94df31338a60b1af 100644 --- a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerConnectTests.swift +++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerConnectTests.swift @@ -17,12 +17,14 @@ final class MessengerConnectTests: XCTestCase { var didLogInWithAuthCallbacks: [AuthCallbacks?] = [] var didSetE2E: [E2E?] = [] var didHandleAuthCallbacks: [AuthCallbacks.Callback] = [] + var didSetIsListeningForMessages: [Bool] = [] let cMixId = 1234 let receptionId = ReceptionIdentity.stub let e2eParams = "e2e-params".data(using: .utf8)! var env: MessengerEnvironment = .unimplemented + env.isListeningForMessages.set = { didSetIsListeningForMessages.append($0) } env.cMix.get = { var cMix: CMix = .unimplemented cMix.getId.run = { cMixId } @@ -62,6 +64,7 @@ final class MessengerConnectTests: XCTestCase { e2eParamsJSON: e2eParams ) ]) + XCTAssertNoDifference(didSetIsListeningForMessages, [false]) XCTAssertEqual(didLogInWithAuthCallbacks.compactMap { $0 }.count, 1) XCTAssertEqual(didSetE2E.compactMap { $0 }.count, 1) diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerDestroyTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerDestroyTests.swift index 36745d028934142b59912d341649e1519b435e1e..ccf999e77e6771724d17e27b6b43ce6a811fb8ae 100644 --- a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerDestroyTests.swift +++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerDestroyTests.swift @@ -14,6 +14,7 @@ final class MessengerDestroyTests: XCTestCase { var didSetE2E: [E2E?] = [] var didSetCMix: [CMix?] = [] var didRemovePassword = 0 + var didSetIsListeningForMessages: [Bool] = [] var env: MessengerEnvironment = .unimplemented env.cMix.get = { @@ -28,6 +29,7 @@ final class MessengerDestroyTests: XCTestCase { env.ud.set = { didSetUD.append($0) } env.e2e.set = { didSetE2E.append($0) } env.cMix.set = { didSetCMix.append($0) } + env.isListeningForMessages.set = { didSetIsListeningForMessages.append($0) } env.fileManager.removeDirectory = { didRemoveDirectory.append($0) } env.passwordStorage.remove = { didRemovePassword += 1 } let destroy: MessengerDestroy = .live(env) @@ -39,6 +41,7 @@ final class MessengerDestroyTests: XCTestCase { XCTAssertNoDifference(didSetUD.map { $0 == nil }, [true]) XCTAssertNoDifference(didSetE2E.map { $0 == nil }, [true]) XCTAssertNoDifference(didSetCMix.map { $0 == nil }, [true]) + XCTAssertNoDifference(didSetIsListeningForMessages, [false]) XCTAssertNoDifference(didRemoveDirectory, [storageDir]) XCTAssertNoDifference(didRemovePassword, 1) } @@ -67,12 +70,14 @@ final class MessengerDestroyTests: XCTestCase { var didSetUD: [UserDiscovery?] = [] var didSetE2E: [E2E?] = [] var didSetCMix: [CMix?] = [] + var didSetIsListeningForMessages: [Bool] = [] var env: MessengerEnvironment = .unimplemented env.cMix.get = { nil } env.ud.set = { didSetUD.append($0) } env.e2e.set = { didSetE2E.append($0) } env.cMix.set = { didSetCMix.append($0) } + env.isListeningForMessages.set = { didSetIsListeningForMessages.append($0) } env.fileManager.removeDirectory = { _ in throw error } let destroy: MessengerDestroy = .live(env) @@ -82,6 +87,7 @@ final class MessengerDestroyTests: XCTestCase { XCTAssertNoDifference(didSetUD.map { $0 == nil }, [true]) XCTAssertNoDifference(didSetE2E.map { $0 == nil }, [true]) XCTAssertNoDifference(didSetCMix.map { $0 == nil }, [true]) + XCTAssertNoDifference(didSetIsListeningForMessages, [false]) } func testRemovePasswordFailure() { @@ -92,12 +98,14 @@ final class MessengerDestroyTests: XCTestCase { var didSetUD: [UserDiscovery?] = [] var didSetE2E: [E2E?] = [] var didSetCMix: [CMix?] = [] + var didSetIsListeningForMessages: [Bool] = [] var env: MessengerEnvironment = .unimplemented env.cMix.get = { nil } env.ud.set = { didSetUD.append($0) } env.e2e.set = { didSetE2E.append($0) } env.cMix.set = { didSetCMix.append($0) } + env.isListeningForMessages.set = { didSetIsListeningForMessages.append($0) } env.storageDir = storageDir env.fileManager.removeDirectory = { didRemoveDirectory.append($0) } env.passwordStorage.remove = { throw error } @@ -109,6 +117,7 @@ final class MessengerDestroyTests: XCTestCase { XCTAssertNoDifference(didSetUD.map { $0 == nil }, [true]) XCTAssertNoDifference(didSetE2E.map { $0 == nil }, [true]) XCTAssertNoDifference(didSetCMix.map { $0 == nil }, [true]) + XCTAssertNoDifference(didSetIsListeningForMessages, [false]) XCTAssertNoDifference(didRemoveDirectory, [storageDir]) } } diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerIsListeningForMessagesTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerIsListeningForMessagesTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..bda5200bd315d498f9af8085aeb0571bfedfb290 --- /dev/null +++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerIsListeningForMessagesTests.swift @@ -0,0 +1,20 @@ +import XCTest +@testable import XXMessengerClient + +final class MessengerIsListeningForMessagesTests: XCTestCase { + func testListening() { + var env: MessengerEnvironment = .unimplemented + env.isListeningForMessages.get = { true } + let isListening: MessengerIsListeningForMessages = .live(env) + + XCTAssertTrue(isListening()) + } + + func testNotListening() { + var env: MessengerEnvironment = .unimplemented + env.isListeningForMessages.get = { false } + let isListening: MessengerIsListeningForMessages = .live(env) + + XCTAssertFalse(isListening()) + } +} diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerListenForMessagesTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerListenForMessagesTests.swift index 948ee33e3d668fdb0eff4a4cfad72f82b908c395..1f78a60a959ffb64d1bfbff41e59cb624ab36eb3 100644 --- a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerListenForMessagesTests.swift +++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerListenForMessagesTests.swift @@ -12,6 +12,7 @@ final class MessengerListenForMessagesTests: XCTestCase { var didRegisterListenerWithParams: [RegisterListenerParams] = [] var didRegisterListenerWithCallback: [Listener] = [] var didHandleMessage: [Message] = [] + var didSetIsListeningForMessages: [Bool] = [] var env: MessengerEnvironment = .unimplemented env.e2e.get = { @@ -25,6 +26,9 @@ final class MessengerListenForMessagesTests: XCTestCase { env.messageListeners.registered = { Listener { message in didHandleMessage.append(message) } } + env.isListeningForMessages.set = { + didSetIsListeningForMessages.append($0) + } let listen: MessengerListenForMessages = .live(env) try listen() @@ -32,6 +36,7 @@ final class MessengerListenForMessagesTests: XCTestCase { XCTAssertNoDifference(didRegisterListenerWithParams, [ .init(senderId: nil, messageType: 2) ]) + XCTAssertNoDifference(didSetIsListeningForMessages, [true]) let message = Message.stub(123) didRegisterListenerWithCallback.first?.handle(message) @@ -40,19 +45,26 @@ final class MessengerListenForMessagesTests: XCTestCase { } func testListenWhenNotLoggedIn() { + var didSetIsListeningForMessages: [Bool] = [] + var env: MessengerEnvironment = .unimplemented env.e2e.get = { nil } + env.isListeningForMessages.set = { didSetIsListeningForMessages.append($0) } let listen: MessengerListenForMessages = .live(env) XCTAssertThrowsError(try listen()) { error in XCTAssertNoDifference(error as? MessengerListenForMessages.Error, .notConnected) } + + XCTAssertNoDifference(didSetIsListeningForMessages, [false]) } func testListenFailure() { struct Failure: Error, Equatable {} let error = Failure() + var didSetIsListeningForMessages: [Bool] = [] + var env: MessengerEnvironment = .unimplemented env.e2e.get = { var e2e: E2E = .unimplemented @@ -60,10 +72,13 @@ final class MessengerListenForMessagesTests: XCTestCase { return e2e } env.messageListeners.registered = { Listener.unimplemented } + env.isListeningForMessages.set = { didSetIsListeningForMessages.append($0) } let listen: MessengerListenForMessages = .live(env) XCTAssertThrowsError(try listen()) { err in XCTAssertNoDifference(err as? Failure, error) } + + XCTAssertNoDifference(didSetIsListeningForMessages, [false]) } } diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerRestoreBackupTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerRestoreBackupTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..fb8fe7da315df608763b572a273ec67ef2c8366c --- /dev/null +++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerRestoreBackupTests.swift @@ -0,0 +1,240 @@ +import CustomDump +import XCTest +import XXClient +@testable import XXMessengerClient + +final class MessengerRestoreBackupTests: XCTestCase { + func testRestore() throws { + let backupData = "backup-data".data(using: .utf8)! + let backupPassphrase = "backup-passphrase" + let ndfData = "ndf-data".data(using: .utf8)! + let password = "password".data(using: .utf8)! + let backupContacts: [Data] = (1...3).map { "contact-\($0)" }.map { $0.data(using: .utf8)! } + let backupParams = BackupParams( + username: "backup-username", + email: "backup-email", + phone: "backup-phone" + ) + let backupReport = BackupReport( + restoredContacts: backupContacts, + params: String(data: try! JSONEncoder().encode(backupParams), encoding: .utf8)! + ) + let cMixParams = "cmix-params".data(using: .utf8)! + let e2eParams = "e2e-params".data(using: .utf8)! + let cMixId = 123 + let e2eId = 456 + let receptionIdentity = ReceptionIdentity( + id: "reception-id".data(using: .utf8)!, + rsaPrivatePem: "reception-rsaPrivatePem".data(using: .utf8)!, + salt: "reception-salt".data(using: .utf8)!, + dhKeyPrivate: "reception-dhKeyPrivate".data(using: .utf8)!, + e2eGrp: "reception-e2eGrp".data(using: .utf8)! + ) + let udContactFromNdf = "ud-contact".data(using: .utf8)! + let udCertFromNdf = "ud-cert".data(using: .utf8)! + let udAddressFromNdf = "ud-address" + + var caughtActions: [CaughtAction] = [] + + var env: MessengerEnvironment = .unimplemented + env.downloadNDF.run = { ndfEnvironment in + caughtActions.append(.didDownloadNDF(environment: ndfEnvironment)) + return ndfData + } + env.generateSecret.run = { _ in password } + env.passwordStorage.save = { caughtActions.append(.didSavePassword(password: $0)) } + env.passwordStorage.load = { password } + env.fileManager.removeDirectory = { caughtActions.append(.didRemoveDirectory(path: $0)) } + env.fileManager.createDirectory = { caughtActions.append(.didCreateDirectory(path: $0)) } + env.newCMixFromBackup.run = { + ndfJSON, storageDir, backupPassphrase, sessionPassword, backupFileContents in + caughtActions.append(.didNewCMixFromBackup( + ndfJSON: ndfJSON, + storageDir: storageDir, + backupPassphrase: backupPassphrase, + sessionPassword: sessionPassword, + backupFileContents: backupFileContents + )) + return backupReport + } + env.getCMixParams.run = { cMixParams } + env.getE2EParams.run = { e2eParams } + env.loadCMix.run = { storageDir, password, cMixParams in + caughtActions.append(.didLoadCMix( + storageDir: storageDir, + password: password, + cMixParams: cMixParams + )) + var cMix: CMix = .unimplemented + cMix.getId.run = { cMixId } + cMix.makeReceptionIdentity.run = { legacy in + caughtActions.append(.cMixDidMakeReceptionIdentity(legacy: legacy)) + return receptionIdentity + } + cMix.startNetworkFollower.run = { timeoutMS in + caughtActions.append(.cMixDidStartNetworkFollower(timeoutMS: timeoutMS)) + } + return cMix + } + env.login.run = { ephemeral, cMixId, _, identity, e2eParams in + caughtActions.append(.didLogin( + ephemeral: ephemeral, + cMixId: cMixId, + identity: identity, + e2eParamsJSON: e2eParams + )) + var e2e: E2E = .unimplemented + e2e.getId.run = { e2eId } + e2e.getUdCertFromNdf.run = { udCertFromNdf } + e2e.getUdContactFromNdf.run = { udContactFromNdf } + e2e.getUdAddressFromNdf.run = { udAddressFromNdf } + return e2e + } + env.newUdManagerFromBackup.run = { params, _ in + caughtActions.append(.didNewUdManagerFromBackup(params: params)) + return .unimplemented + } + env.authCallbacks.registered = { + AuthCallbacks { _ in } + } + env.cMix.set = { _ in caughtActions.append(.didSetCMix) } + env.e2e.set = { _ in caughtActions.append(.didSetE2E) } + env.ud.set = { _ in caughtActions.append(.didSetUD) } + env.isListeningForMessages.set = { + caughtActions.append(.didSetIsListeningForMessages(isListening: $0)) + } + + let restore: MessengerRestoreBackup = .live(env) + + let result = try restore( + backupData: backupData, + backupPassphrase: backupPassphrase + ) + + XCTAssertNoDifference(caughtActions, [ + .didDownloadNDF( + environment: env.ndfEnvironment + ), + .didSavePassword( + password: password + ), + .didRemoveDirectory( + path: env.storageDir + ), + .didCreateDirectory( + path: env.storageDir + ), + .didNewCMixFromBackup( + ndfJSON: String(data: ndfData, encoding: .utf8)!, + storageDir: env.storageDir, + backupPassphrase: backupPassphrase, + sessionPassword: password, + backupFileContents: backupData + ), + .didLoadCMix( + storageDir: env.storageDir, + password: password, + cMixParams: cMixParams + ), + .cMixDidStartNetworkFollower( + timeoutMS: 30_000 + ), + .cMixDidMakeReceptionIdentity( + legacy: true + ), + .didLogin( + ephemeral: false, + cMixId: cMixId, + identity: receptionIdentity, + e2eParamsJSON: e2eParams + ), + .didNewUdManagerFromBackup(params: .init( + e2eId: e2eId, + username: Fact(type: .username, value: backupParams.username), + email: Fact(type: .email, value: backupParams.email!), + phone: Fact(type: .phone, value: backupParams.phone!), + cert: udCertFromNdf, + contact: udContactFromNdf, + address: udAddressFromNdf + )), + .didSetCMix, + .didSetE2E, + .didSetUD, + .didSetIsListeningForMessages( + isListening: false + ), + ]) + + XCTAssertNoDifference(result, MessengerRestoreBackup.Result( + restoredParams: backupParams, + restoredContacts: backupContacts + )) + } + + func testDownloadNdfFailure() { + struct Failure: Error, Equatable {} + let failure = Failure() + + var actions: [CaughtAction] = [] + + var env: MessengerEnvironment = .unimplemented + env.downloadNDF.run = { _ in throw failure } + env.fileManager.removeDirectory = { actions.append(.didRemoveDirectory(path: $0)) } + let restore: MessengerRestoreBackup = .live(env) + + XCTAssertThrowsError(try restore(backupData: Data(), backupPassphrase: "")) { error in + XCTAssertNoDifference(error as? Failure, failure) + } + XCTAssertNoDifference(actions, [ + .didRemoveDirectory(path: env.storageDir) + ]) + } +} + +private enum CaughtAction: Equatable { + case didDownloadNDF( + environment: NDFEnvironment + ) + case didSavePassword( + password: Data + ) + case didRemoveDirectory( + path: String + ) + case didCreateDirectory( + path: String + ) + case didNewCMixFromBackup( + ndfJSON: String, + storageDir: String, + backupPassphrase: String, + sessionPassword: Data, + backupFileContents: Data + ) + case didLoadCMix( + storageDir: String, + password: Data, + cMixParams: Data + ) + case didLogin( + ephemeral: Bool, + cMixId: Int, + identity: ReceptionIdentity, + e2eParamsJSON: Data + ) + case cMixDidMakeReceptionIdentity( + legacy: Bool + ) + case cMixDidStartNetworkFollower( + timeoutMS: Int + ) + case didNewUdManagerFromBackup( + params: NewUdManagerFromBackup.Params + ) + case didSetCMix + case didSetE2E + case didSetUD + case didSetIsListeningForMessages( + isListening: Bool + ) +}