From bc69f0f25f3d4b3cd08802ac2e4a392292fc8c06 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Thu, 29 Sep 2022 10:47:01 +0200 Subject: [PATCH 01/15] Add BackupFeature library --- .../xcschemes/BackupFeature.xcscheme | 78 +++++++++++++++++++ Examples/xx-messenger/Package.swift | 16 ++++ .../xcschemes/XXMessenger.xcscheme | 10 +++ .../Sources/BackupFeature/BackupFeature.swift | 28 +++++++ .../Sources/BackupFeature/BackupView.swift | 40 ++++++++++ .../BackupFeatureTests.swift | 15 ++++ 6 files changed, 187 insertions(+) create mode 100644 Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/BackupFeature.xcscheme create mode 100644 Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift create mode 100644 Examples/xx-messenger/Sources/BackupFeature/BackupView.swift create mode 100644 Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift diff --git a/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/BackupFeature.xcscheme b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/BackupFeature.xcscheme new file mode 100644 index 00000000..61d3cc58 --- /dev/null +++ b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/BackupFeature.xcscheme @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Scheme + LastUpgradeVersion = "1400" + version = "1.3"> + <BuildAction + parallelizeBuildables = "YES" + buildImplicitDependencies = "YES"> + <BuildActionEntries> + <BuildActionEntry + buildForTesting = "YES" + buildForRunning = "YES" + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "BackupFeature" + BuildableName = "BackupFeature" + BlueprintName = "BackupFeature" + ReferencedContainer = "container:"> + </BuildableReference> + </BuildActionEntry> + </BuildActionEntries> + </BuildAction> + <TestAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + shouldUseLaunchSchemeArgsEnv = "YES" + codeCoverageEnabled = "YES"> + <Testables> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "BackupFeatureTests" + BuildableName = "BackupFeatureTests" + BlueprintName = "BackupFeatureTests" + ReferencedContainer = "container:"> + </BuildableReference> + </TestableReference> + </Testables> + </TestAction> + <LaunchAction + buildConfiguration = "Debug" + selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" + selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + launchStyle = "0" + useCustomWorkingDirectory = "NO" + ignoresPersistentStateOnLaunch = "NO" + debugDocumentVersioning = "YES" + debugServiceExtension = "internal" + allowLocationSimulation = "YES"> + </LaunchAction> + <ProfileAction + buildConfiguration = "Release" + shouldUseLaunchSchemeArgsEnv = "YES" + savedToolIdentifier = "" + useCustomWorkingDirectory = "NO" + debugDocumentVersioning = "YES"> + <MacroExpansion> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "BackupFeature" + BuildableName = "BackupFeature" + BlueprintName = "BackupFeature" + ReferencedContainer = "container:"> + </BuildableReference> + </MacroExpansion> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index b310549f..c1294b27 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -15,6 +15,7 @@ let package = Package( products: [ .library(name: "AppCore", targets: ["AppCore"]), .library(name: "AppFeature", targets: ["AppFeature"]), + .library(name: "BackupFeature", targets: ["BackupFeature"]), .library(name: "ChatFeature", targets: ["ChatFeature"]), .library(name: "CheckContactAuthFeature", targets: ["CheckContactAuthFeature"]), .library(name: "ConfirmRequestFeature", targets: ["ConfirmRequestFeature"]), @@ -112,6 +113,21 @@ let package = Package( ], swiftSettings: swiftSettings ), + .target( + name: "BackupFeature", + dependencies: [ + .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), + ], + swiftSettings: swiftSettings + ), + .testTarget( + name: "BackupFeatureTests", + dependencies: [ + .target(name: "BackupFeature"), + ], + swiftSettings: swiftSettings + ), .target( name: "ChatFeature", dependencies: [ diff --git a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme index aa97ea7d..b50f1c10 100644 --- a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme +++ b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme @@ -49,6 +49,16 @@ ReferencedContainer = "container:.."> </BuildableReference> </TestableReference> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "BackupFeatureTests" + BuildableName = "BackupFeatureTests" + BlueprintName = "BackupFeatureTests" + ReferencedContainer = "container:.."> + </BuildableReference> + </TestableReference> <TestableReference skipped = "NO"> <BuildableReference diff --git a/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift b/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift new file mode 100644 index 00000000..eebbbc0b --- /dev/null +++ b/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift @@ -0,0 +1,28 @@ +import ComposableArchitecture +import XCTestDynamicOverlay + +public struct BackupState: Equatable { + public init() {} +} + +public enum BackupAction: Equatable { + case start +} + +public struct BackupEnvironment { + public init() {} +} + +#if DEBUG +extension BackupEnvironment { + public static let unimplemented = BackupEnvironment() +} +#endif + +public let backupReducer = Reducer<BackupState, BackupAction, BackupEnvironment> +{ state, action, env in + switch action { + case .start: + return .none + } +} diff --git a/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift b/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift new file mode 100644 index 00000000..874e6137 --- /dev/null +++ b/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift @@ -0,0 +1,40 @@ +import ComposableArchitecture +import SwiftUI + +public struct BackupView: View { + public init(store: Store<BackupState, BackupAction>) { + self.store = store + } + + let store: Store<BackupState, BackupAction> + + struct ViewState: Equatable { + init(state: BackupState) {} + } + + public var body: some View { + WithViewStore(store, observe: ViewState.init) { viewStore in + Form { + + } + .navigationTitle("Backup") + .task { + viewStore.send(.start) + } + } + } +} + +#if DEBUG +public struct BackupView_Previews: PreviewProvider { + public static var previews: some View { + NavigationView { + BackupView(store: Store( + initialState: BackupState(), + reducer: .empty, + environment: () + )) + } + } +} +#endif diff --git a/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift b/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift new file mode 100644 index 00000000..a95240ca --- /dev/null +++ b/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift @@ -0,0 +1,15 @@ +import ComposableArchitecture +import XCTest +@testable import BackupFeature + +final class BackupFeatureTests: XCTestCase { + func testStart() { + let store = TestStore( + initialState: BackupState(), + reducer: backupReducer, + environment: .unimplemented + ) + + store.send(.start) + } +} -- GitLab From 93b98fa4f7e389405b83c99ca546b663fbe21877 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Thu, 29 Sep 2022 10:57:03 +0200 Subject: [PATCH 02/15] Present Backup from Home --- Examples/xx-messenger/Package.swift | 2 ++ .../AppFeature/AppEnvironment+Live.swift | 4 +++ .../Sources/HomeFeature/HomeFeature.swift | 34 ++++++++++++++++--- .../Sources/HomeFeature/HomeView.swift | 21 ++++++++++++ .../HomeFeatureTests/HomeFeatureTests.swift | 27 +++++++++++++++ 5 files changed, 84 insertions(+), 4 deletions(-) diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index c1294b27..8fa55d88 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -84,6 +84,7 @@ let package = Package( name: "AppFeature", dependencies: [ .target(name: "AppCore"), + .target(name: "BackupFeature"), .target(name: "ChatFeature"), .target(name: "CheckContactAuthFeature"), .target(name: "ConfirmRequestFeature"), @@ -231,6 +232,7 @@ let package = Package( name: "HomeFeature", dependencies: [ .target(name: "AppCore"), + .target(name: "BackupFeature"), .target(name: "ContactsFeature"), .target(name: "RegisterFeature"), .target(name: "UserSearchFeature"), diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index 24e5bd89..0ed4d6b5 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -1,4 +1,5 @@ import AppCore +import BackupFeature import ChatFeature import CheckContactAuthFeature import ConfirmRequestFeature @@ -149,6 +150,9 @@ extension AppEnvironment { bgQueue: bgQueue, contact: { contactEnvironment } ) + }, + backup: { + BackupEnvironment() } ) } diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift index 068cdb4d..e576e6f1 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift @@ -1,4 +1,5 @@ import AppCore +import BackupFeature import Combine import ComposableArchitecture import ComposablePresentation @@ -20,7 +21,8 @@ public struct HomeState: Equatable { alert: AlertState<HomeAction>? = nil, register: RegisterState? = nil, contacts: ContactsState? = nil, - userSearch: UserSearchState? = nil + userSearch: UserSearchState? = nil, + backup: BackupState? = nil ) { self.failure = failure self.isNetworkHealthy = isNetworkHealthy @@ -29,6 +31,7 @@ public struct HomeState: Equatable { self.register = register self.contacts = contacts self.userSearch = userSearch + self.backup = backup } public var failure: String? @@ -39,6 +42,7 @@ public struct HomeState: Equatable { public var register: RegisterState? public var contacts: ContactsState? public var userSearch: UserSearchState? + public var backup: BackupState? } public enum HomeAction: Equatable { @@ -72,9 +76,12 @@ public enum HomeAction: Equatable { case didDismissUserSearch case contactsButtonTapped case didDismissContacts + case backupButtonTapped + case didDismissBackup case register(RegisterAction) case contacts(ContactsAction) case userSearch(UserSearchAction) + case backup(BackupAction) } public struct HomeEnvironment { @@ -85,7 +92,8 @@ public struct HomeEnvironment { bgQueue: AnySchedulerOf<DispatchQueue>, register: @escaping () -> RegisterEnvironment, contacts: @escaping () -> ContactsEnvironment, - userSearch: @escaping () -> UserSearchEnvironment + userSearch: @escaping () -> UserSearchEnvironment, + backup: @escaping () -> BackupEnvironment ) { self.messenger = messenger self.dbManager = dbManager @@ -94,6 +102,7 @@ public struct HomeEnvironment { self.register = register self.contacts = contacts self.userSearch = userSearch + self.backup = backup } public var messenger: Messenger @@ -103,6 +112,7 @@ public struct HomeEnvironment { public var register: () -> RegisterEnvironment public var contacts: () -> ContactsEnvironment public var userSearch: () -> UserSearchEnvironment + public var backup: () -> BackupEnvironment } extension HomeEnvironment { @@ -113,7 +123,8 @@ extension HomeEnvironment { bgQueue: .unimplemented, register: { .unimplemented }, contacts: { .unimplemented }, - userSearch: { .unimplemented } + userSearch: { .unimplemented }, + backup: { .unimplemented } ) } @@ -267,7 +278,15 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> state.register = nil return Effect(value: .messenger(.start)) - case .register(_), .contacts(_), .userSearch(_): + case .backupButtonTapped: + state.backup = BackupState() + return .none + + case .didDismissBackup: + state.backup = nil + return .none + + case .register(_), .contacts(_), .userSearch(_), .backup(_): return .none } } @@ -292,3 +311,10 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> action: /HomeAction.userSearch, environment: { $0.userSearch() } ) +.presenting( + backupReducer, + state: .keyPath(\.backup), + id: .notNil(), + action: /HomeAction.backup, + environment: { $0.backup() } +) diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift index 03907b18..95bde09c 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift @@ -1,3 +1,4 @@ +import BackupFeature import ComposableArchitecture import ComposablePresentation import ContactsFeature @@ -111,6 +112,16 @@ public struct HomeView: View { } Section { + Button { + viewStore.send(.backupButtonTapped) + } label: { + HStack { + Text("Backup") + Spacer() + Image(systemName: "chevron.forward") + } + } + Button(role: .destructive) { viewStore.send(.deleteAccount(.buttonTapped)) } label: { @@ -152,6 +163,16 @@ public struct HomeView: View { }, destination: UserSearchView.init(store:) )) + .background(NavigationLinkWithStore( + store.scope( + state: \.backup, + action: HomeAction.backup + ), + onDeactivate: { + viewStore.send(.didDismissBackup) + }, + destination: BackupView.init(store:) + )) } .navigationViewStyle(.stack) .task { viewStore.send(.messenger(.start)) } diff --git a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift index b172793c..cb9ee3b6 100644 --- a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift +++ b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift @@ -1,4 +1,5 @@ import AppCore +import BackupFeature import ComposableArchitecture import ContactsFeature import CustomDump @@ -504,4 +505,30 @@ final class HomeFeatureTests: XCTestCase { $0.contacts = nil } } + + func testBackupButtonTapped() { + let store = TestStore( + initialState: HomeState(), + reducer: homeReducer, + environment: .unimplemented + ) + + store.send(.backupButtonTapped) { + $0.backup = BackupState() + } + } + + func testDidDismissBackup() { + let store = TestStore( + initialState: HomeState( + backup: BackupState() + ), + reducer: homeReducer, + environment: .unimplemented + ) + + store.send(.didDismissBackup) { + $0.backup = nil + } + } } -- GitLab From 4aa59f6dd32aaaf8bb6e8298c12c15041c48e4f5 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Thu, 29 Sep 2022 14:40:45 +0200 Subject: [PATCH 03/15] Fix race condition in MessengerStartBackup --- .../Functions/MessengerStartBackup.swift | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerStartBackup.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerStartBackup.swift index 254bc6b9..1512d361 100644 --- a/Sources/XXMessengerClient/Messenger/Functions/MessengerStartBackup.swift +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerStartBackup.swift @@ -1,3 +1,4 @@ +import Foundation import XCTestDynamicOverlay import XXClient @@ -33,24 +34,26 @@ extension MessengerStartBackup { let paramsData = try params.encode() let paramsString = String(data: paramsData, encoding: .utf8)! var didAddParams = false - func addParams() { - guard let backup = env.backup() else { return } - backup.addJSON(paramsString) - didAddParams = true - } + var semaphore: DispatchSemaphore? = .init(value: 0) let backup = try env.initializeBackup( e2eId: e2e.getId(), udId: ud.getId(), password: password, callback: .init { data in + semaphore?.wait() if !didAddParams { - addParams() + if let backup = env.backup() { + backup.addJSON(paramsString) + didAddParams = true + } } else { env.backupCallbacks.registered().handle(data) } } ) env.backup.set(backup) + semaphore?.signal() + semaphore = nil } } } -- GitLab From 5a729d9f7e577211d5abcc48deb976396ad9e345 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Thu, 29 Sep 2022 15:04:13 +0200 Subject: [PATCH 04/15] Add BackupStorage utility --- .../Utils/BackupStorage.swift | 66 +++++++++++++++++ .../Utils/BackupStorageTests.swift | 70 +++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 Sources/XXMessengerClient/Utils/BackupStorage.swift create mode 100644 Tests/XXMessengerClientTests/Utils/BackupStorageTests.swift diff --git a/Sources/XXMessengerClient/Utils/BackupStorage.swift b/Sources/XXMessengerClient/Utils/BackupStorage.swift new file mode 100644 index 00000000..b13c86cb --- /dev/null +++ b/Sources/XXMessengerClient/Utils/BackupStorage.swift @@ -0,0 +1,66 @@ +import Foundation +import XCTestDynamicOverlay +import XXClient + +public struct BackupStorage { + public struct Backup: Equatable { + public init( + date: Date, + data: Data + ) { + self.date = date + self.data = data + } + + public var date: Date + public var data: Data + } + + public typealias Observer = (Backup?) -> Void + + public var store: (Data) -> Void + public var observe: (@escaping Observer) -> Cancellable + public var remove: () -> Void +} + +extension BackupStorage { + public static func live( + now: @escaping () -> Date + ) -> BackupStorage { + var observers: [UUID: Observer] = [:] + var backup: Backup? + func notifyObservers() { + observers.values.forEach { $0(backup) } + } + + return BackupStorage( + store: { data in + backup = Backup( + date: now(), + data: data + ) + notifyObservers() + }, + observe: { observer in + let id = UUID() + observers[id] = observer + defer { observers[id]?(backup) } + return Cancellable { + observers[id] = nil + } + }, + remove: { + backup = nil + notifyObservers() + } + ) + } +} + +extension BackupStorage { + public static let unimplemented = BackupStorage( + store: XCTUnimplemented("\(Self.self).store"), + observe: XCTUnimplemented("\(Self.self).observe", placeholder: Cancellable {}), + remove: XCTUnimplemented("\(Self.self).remove") + ) +} diff --git a/Tests/XXMessengerClientTests/Utils/BackupStorageTests.swift b/Tests/XXMessengerClientTests/Utils/BackupStorageTests.swift new file mode 100644 index 00000000..3dc8272e --- /dev/null +++ b/Tests/XXMessengerClientTests/Utils/BackupStorageTests.swift @@ -0,0 +1,70 @@ +import CustomDump +import XCTest +@testable import XXMessengerClient + +final class BackupStorageTests: XCTestCase { + func testStorage() { + var now: Date = .init(0) + let storage: BackupStorage = .live( + now: { now } + ) + + var didObserveA: [BackupStorage.Backup?] = [] + let observerA = storage.observe { backup in + didObserveA.append(backup) + } + + XCTAssertNoDifference(didObserveA, [nil]) + + now = .init(1) + didObserveA = [] + let data1 = "data-1".data(using: .utf8)! + storage.store(data1) + + XCTAssertNoDifference(didObserveA, [.init(date: .init(1), data: data1)]) + + now = .init(2) + didObserveA = [] + var didObserveB: [BackupStorage.Backup?] = [] + let observerB = storage.observe { backup in + didObserveB.append(backup) + } + + XCTAssertNoDifference(didObserveA, []) + XCTAssertNoDifference(didObserveB, [.init(date: .init(1), data: data1)]) + + now = .init(3) + didObserveA = [] + didObserveB = [] + let data2 = "data-2".data(using: .utf8)! + storage.store(data2) + + XCTAssertNoDifference(didObserveA, [.init(date: .init(3), data: data2)]) + XCTAssertNoDifference(didObserveB, [.init(date: .init(3), data: data2)]) + + now = .init(4) + didObserveA = [] + didObserveB = [] + observerA.cancel() + storage.remove() + + XCTAssertNoDifference(didObserveA, []) + XCTAssertNoDifference(didObserveB, [nil]) + + now = .init(5) + didObserveA = [] + didObserveB = [] + observerB.cancel() + let data3 = "data-3".data(using: .utf8)! + storage.store(data3) + + XCTAssertNoDifference(didObserveA, []) + XCTAssertNoDifference(didObserveB, []) + } +} + +private extension Date { + init(_ timeIntervalSince1970: TimeInterval) { + self.init(timeIntervalSince1970: timeIntervalSince1970) + } +} -- GitLab From cfafee099680b350b23d78fb71ab36a12b4e9b4e Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Thu, 29 Sep 2022 15:15:52 +0200 Subject: [PATCH 05/15] Improve MessengerIsBackupRunning.unimplemented --- .../Messenger/Functions/MessengerIsBackupRunning.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerIsBackupRunning.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerIsBackupRunning.swift index 08453e23..4300ec19 100644 --- a/Sources/XXMessengerClient/Messenger/Functions/MessengerIsBackupRunning.swift +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerIsBackupRunning.swift @@ -19,6 +19,6 @@ extension MessengerIsBackupRunning { extension MessengerIsBackupRunning { public static let unimplemented = MessengerIsBackupRunning( - run: XCTUnimplemented("\(Self.self)") + run: XCTUnimplemented("\(Self.self)", placeholder: false) ) } -- GitLab From 5375dc39abf8558f60e62ec79a7aa0867d453d8f Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Thu, 29 Sep 2022 15:29:43 +0200 Subject: [PATCH 06/15] Register backup callback and update storage --- .../AppFeature/AppEnvironment+Live.swift | 2 + .../Sources/AppFeature/AppFeature.swift | 5 ++ .../AppFeatureTests/AppFeatureTests.swift | 90 +++++++++++++++---- 3 files changed, 80 insertions(+), 17 deletions(-) diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index 0ed4d6b5..b6eab6a6 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -28,6 +28,7 @@ extension AppEnvironment { handleConfirm: .live(db: dbManager.getDB), handleReset: .live(db: dbManager.getDB) ) + let backupStorage = BackupStorage.live(now: Date.init) let mainQueue = DispatchQueue.main.eraseToAnyScheduler() let bgQueue = DispatchQueue.global(qos: .background).eraseToAnyScheduler() @@ -91,6 +92,7 @@ extension AppEnvironment { messenger: messenger, db: dbManager.getDB ), + backupStorage: backupStorage, log: .live(), mainQueue: mainQueue, bgQueue: bgQueue, diff --git a/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift b/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift index caae3368..145da907 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift @@ -50,6 +50,7 @@ struct AppEnvironment { var messenger: Messenger var authHandler: AuthCallbackHandler var messageListener: MessageListenerHandler + var backupStorage: BackupStorage var log: Logger var mainQueue: AnySchedulerOf<DispatchQueue> var bgQueue: AnySchedulerOf<DispatchQueue> @@ -64,6 +65,7 @@ extension AppEnvironment { messenger: .unimplemented, authHandler: .unimplemented, messageListener: .unimplemented, + backupStorage: .unimplemented, log: .unimplemented, mainQueue: .unimplemented, bgQueue: .unimplemented, @@ -94,6 +96,9 @@ let appReducer = Reducer<AppState, AppAction, AppEnvironment> cancellables.append(env.messageListener(onError: { error in env.log(.error(error as NSError)) })) + cancellables.append(env.messenger.registerBackupCallback(.init { data in + env.backupStorage.store(data) + })) let isLoaded = env.messenger.isLoaded() let isCreated = env.messenger.isCreated() diff --git a/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift b/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift index 5bda4eb3..098a026c 100644 --- a/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift +++ b/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift @@ -10,7 +10,7 @@ import XXClient final class AppFeatureTests: XCTestCase { func testStartWithoutMessengerCreated() { - var actions: [Action] = [] + var actions: [Action]! let store = TestStore( initialState: AppState(), @@ -34,24 +34,29 @@ final class AppFeatureTests: XCTestCase { actions.append(.didStartMessageListener) return Cancellable {} } + store.environment.messenger.registerBackupCallback.run = { _ in + actions.append(.didRegisterBackupCallback) + return Cancellable {} + } + actions = [] store.send(.start) store.receive(.set(\.$screen, .welcome(WelcomeState()))) { $0.screen = .welcome(WelcomeState()) } - XCTAssertNoDifference(actions, [ .didMakeDB, .didStartAuthHandler, .didStartMessageListener, + .didRegisterBackupCallback, ]) store.send(.stop) } func testStartWithMessengerCreated() { - var actions: [Action] = [] + var actions: [Action]! let store = TestStore( initialState: AppState(), @@ -78,17 +83,22 @@ final class AppFeatureTests: XCTestCase { actions.append(.didStartMessageListener) return Cancellable {} } + store.environment.messenger.registerBackupCallback.run = { _ in + actions.append(.didRegisterBackupCallback) + return Cancellable {} + } + actions = [] store.send(.start) store.receive(.set(\.$screen, .home(HomeState()))) { $0.screen = .home(HomeState()) } - XCTAssertNoDifference(actions, [ .didMakeDB, .didStartAuthHandler, .didStartMessageListener, + .didRegisterBackupCallback, .didLoadMessenger, ]) @@ -96,7 +106,7 @@ final class AppFeatureTests: XCTestCase { } func testWelcomeFinished() { - var actions: [Action] = [] + var actions: [Action]! let store = TestStore( initialState: AppState( @@ -122,7 +132,12 @@ final class AppFeatureTests: XCTestCase { actions.append(.didStartMessageListener) return Cancellable {} } + store.environment.messenger.registerBackupCallback.run = { _ in + actions.append(.didRegisterBackupCallback) + return Cancellable {} + } + actions = [] store.send(.welcome(.finished)) { $0.screen = .loading } @@ -130,10 +145,10 @@ final class AppFeatureTests: XCTestCase { store.receive(.set(\.$screen, .home(HomeState()))) { $0.screen = .home(HomeState()) } - XCTAssertNoDifference(actions, [ .didStartAuthHandler, .didStartMessageListener, + .didRegisterBackupCallback, .didLoadMessenger, ]) @@ -141,7 +156,7 @@ final class AppFeatureTests: XCTestCase { } func testRestoreFinished() { - var actions: [Action] = [] + var actions: [Action]! let store = TestStore( initialState: AppState( @@ -167,7 +182,12 @@ final class AppFeatureTests: XCTestCase { actions.append(.didStartMessageListener) return Cancellable {} } + store.environment.messenger.registerBackupCallback.run = { _ in + actions.append(.didRegisterBackupCallback) + return Cancellable {} + } + actions = [] store.send(.restore(.finished)) { $0.screen = .loading } @@ -175,10 +195,10 @@ final class AppFeatureTests: XCTestCase { store.receive(.set(\.$screen, .home(HomeState()))) { $0.screen = .home(HomeState()) } - XCTAssertNoDifference(actions, [ .didStartAuthHandler, .didStartMessageListener, + .didRegisterBackupCallback, .didLoadMessenger, ]) @@ -186,7 +206,7 @@ final class AppFeatureTests: XCTestCase { } func testHomeDidDeleteAccount() { - var actions: [Action] = [] + var actions: [Action]! let store = TestStore( initialState: AppState( @@ -209,7 +229,12 @@ final class AppFeatureTests: XCTestCase { actions.append(.didStartMessageListener) return Cancellable {} } + store.environment.messenger.registerBackupCallback.run = { _ in + actions.append(.didRegisterBackupCallback) + return Cancellable {} + } + actions = [] store.send(.home(.deleteAccount(.success))) { $0.screen = .loading } @@ -217,10 +242,10 @@ final class AppFeatureTests: XCTestCase { store.receive(.set(\.$screen, .welcome(WelcomeState()))) { $0.screen = .welcome(WelcomeState()) } - XCTAssertNoDifference(actions, [ .didStartAuthHandler, .didStartMessageListener, + .didRegisterBackupCallback, ]) store.send(.stop) @@ -284,7 +309,7 @@ final class AppFeatureTests: XCTestCase { struct Failure: Error {} let error = Failure() - var actions: [Action] = [] + var actions: [Action]! let store = TestStore( initialState: AppState(), @@ -306,7 +331,12 @@ final class AppFeatureTests: XCTestCase { actions.append(.didStartMessageListener) return Cancellable {} } + store.environment.messenger.registerBackupCallback.run = { _ in + actions.append(.didRegisterBackupCallback) + return Cancellable {} + } + actions = [] store.send(.start) store.receive(.set(\.$screen, .failure(error.localizedDescription))) { @@ -316,15 +346,17 @@ final class AppFeatureTests: XCTestCase { XCTAssertNoDifference(actions, [ .didStartAuthHandler, .didStartMessageListener, + .didRegisterBackupCallback, ]) store.send(.stop) } func testStartHandlersAndListeners() { - var actions: [Action] = [] + var actions: [Action]! var authHandlerOnError: [AuthCallbackHandler.OnError] = [] var messageListenerOnError: [MessageListenerHandler.OnError] = [] + var backupCallback: [UpdateBackupFunc] = [] let store = TestStore( initialState: AppState(), @@ -351,22 +383,33 @@ final class AppFeatureTests: XCTestCase { actions.append(.didCancelMessageListener) } } + store.environment.messenger.registerBackupCallback.run = { callback in + backupCallback.append(callback) + actions.append(.didRegisterBackupCallback) + return Cancellable { + actions.append(.didCancelBackupCallback) + } + } store.environment.log.run = { msg, _, _, _ in actions.append(.didLog(msg)) } + store.environment.backupStorage.store = { data in + actions.append(.didStoreBackup(data)) + } + actions = [] store.send(.start) store.receive(.set(\.$screen, .home(HomeState()))) { $0.screen = .home(HomeState()) } - XCTAssertNoDifference(actions, [ .didStartAuthHandler, .didStartMessageListener, + .didRegisterBackupCallback, ]) - actions = [] + actions = [] store.send(.start) { $0.screen = .loading } @@ -374,15 +417,16 @@ final class AppFeatureTests: XCTestCase { store.receive(.set(\.$screen, .home(HomeState()))) { $0.screen = .home(HomeState()) } - XCTAssertNoDifference(actions, [ .didCancelAuthHandler, .didCancelMessageListener, + .didCancelBackupCallback, .didStartAuthHandler, .didStartMessageListener, + .didRegisterBackupCallback, ]) - actions = [] + actions = [] struct AuthError: Error {} let authError = AuthError() authHandlerOnError.first?(authError) @@ -390,8 +434,8 @@ final class AppFeatureTests: XCTestCase { XCTAssertNoDifference(actions, [ .didLog(.error(authError as NSError)) ]) - actions = [] + actions = [] struct MessageError: Error {} let messageError = MessageError() messageListenerOnError.first?(messageError) @@ -399,13 +443,22 @@ final class AppFeatureTests: XCTestCase { XCTAssertNoDifference(actions, [ .didLog(.error(messageError as NSError)) ]) + actions = [] + let backupData = "backup".data(using: .utf8)! + backupCallback.first?.handle(backupData) + XCTAssertNoDifference(actions, [ + .didStoreBackup(backupData), + ]) + + actions = [] store.send(.stop) XCTAssertNoDifference(actions, [ .didCancelAuthHandler, .didCancelMessageListener, + .didCancelBackupCallback, ]) } } @@ -414,8 +467,11 @@ private enum Action: Equatable { case didMakeDB case didStartAuthHandler case didStartMessageListener + case didRegisterBackupCallback case didLoadMessenger case didCancelAuthHandler case didCancelMessageListener + case didCancelBackupCallback case didLog(Logger.Message) + case didStoreBackup(Data) } -- GitLab From df699f4674de8c58c4b36d617d70af298dcf3337 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Thu, 29 Sep 2022 17:31:34 +0200 Subject: [PATCH 07/15] Implement example backup feature --- Examples/xx-messenger/Package.swift | 2 + .../AppFeature/AppEnvironment+Live.swift | 8 +- .../Sources/BackupFeature/Alerts.swift | 10 + .../Sources/BackupFeature/BackupFeature.swift | 218 ++++++++- .../Sources/BackupFeature/BackupView.swift | 207 +++++++- .../BackupFeatureTests.swift | 441 +++++++++++++++++- 6 files changed, 873 insertions(+), 13 deletions(-) create mode 100644 Examples/xx-messenger/Sources/BackupFeature/Alerts.swift diff --git a/Examples/xx-messenger/Package.swift b/Examples/xx-messenger/Package.swift index 8fa55d88..4668d629 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -117,8 +117,10 @@ let package = Package( .target( name: "BackupFeature", dependencies: [ + .target(name: "AppCore"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), + .product(name: "XXModels", package: "client-ios-db"), ], swiftSettings: swiftSettings ), diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index b6eab6a6..a81b863a 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -154,7 +154,13 @@ extension AppEnvironment { ) }, backup: { - BackupEnvironment() + BackupEnvironment( + messenger: messenger, + db: dbManager.getDB, + backupStorage: backupStorage, + mainQueue: mainQueue, + bgQueue: bgQueue + ) } ) } diff --git a/Examples/xx-messenger/Sources/BackupFeature/Alerts.swift b/Examples/xx-messenger/Sources/BackupFeature/Alerts.swift new file mode 100644 index 00000000..2bd95f77 --- /dev/null +++ b/Examples/xx-messenger/Sources/BackupFeature/Alerts.swift @@ -0,0 +1,10 @@ +import ComposableArchitecture + +extension AlertState where Action == BackupAction { + public static func error(_ error: Error) -> AlertState<BackupAction> { + AlertState( + title: TextState("Error"), + message: TextState(error.localizedDescription) + ) + } +} diff --git a/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift b/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift index eebbbc0b..128c375b 100644 --- a/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift +++ b/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift @@ -1,28 +1,232 @@ +import AppCore +import Combine import ComposableArchitecture -import XCTestDynamicOverlay +import Foundation +import XXClient +import XXMessengerClient +import XXModels public struct BackupState: Equatable { - public init() {} + public enum Error: String, Swift.Error, Equatable { + case dbContactNotFound + case dbContactUsernameMissing + } + + public init( + isRunning: Bool = false, + isStarting: Bool = false, + isResuming: Bool = false, + isStopping: Bool = false, + backup: BackupStorage.Backup? = nil, + alert: AlertState<BackupAction>? = nil, + passphrase: String = "", + isExporting: Bool = false, + exportData: Data? = nil + ) { + self.isRunning = isRunning + self.isStarting = isStarting + self.isResuming = isResuming + self.isStopping = isStopping + self.backup = backup + self.alert = alert + self.passphrase = passphrase + self.isExporting = isExporting + self.exportData = exportData + } + + public var isRunning: Bool + public var isStarting: Bool + public var isResuming: Bool + public var isStopping: Bool + public var backup: BackupStorage.Backup? + public var alert: AlertState<BackupAction>? + @BindableState public var passphrase: String + @BindableState public var isExporting: Bool + public var exportData: Data? } -public enum BackupAction: Equatable { - case start +public enum BackupAction: Equatable, BindableAction { + case task + case cancelTask + case startTapped + case resumeTapped + case stopTapped + case exportTapped + case alertDismissed + case backupUpdated(BackupStorage.Backup?) + case didStart(failure: NSError?) + case didResume(failure: NSError?) + case didStop(failure: NSError?) + case didExport(failure: NSError?) + case binding(BindingAction<BackupState>) } public struct BackupEnvironment { - public init() {} + public init( + messenger: Messenger, + db: DBManagerGetDB, + backupStorage: BackupStorage, + mainQueue: AnySchedulerOf<DispatchQueue>, + bgQueue: AnySchedulerOf<DispatchQueue> + ) { + self.messenger = messenger + self.db = db + self.backupStorage = backupStorage + self.mainQueue = mainQueue + self.bgQueue = bgQueue + } + + public var messenger: Messenger + public var db: DBManagerGetDB + public var backupStorage: BackupStorage + public var mainQueue: AnySchedulerOf<DispatchQueue> + public var bgQueue: AnySchedulerOf<DispatchQueue> } #if DEBUG extension BackupEnvironment { - public static let unimplemented = BackupEnvironment() + public static let unimplemented = BackupEnvironment( + messenger: .unimplemented, + db: .unimplemented, + backupStorage: .unimplemented, + mainQueue: .unimplemented, + bgQueue: .unimplemented + ) } #endif public let backupReducer = Reducer<BackupState, BackupAction, BackupEnvironment> { state, action, env in + enum TaskEffectId {} + switch action { - case .start: + case .task: + state.isRunning = env.messenger.isBackupRunning() + return Effect.run { subscriber in + let cancellable = env.backupStorage.observe { backup in + subscriber.send(.backupUpdated(backup)) + } + return AnyCancellable { cancellable.cancel() } + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + .cancellable(id: TaskEffectId.self, cancelInFlight: true) + + case .cancelTask: + return .cancel(id: TaskEffectId.self) + + case .startTapped: + state.isStarting = true + return Effect.run { [state] subscriber in + do { + let e2e: E2E = try env.messenger.e2e.tryGet() + let contactID = try e2e.getContact().getId() + let db = try env.db() + let query = XXModels.Contact.Query(id: [contactID]) + guard let contact = try db.fetchContacts(query).first else { + throw BackupState.Error.dbContactNotFound + } + guard let username = contact.username else { + throw BackupState.Error.dbContactUsernameMissing + } + try env.messenger.startBackup( + password: state.passphrase, + params: BackupParams(username: username) + ) + subscriber.send(.didStart(failure: nil)) + } catch { + subscriber.send(.didStart(failure: error as NSError)) + } + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + + case .resumeTapped: + state.isResuming = true + return Effect.run { subscriber in + do { + try env.messenger.resumeBackup() + subscriber.send(.didResume(failure: nil)) + } catch { + subscriber.send(.didResume(failure: error as NSError)) + } + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + + case .stopTapped: + state.isStopping = true + return Effect.run { subscriber in + do { + try env.messenger.stopBackup() + env.backupStorage.remove() + subscriber.send(.didStop(failure: nil)) + } catch { + subscriber.send(.didStop(failure: error as NSError)) + } + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + + case .exportTapped: + state.isExporting = true + state.exportData = state.backup?.data + return .none + + case .alertDismissed: + state.alert = nil + return .none + + case .backupUpdated(let backup): + state.backup = backup + return .none + + case .didStart(let failure): + state.isRunning = env.messenger.isBackupRunning() + state.isStarting = false + if let failure { + state.alert = .error(failure) + } else { + state.passphrase = "" + } + return .none + + case .didResume(let failure): + state.isRunning = env.messenger.isBackupRunning() + state.isResuming = false + if let failure { + state.alert = .error(failure) + } + return .none + + case .didStop(let failure): + state.isRunning = env.messenger.isBackupRunning() + state.isStopping = false + if let failure { + state.alert = .error(failure) + } + return .none + + case .didExport(let failure): + state.isExporting = false + state.exportData = nil + if let failure { + state.alert = .error(failure) + } + return .none + + case .binding(_): return .none } } +.binding() diff --git a/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift b/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift index 874e6137..13873f89 100644 --- a/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift +++ b/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift @@ -1,5 +1,6 @@ import ComposableArchitecture import SwiftUI +import UniformTypeIdentifiers public struct BackupView: View { public init(store: Store<BackupState, BackupAction>) { @@ -9,20 +10,220 @@ public struct BackupView: View { let store: Store<BackupState, BackupAction> struct ViewState: Equatable { - init(state: BackupState) {} + struct Backup: Equatable { + var date: Date + var size: Int + } + + init(state: BackupState) { + isRunning = state.isRunning + isStarting = state.isStarting + isResuming = state.isResuming + isStopping = state.isStopping + backup = state.backup.map { backup in + Backup(date: backup.date, size: backup.data.count) + } + passphrase = state.passphrase + isExporting = state.isExporting + exportData = state.exportData + } + + var isRunning: Bool + var isStarting: Bool + var isResuming: Bool + var isStopping: Bool + var isLoading: Bool { isStarting || isResuming || isStopping } + var backup: Backup? + var passphrase: String + var isExporting: Bool + var exportData: Data? } public var body: some View { WithViewStore(store, observe: ViewState.init) { viewStore in Form { - + Group { + if !viewStore.isRunning { + newBackupSection(viewStore) + } + if viewStore.isRunning || viewStore.backup != nil { + backupSection(viewStore) + } + } + .disabled(viewStore.isLoading) + .alert( + store.scope(state: \.alert), + dismiss: .alertDismissed + ) } .navigationTitle("Backup") .task { - viewStore.send(.start) + await viewStore.send(.task).finish() + } + } + } + + @ViewBuilder func newBackupSection( + _ viewStore: ViewStore<ViewState, BackupAction> + ) -> some View { + Section { + SecureField( + text: viewStore.binding( + get: \.passphrase, + send: { .set(\.$passphrase, $0) } + ), + prompt: Text("Backup passphrase"), + label: { Text("Backup passphrase") } + ) + Button { + viewStore.send(.startTapped) + } label: { + HStack { + Text("Start") + Spacer() + if viewStore.isStarting { + ProgressView() + } else { + Image(systemName: "play.fill") + } + } + } + } header: { + Text("New backup") + } + } + + @ViewBuilder func backupSection( + _ viewStore: ViewStore<ViewState, BackupAction> + ) -> some View { + Section { + backupView(viewStore) + stopView(viewStore) + resumeView(viewStore) + } header: { + Text("Backup") + } + } + + @ViewBuilder func backupView( + _ viewStore: ViewStore<ViewState, BackupAction> + ) -> some View { + if let backup = viewStore.backup { + HStack { + Text("Date") + Spacer() + Text(backup.date.formatted()) + } + HStack { + Text("Size") + Spacer() + Text(format(bytesCount: backup.size)) + } + Button { + viewStore.send(.exportTapped) + } label: { + HStack { + Text("Export") + Spacer() + if viewStore.isExporting { + ProgressView() + } else { + Image(systemName: "square.and.arrow.up") + } + } + } + .disabled(viewStore.isExporting) + .fileExporter( + isPresented: viewStore.binding( + get: \.isExporting, + send: { .set(\.$isExporting, $0) } + ), + document: viewStore.exportData.map(ExportedDocument.init(data:)), + contentType: .data, + defaultFilename: "backup.xxm", + onCompletion: { result in + switch result { + case .success(_): + viewStore.send(.didExport(failure: nil)) + case .failure(let error): + viewStore.send(.didExport(failure: error as NSError?)) + } + } + ) + } else { + Text("No backup") + } + } + + @ViewBuilder func stopView( + _ viewStore: ViewStore<ViewState, BackupAction> + ) -> some View { + if viewStore.isRunning { + Button { + viewStore.send(.stopTapped) + } label: { + HStack { + Text("Stop") + Spacer() + if viewStore.isStopping { + ProgressView() + } else { + Image(systemName: "stop.fill") + } + } + } + } + } + + @ViewBuilder func resumeView( + _ viewStore: ViewStore<ViewState, BackupAction> + ) -> some View { + if !viewStore.isRunning, viewStore.backup != nil { + Button { + viewStore.send(.resumeTapped) + } label: { + HStack { + Text("Resume") + Spacer() + if viewStore.isResuming { + ProgressView() + } else { + Image(systemName: "playpause.fill") + } + } } } } + + func format(bytesCount bytes: Int) -> String { + let formatter = ByteCountFormatter() + formatter.allowedUnits = [.useMB, .useKB] + formatter.countStyle = .binary + return formatter.string(fromByteCount: Int64(bytes)) + } +} + +private struct ExportedDocument: FileDocument { + enum Error: Swift.Error { + case notAvailable + } + + static var readableContentTypes: [UTType] = [] + static var writableContentTypes: [UTType] = [.data] + + var data: Data + + init(data: Data) { + self.data = data + } + + init(configuration: ReadConfiguration) throws { + throw Error.notAvailable + } + + func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { + FileWrapper(regularFileWithContents: data) + } } #if DEBUG diff --git a/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift b/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift index a95240ca..ebb1510b 100644 --- a/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift +++ b/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift @@ -1,15 +1,452 @@ import ComposableArchitecture import XCTest +import XXClient +import XXMessengerClient +import XXModels @testable import BackupFeature final class BackupFeatureTests: XCTestCase { - func testStart() { + func testTask() { + var isBackupRunning: [Bool] = [false] + var observers: [UUID: BackupStorage.Observer] = [:] + + let store = TestStore( + initialState: BackupState(), + reducer: backupReducer, + environment: .unimplemented + ) + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.isBackupRunning.run = { + isBackupRunning.removeFirst() + } + store.environment.backupStorage.observe = { + let id = UUID() + observers[id] = $0 + return Cancellable { observers[id] = nil } + } + + store.send(.task) + + XCTAssertNoDifference(observers.count, 1) + + let backup = BackupStorage.Backup( + date: .init(timeIntervalSince1970: 1), + data: "backup".data(using: .utf8)! + ) + observers.values.forEach { $0(backup) } + + store.receive(.backupUpdated(backup)) { + $0.backup = backup + } + + observers.values.forEach { $0(nil) } + + store.receive(.backupUpdated(nil)) { + $0.backup = nil + } + + store.send(.cancelTask) + + XCTAssertNoDifference(observers.count, 0) + } + + func testStartBackup() { + var actions: [Action]! + var isBackupRunning: [Bool] = [true] + let contactID = "contact-id".data(using: .utf8)! + let dbContact = XXModels.Contact( + id: contactID, + username: "db-contact-username" + ) + let passphrase = "backup-password" + + let store = TestStore( + initialState: BackupState(), + reducer: backupReducer, + environment: .unimplemented + ) + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.startBackup.run = { passphrase, params in + actions.append(.didStartBackup(passphrase: passphrase, params: params)) + } + store.environment.messenger.isBackupRunning.run = { + isBackupRunning.removeFirst() + } + store.environment.messenger.e2e.get = { + var e2e: E2E = .unimplemented + e2e.getContact.run = { + var contact: XXClient.Contact = .unimplemented(Data()) + contact.getIdFromContact.run = { _ in contactID } + return contact + } + return e2e + } + store.environment.db.run = { + var db: Database = .unimplemented + db.fetchContacts.run = { _ in return [dbContact] } + return db + } + + actions = [] + store.send(.set(\.$passphrase, passphrase)) { + $0.passphrase = passphrase + } + + XCTAssertNoDifference(actions, []) + + actions = [] + store.send(.startTapped) { + $0.isStarting = true + } + + XCTAssertNoDifference(actions, [ + .didStartBackup( + passphrase: passphrase, + params: .init(username: dbContact.username!) + ) + ]) + + store.receive(.didStart(failure: nil)) { + $0.isRunning = true + $0.isStarting = false + $0.passphrase = "" + } + } + + func testStartBackupWithoutDbContact() { + var isBackupRunning: [Bool] = [false] + let contactID = "contact-id".data(using: .utf8)! + + let store = TestStore( + initialState: BackupState( + passphrase: "1234" + ), + reducer: backupReducer, + environment: .unimplemented + ) + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.isBackupRunning.run = { + isBackupRunning.removeFirst() + } + store.environment.messenger.e2e.get = { + var e2e: E2E = .unimplemented + e2e.getContact.run = { + var contact: XXClient.Contact = .unimplemented(Data()) + contact.getIdFromContact.run = { _ in contactID } + return contact + } + return e2e + } + store.environment.db.run = { + var db: Database = .unimplemented + db.fetchContacts.run = { _ in [] } + return db + } + + store.send(.startTapped) { + $0.isStarting = true + } + + let failure = BackupState.Error.dbContactNotFound + store.receive(.didStart(failure: failure as NSError)) { + $0.isRunning = false + $0.isStarting = false + $0.alert = .error(failure) + } + } + + func testStartBackupWithoutDbContactUsername() { + var isBackupRunning: [Bool] = [false] + let contactID = "contact-id".data(using: .utf8)! + let dbContact = XXModels.Contact( + id: contactID, + username: nil + ) + + let store = TestStore( + initialState: BackupState( + passphrase: "1234" + ), + reducer: backupReducer, + environment: .unimplemented + ) + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.isBackupRunning.run = { + isBackupRunning.removeFirst() + } + store.environment.messenger.e2e.get = { + var e2e: E2E = .unimplemented + e2e.getContact.run = { + var contact: XXClient.Contact = .unimplemented(Data()) + contact.getIdFromContact.run = { _ in contactID } + return contact + } + return e2e + } + store.environment.db.run = { + var db: Database = .unimplemented + db.fetchContacts.run = { _ in [dbContact] } + return db + } + + store.send(.startTapped) { + $0.isStarting = true + } + + let failure = BackupState.Error.dbContactUsernameMissing + store.receive(.didStart(failure: failure as NSError)) { + $0.isRunning = false + $0.isStarting = false + $0.alert = .error(failure) + } + } + + func testStartBackupFailure() { + struct Failure: Error {} + let failure = Failure() + var isBackupRunning: [Bool] = [false] + let contactID = "contact-id".data(using: .utf8)! + let dbContact = XXModels.Contact( + id: contactID, + username: "db-contact-username" + ) + + let store = TestStore( + initialState: BackupState( + passphrase: "1234" + ), + reducer: backupReducer, + environment: .unimplemented + ) + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.startBackup.run = { _, _ in + throw failure + } + store.environment.messenger.isBackupRunning.run = { + isBackupRunning.removeFirst() + } + store.environment.messenger.e2e.get = { + var e2e: E2E = .unimplemented + e2e.getContact.run = { + var contact: XXClient.Contact = .unimplemented(Data()) + contact.getIdFromContact.run = { _ in contactID } + return contact + } + return e2e + } + store.environment.db.run = { + var db: Database = .unimplemented + db.fetchContacts.run = { _ in return [dbContact] } + return db + } + + store.send(.startTapped) { + $0.isStarting = true + } + + store.receive(.didStart(failure: failure as NSError)) { + $0.isRunning = false + $0.isStarting = false + $0.alert = .error(failure) + } + } + + func testResumeBackup() { + var actions: [Action]! + var isBackupRunning: [Bool] = [true] + + let store = TestStore( + initialState: BackupState(), + reducer: backupReducer, + environment: .unimplemented + ) + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.resumeBackup.run = { + actions.append(.didResumeBackup) + } + store.environment.messenger.isBackupRunning.run = { + isBackupRunning.removeFirst() + } + + actions = [] + store.send(.resumeTapped) { + $0.isResuming = true + } + + XCTAssertNoDifference(actions, [.didResumeBackup]) + + actions = [] + store.receive(.didResume(failure: nil)) { + $0.isRunning = true + $0.isResuming = false + } + + XCTAssertNoDifference(actions, []) + } + + func testResumeBackupFailure() { + struct Failure: Error {} + let failure = Failure() + var isBackupRunning: [Bool] = [false] + + let store = TestStore( + initialState: BackupState(), + reducer: backupReducer, + environment: .unimplemented + ) + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.resumeBackup.run = { + throw failure + } + store.environment.messenger.isBackupRunning.run = { + isBackupRunning.removeFirst() + } + + store.send(.resumeTapped) { + $0.isResuming = true + } + + store.receive(.didResume(failure: failure as NSError)) { + $0.isRunning = false + $0.isResuming = false + $0.alert = .error(failure) + } + } + + func testStopBackup() { + var actions: [Action]! + var isBackupRunning: [Bool] = [false] + + let store = TestStore( + initialState: BackupState(), + reducer: backupReducer, + environment: .unimplemented + ) + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.stopBackup.run = { + actions.append(.didStopBackup) + } + store.environment.messenger.isBackupRunning.run = { + isBackupRunning.removeFirst() + } + store.environment.backupStorage.remove = { + actions.append(.didRemoveBackup) + } + + actions = [] + store.send(.stopTapped) { + $0.isStopping = true + } + + XCTAssertNoDifference(actions, [ + .didStopBackup, + .didRemoveBackup, + ]) + + actions = [] + store.receive(.didStop(failure: nil)) { + $0.isRunning = false + $0.isStopping = false + } + + XCTAssertNoDifference(actions, []) + } + + func testStopBackupFailure() { + struct Failure: Error {} + let failure = Failure() + var isBackupRunning: [Bool] = [true] + let store = TestStore( initialState: BackupState(), reducer: backupReducer, environment: .unimplemented ) + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.stopBackup.run = { + throw failure + } + store.environment.messenger.isBackupRunning.run = { + isBackupRunning.removeFirst() + } - store.send(.start) + store.send(.stopTapped) { + $0.isStopping = true + } + + store.receive(.didStop(failure: failure as NSError)) { + $0.isRunning = true + $0.isStopping = false + $0.alert = .error(failure) + } } + + func testAlertDismissed() { + let store = TestStore( + initialState: BackupState( + alert: .error(NSError(domain: "test", code: 0)) + ), + reducer: backupReducer, + environment: .unimplemented + ) + + store.send(.alertDismissed) { + $0.alert = nil + } + } + + func testExportBackup() { + let backupData = "backup-data".data(using: .utf8)! + + let store = TestStore( + initialState: BackupState( + backup: .init( + date: Date(), + data: backupData + ) + ), + reducer: backupReducer, + environment: .unimplemented + ) + + store.send(.exportTapped) { + $0.isExporting = true + $0.exportData = backupData + } + + store.send(.didExport(failure: nil)) { + $0.isExporting = false + $0.exportData = nil + } + + store.send(.exportTapped) { + $0.isExporting = true + $0.exportData = backupData + } + + let failure = NSError(domain: "test", code: 0) + store.send(.didExport(failure: failure)) { + $0.isExporting = false + $0.exportData = nil + $0.alert = .error(failure) + } + } +} + +private enum Action: Equatable { + case didRegisterObserver + case didStartBackup(passphrase: String, params: BackupParams) + case didResumeBackup + case didStopBackup + case didRemoveBackup + case didFetchContacts(XXModels.Contact.Query) } -- GitLab From c4fdba3cc937c2602e978f8bb72a6821ca56df46 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Thu, 29 Sep 2022 18:18:54 +0200 Subject: [PATCH 08/15] Refactor --- .../Messenger/Functions/MessengerCreate.swift | 2 +- .../Messenger/Functions/MessengerDestroy.swift | 2 +- .../Functions/MessengerRestoreBackup.swift | 2 +- .../Utils/MessengerFileManager.swift | 6 +++--- .../Messenger/Functions/MessengerCreateTests.swift | 14 +++++++------- .../Functions/MessengerDestroyTests.swift | 14 +++++++------- .../Functions/MessengerRestoreBackupTests.swift | 6 +++--- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerCreate.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerCreate.swift index ed9d193d..bee7f472 100644 --- a/Sources/XXMessengerClient/Messenger/Functions/MessengerCreate.swift +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerCreate.swift @@ -16,7 +16,7 @@ extension MessengerCreate { let password = env.generateSecret() try env.passwordStorage.save(password) let storageDir = env.storageDir - try env.fileManager.removeDirectory(storageDir) + try env.fileManager.removeItem(storageDir) try env.fileManager.createDirectory(storageDir) try env.newCMix( ndfJSON: String(data: ndfData, encoding: .utf8)!, diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerDestroy.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerDestroy.swift index 7d6932ba..89c472ce 100644 --- a/Sources/XXMessengerClient/Messenger/Functions/MessengerDestroy.swift +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerDestroy.swift @@ -23,7 +23,7 @@ extension MessengerDestroy { env.e2e.set(nil) env.cMix.set(nil) env.isListeningForMessages.set(false) - try env.fileManager.removeDirectory(env.storageDir) + try env.fileManager.removeItem(env.storageDir) try env.passwordStorage.remove() } } diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerRestoreBackup.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerRestoreBackup.swift index b196faca..e1f0ec10 100644 --- a/Sources/XXMessengerClient/Messenger/Functions/MessengerRestoreBackup.swift +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerRestoreBackup.swift @@ -33,7 +33,7 @@ extension MessengerRestoreBackup { let ndfData = try env.downloadNDF(env.ndfEnvironment) let password = env.generateSecret() try env.passwordStorage.save(password) - try env.fileManager.removeDirectory(storageDir) + try env.fileManager.removeItem(storageDir) try env.fileManager.createDirectory(storageDir) let report = try env.newCMixFromBackup( ndfJSON: String(data: ndfData, encoding: .utf8)!, diff --git a/Sources/XXMessengerClient/Utils/MessengerFileManager.swift b/Sources/XXMessengerClient/Utils/MessengerFileManager.swift index 700990ea..9e2c1590 100644 --- a/Sources/XXMessengerClient/Utils/MessengerFileManager.swift +++ b/Sources/XXMessengerClient/Utils/MessengerFileManager.swift @@ -3,7 +3,7 @@ import XCTestDynamicOverlay public struct MessengerFileManager { public var isDirectoryEmpty: (String) -> Bool - public var removeDirectory: (String) throws -> Void + public var removeItem: (String) throws -> Void public var createDirectory: (String) throws -> Void } @@ -16,7 +16,7 @@ extension MessengerFileManager { let contents = try? fileManager.contentsOfDirectory(atPath: path) return contents?.isEmpty ?? true }, - removeDirectory: { path in + removeItem: { path in if fileManager.fileExists(atPath: path) { try fileManager.removeItem(atPath: path) } @@ -34,7 +34,7 @@ extension MessengerFileManager { extension MessengerFileManager { public static let unimplemented = MessengerFileManager( isDirectoryEmpty: XCTUnimplemented("\(Self.self).isDirectoryEmpty", placeholder: false), - removeDirectory: XCTUnimplemented("\(Self.self).removeDirectory"), + removeItem: XCTUnimplemented("\(Self.self).removeItem"), createDirectory: XCTUnimplemented("\(Self.self).createDirectory") ) } diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerCreateTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerCreateTests.swift index 2180dda3..0c8ce3ba 100644 --- a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerCreateTests.swift +++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerCreateTests.swift @@ -15,7 +15,7 @@ final class MessengerCreateTests: XCTestCase { var didDownloadNDF: [NDFEnvironment] = [] var didGenerateSecret: [Int] = [] var didSavePassword: [Data] = [] - var didRemoveDirectory: [String] = [] + var didRemoveItem: [String] = [] var didCreateDirectory: [String] = [] var didNewCMix: [DidNewCMix] = [] @@ -37,8 +37,8 @@ final class MessengerCreateTests: XCTestCase { didSavePassword.append(password) } env.storageDir = storageDir - env.fileManager.removeDirectory = { path in - didRemoveDirectory.append(path) + env.fileManager.removeItem = { path in + didRemoveItem.append(path) } env.fileManager.createDirectory = { path in didCreateDirectory.append(path) @@ -58,7 +58,7 @@ final class MessengerCreateTests: XCTestCase { XCTAssertNoDifference(didDownloadNDF, [.unimplemented]) XCTAssertNoDifference(didGenerateSecret, [32]) XCTAssertNoDifference(didSavePassword, [password]) - XCTAssertNoDifference(didRemoveDirectory, [storageDir]) + XCTAssertNoDifference(didRemoveItem, [storageDir]) XCTAssertNoDifference(didCreateDirectory, [storageDir]) XCTAssertNoDifference(didNewCMix, [.init( ndfJSON: String(data: ndf, encoding: .utf8)!, @@ -108,7 +108,7 @@ final class MessengerCreateTests: XCTestCase { env.generateSecret.run = { _ in "password".data(using: .utf8)! } env.passwordStorage.save = { _ in } env.storageDir = "storage-dir" - env.fileManager.removeDirectory = { _ in throw error } + env.fileManager.removeItem = { _ in throw error } let create: MessengerCreate = .live(env) XCTAssertThrowsError(try create()) { err in @@ -126,7 +126,7 @@ final class MessengerCreateTests: XCTestCase { env.generateSecret.run = { _ in "password".data(using: .utf8)! } env.passwordStorage.save = { _ in } env.storageDir = "storage-dir" - env.fileManager.removeDirectory = { _ in } + env.fileManager.removeItem = { _ in } env.fileManager.createDirectory = { _ in throw error } let create: MessengerCreate = .live(env) @@ -145,7 +145,7 @@ final class MessengerCreateTests: XCTestCase { env.generateSecret.run = { _ in "password".data(using: .utf8)! } env.passwordStorage.save = { _ in } env.storageDir = "storage-dir" - env.fileManager.removeDirectory = { _ in } + env.fileManager.removeItem = { _ in } env.fileManager.createDirectory = { _ in } env.newCMix.run = { _, _, _, _ in throw error } let create: MessengerCreate = .live(env) diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerDestroyTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerDestroyTests.swift index ccf999e7..99a52e84 100644 --- a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerDestroyTests.swift +++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerDestroyTests.swift @@ -9,7 +9,7 @@ final class MessengerDestroyTests: XCTestCase { var hasRunningProcesses: [Bool] = [true, true, false] var didStopNetworkFollower = 0 var didSleep: [TimeInterval] = [] - var didRemoveDirectory: [String] = [] + var didRemoveItem: [String] = [] var didSetUD: [UserDiscovery?] = [] var didSetE2E: [E2E?] = [] var didSetCMix: [CMix?] = [] @@ -30,7 +30,7 @@ final class MessengerDestroyTests: XCTestCase { 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.fileManager.removeItem = { didRemoveItem.append($0) } env.passwordStorage.remove = { didRemovePassword += 1 } let destroy: MessengerDestroy = .live(env) @@ -42,7 +42,7 @@ final class MessengerDestroyTests: XCTestCase { XCTAssertNoDifference(didSetE2E.map { $0 == nil }, [true]) XCTAssertNoDifference(didSetCMix.map { $0 == nil }, [true]) XCTAssertNoDifference(didSetIsListeningForMessages, [false]) - XCTAssertNoDifference(didRemoveDirectory, [storageDir]) + XCTAssertNoDifference(didRemoveItem, [storageDir]) XCTAssertNoDifference(didRemovePassword, 1) } @@ -78,7 +78,7 @@ final class MessengerDestroyTests: XCTestCase { env.e2e.set = { didSetE2E.append($0) } env.cMix.set = { didSetCMix.append($0) } env.isListeningForMessages.set = { didSetIsListeningForMessages.append($0) } - env.fileManager.removeDirectory = { _ in throw error } + env.fileManager.removeItem = { _ in throw error } let destroy: MessengerDestroy = .live(env) XCTAssertThrowsError(try destroy()) { err in @@ -94,7 +94,7 @@ final class MessengerDestroyTests: XCTestCase { struct Error: Swift.Error, Equatable {} let error = Error() let storageDir = "test-storage-dir" - var didRemoveDirectory: [String] = [] + var didRemoveItem: [String] = [] var didSetUD: [UserDiscovery?] = [] var didSetE2E: [E2E?] = [] var didSetCMix: [CMix?] = [] @@ -107,7 +107,7 @@ final class MessengerDestroyTests: XCTestCase { env.cMix.set = { didSetCMix.append($0) } env.isListeningForMessages.set = { didSetIsListeningForMessages.append($0) } env.storageDir = storageDir - env.fileManager.removeDirectory = { didRemoveDirectory.append($0) } + env.fileManager.removeItem = { didRemoveItem.append($0) } env.passwordStorage.remove = { throw error } let destroy: MessengerDestroy = .live(env) @@ -118,6 +118,6 @@ final class MessengerDestroyTests: XCTestCase { XCTAssertNoDifference(didSetE2E.map { $0 == nil }, [true]) XCTAssertNoDifference(didSetCMix.map { $0 == nil }, [true]) XCTAssertNoDifference(didSetIsListeningForMessages, [false]) - XCTAssertNoDifference(didRemoveDirectory, [storageDir]) + XCTAssertNoDifference(didRemoveItem, [storageDir]) } } diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerRestoreBackupTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerRestoreBackupTests.swift index 672adc37..7e55ddc3 100644 --- a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerRestoreBackupTests.swift +++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerRestoreBackupTests.swift @@ -40,7 +40,7 @@ final class MessengerRestoreBackupTests: XCTestCase { 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.removeItem = { caughtActions.append(.didRemoveItem(path: $0)) } env.fileManager.createDirectory = { caughtActions.append(.didCreateDirectory(path: $0)) } env.newCMixFromBackup.run = { ndfJSON, storageDir, backupPassphrase, sessionPassword, backupFileContents in @@ -114,7 +114,7 @@ final class MessengerRestoreBackupTests: XCTestCase { .didSavePassword( password: password ), - .didRemoveDirectory( + .didRemoveItem( path: env.storageDir ), .didCreateDirectory( @@ -185,7 +185,7 @@ private enum CaughtAction: Equatable { case didSavePassword( password: Data ) - case didRemoveDirectory( + case didRemoveItem( path: String ) case didCreateDirectory( -- GitLab From e22c3daf11b50526c798445b18cac81610349eee Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Thu, 29 Sep 2022 18:56:34 +0200 Subject: [PATCH 09/15] Update MessengerFileManager --- .../Utils/MessengerFileManager.swift | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Sources/XXMessengerClient/Utils/MessengerFileManager.swift b/Sources/XXMessengerClient/Utils/MessengerFileManager.swift index 9e2c1590..ad116d82 100644 --- a/Sources/XXMessengerClient/Utils/MessengerFileManager.swift +++ b/Sources/XXMessengerClient/Utils/MessengerFileManager.swift @@ -5,6 +5,9 @@ public struct MessengerFileManager { public var isDirectoryEmpty: (String) -> Bool public var removeItem: (String) throws -> Void public var createDirectory: (String) throws -> Void + public var saveFile: (String, Data) throws -> Void + public var loadFile: (String) throws -> Data? + public var modifiedTime: (String) throws -> Date? } extension MessengerFileManager { @@ -26,6 +29,16 @@ extension MessengerFileManager { atPath: path, withIntermediateDirectories: true ) + }, + saveFile: { path, data in + try data.write(to: URL(fileURLWithPath: path)) + }, + loadFile: { path in + try Data(contentsOf: URL(fileURLWithPath: path)) + }, + modifiedTime: { path in + let attributes = try fileManager.attributesOfItem(atPath: path) + return attributes[.modificationDate] as? Date } ) } @@ -35,6 +48,9 @@ extension MessengerFileManager { public static let unimplemented = MessengerFileManager( isDirectoryEmpty: XCTUnimplemented("\(Self.self).isDirectoryEmpty", placeholder: false), removeItem: XCTUnimplemented("\(Self.self).removeItem"), - createDirectory: XCTUnimplemented("\(Self.self).createDirectory") + createDirectory: XCTUnimplemented("\(Self.self).createDirectory"), + saveFile: XCTUnimplemented("\(Self.self).saveFile"), + loadFile: XCTUnimplemented("\(Self.self).loadFile"), + modifiedTime: XCTUnimplemented("\(Self.self).modifiedTime") ) } -- GitLab From c2c18618ba0f33b0f67a7d0a8ebe15b338a4f6df Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Thu, 29 Sep 2022 18:57:07 +0200 Subject: [PATCH 10/15] Create BackupStorage.onDisk implementation --- .../Utils/BackupStorage.swift | 24 +++- .../Utils/BackupStorageTests.swift | 103 +++++++++++++----- 2 files changed, 91 insertions(+), 36 deletions(-) diff --git a/Sources/XXMessengerClient/Utils/BackupStorage.swift b/Sources/XXMessengerClient/Utils/BackupStorage.swift index b13c86cb..e85e2e86 100644 --- a/Sources/XXMessengerClient/Utils/BackupStorage.swift +++ b/Sources/XXMessengerClient/Utils/BackupStorage.swift @@ -18,28 +18,39 @@ public struct BackupStorage { public typealias Observer = (Backup?) -> Void - public var store: (Data) -> Void + public var store: (Data) throws -> Void public var observe: (@escaping Observer) -> Cancellable - public var remove: () -> Void + public var remove: () throws -> Void } extension BackupStorage { - public static func live( - now: @escaping () -> Date + public static func onDisk( + now: @escaping () -> Date = Date.init, + fileManager: MessengerFileManager = .live(), + path: String = FileManager.default + .urls(for: .applicationSupportDirectory, in: .userDomainMask) + .first! + .appendingPathComponent("backup.xxm") + .path ) -> BackupStorage { var observers: [UUID: Observer] = [:] var backup: Backup? func notifyObservers() { observers.values.forEach { $0(backup) } } - + if let fileData = try? fileManager.loadFile(path), + let fileDate = try? fileManager.modifiedTime(path) { + backup = Backup(date: fileDate, data: fileData) + } return BackupStorage( store: { data in - backup = Backup( + let newBackup = Backup( date: now(), data: data ) + backup = newBackup notifyObservers() + try fileManager.saveFile(path, newBackup.data) }, observe: { observer in let id = UUID() @@ -52,6 +63,7 @@ extension BackupStorage { remove: { backup = nil notifyObservers() + try fileManager.removeItem(path) } ) } diff --git a/Tests/XXMessengerClientTests/Utils/BackupStorageTests.swift b/Tests/XXMessengerClientTests/Utils/BackupStorageTests.swift index 3dc8272e..d373ce6e 100644 --- a/Tests/XXMessengerClientTests/Utils/BackupStorageTests.swift +++ b/Tests/XXMessengerClientTests/Utils/BackupStorageTests.swift @@ -3,63 +3,98 @@ import XCTest @testable import XXMessengerClient final class BackupStorageTests: XCTestCase { - func testStorage() { + func testStorage() throws { + var actions: [Action]! + var now: Date = .init(0) - let storage: BackupStorage = .live( - now: { now } + let path = "backup-path" + let fileData = "file-data".data(using: .utf8)! + let fileDate = Date(123) + var fileManager = MessengerFileManager.unimplemented + fileManager.loadFile = { path in + actions.append(.didLoadFile(path)) + return fileData + } + fileManager.modifiedTime = { path in + actions.append(.didGetModifiedTime(path)) + return fileDate + } + fileManager.saveFile = { path, data in + actions.append(.didSaveFile(path, data)) + } + fileManager.removeItem = { path in + actions.append(.didRemoveItem(path)) + } + actions = [] + let storage: BackupStorage = .onDisk( + now: { now }, + fileManager: fileManager, + path: path ) - var didObserveA: [BackupStorage.Backup?] = [] + XCTAssertNoDifference(actions, [ + .didLoadFile(path), + .didGetModifiedTime(path), + ]) + + actions = [] let observerA = storage.observe { backup in - didObserveA.append(backup) + actions.append(.didObserve("A", backup)) } - XCTAssertNoDifference(didObserveA, [nil]) + XCTAssertNoDifference(actions, [ + .didObserve("A", .init(date: fileDate, data: fileData)) + ]) + actions = [] now = .init(1) - didObserveA = [] let data1 = "data-1".data(using: .utf8)! - storage.store(data1) + try storage.store(data1) - XCTAssertNoDifference(didObserveA, [.init(date: .init(1), data: data1)]) + XCTAssertNoDifference(actions, [ + .didObserve("A", .init(date: .init(1), data: data1)), + .didSaveFile(path, data1), + ]) + actions = [] now = .init(2) - didObserveA = [] - var didObserveB: [BackupStorage.Backup?] = [] let observerB = storage.observe { backup in - didObserveB.append(backup) + actions.append(.didObserve("B", backup)) } - XCTAssertNoDifference(didObserveA, []) - XCTAssertNoDifference(didObserveB, [.init(date: .init(1), data: data1)]) + XCTAssertNoDifference(actions, [ + .didObserve("B", .init(date: .init(1), data: data1)) + ]) + actions = [] now = .init(3) - didObserveA = [] - didObserveB = [] + observerA.cancel() let data2 = "data-2".data(using: .utf8)! - storage.store(data2) + try storage.store(data2) - XCTAssertNoDifference(didObserveA, [.init(date: .init(3), data: data2)]) - XCTAssertNoDifference(didObserveB, [.init(date: .init(3), data: data2)]) + XCTAssertNoDifference(actions, [ + .didObserve("B", .init(date: .init(3), data: data2)), + .didSaveFile(path, data2), + ]) + actions = [] now = .init(4) - didObserveA = [] - didObserveB = [] - observerA.cancel() - storage.remove() + try storage.remove() - XCTAssertNoDifference(didObserveA, []) - XCTAssertNoDifference(didObserveB, [nil]) + XCTAssertNoDifference(actions, [ + .didObserve("B", nil), + .didRemoveItem(path), + ]) + actions = [] now = .init(5) - didObserveA = [] - didObserveB = [] observerB.cancel() let data3 = "data-3".data(using: .utf8)! - storage.store(data3) + try storage.store(data3) - XCTAssertNoDifference(didObserveA, []) - XCTAssertNoDifference(didObserveB, []) + XCTAssertNoDifference(actions, [ + .didSaveFile(path, data3), + ]) } } @@ -68,3 +103,11 @@ private extension Date { self.init(timeIntervalSince1970: timeIntervalSince1970) } } + +private enum Action: Equatable { + case didLoadFile(String) + case didGetModifiedTime(String) + case didObserve(String, BackupStorage.Backup?) + case didSaveFile(String, Data) + case didRemoveItem(String) +} -- GitLab From d89f26a26cafa7d6795916579cbc98de68a91f12 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Thu, 29 Sep 2022 18:57:31 +0200 Subject: [PATCH 11/15] Update example app --- .../xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift | 2 +- Examples/xx-messenger/Sources/AppFeature/AppFeature.swift | 2 +- Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index a81b863a..3cf2cb0b 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -28,7 +28,7 @@ extension AppEnvironment { handleConfirm: .live(db: dbManager.getDB), handleReset: .live(db: dbManager.getDB) ) - let backupStorage = BackupStorage.live(now: Date.init) + let backupStorage = BackupStorage.onDisk() let mainQueue = DispatchQueue.main.eraseToAnyScheduler() let bgQueue = DispatchQueue.global(qos: .background).eraseToAnyScheduler() diff --git a/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift b/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift index 145da907..07be9948 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift @@ -97,7 +97,7 @@ let appReducer = Reducer<AppState, AppAction, AppEnvironment> env.log(.error(error as NSError)) })) cancellables.append(env.messenger.registerBackupCallback(.init { data in - env.backupStorage.store(data) + try? env.backupStorage.store(data) })) let isLoaded = env.messenger.isLoaded() diff --git a/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift b/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift index 128c375b..e1ce8fb2 100644 --- a/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift +++ b/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift @@ -166,7 +166,7 @@ public let backupReducer = Reducer<BackupState, BackupAction, BackupEnvironment> return Effect.run { subscriber in do { try env.messenger.stopBackup() - env.backupStorage.remove() + try env.backupStorage.remove() subscriber.send(.didStop(failure: nil)) } catch { subscriber.send(.didStop(failure: error as NSError)) -- GitLab From efc1f3468eae328678bf8acea204afec37083b2e Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Thu, 29 Sep 2022 18:59:02 +0200 Subject: [PATCH 12/15] Update BackupView --- .../xx-messenger/Sources/BackupFeature/BackupView.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift b/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift index 13873f89..54fc6567 100644 --- a/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift +++ b/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift @@ -43,12 +43,12 @@ public struct BackupView: View { WithViewStore(store, observe: ViewState.init) { viewStore in Form { Group { - if !viewStore.isRunning { - newBackupSection(viewStore) - } if viewStore.isRunning || viewStore.backup != nil { backupSection(viewStore) } + if !viewStore.isRunning { + newBackupSection(viewStore) + } } .disabled(viewStore.isLoading) .alert( -- GitLab From aabd0d05f1bee4baaa3940af1cf061d2ddb58771 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Thu, 29 Sep 2022 19:17:15 +0200 Subject: [PATCH 13/15] Try to resume backup on launch --- Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift | 4 ++++ .../Tests/HomeFeatureTests/HomeFeatureTests.swift | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift index e576e6f1..87a51600 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift @@ -156,6 +156,10 @@ public let homeReducer = Reducer<HomeState, HomeAction, HomeEnvironment> try env.messenger.logIn() } + if !env.messenger.isBackupRunning() { + try? env.messenger.resumeBackup() + } + return .success(.messenger(.didStartRegistered)) } catch { return .success(.messenger(.failure(error as NSError))) diff --git a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift index cb9ee3b6..3a57414b 100644 --- a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift +++ b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift @@ -56,6 +56,7 @@ final class HomeFeatureTests: XCTestCase { var messengerDidConnect = 0 var messengerDidListenForMessages = 0 var messengerDidLogIn = 0 + var messengerDidResumeBackup = 0 store.environment.bgQueue = .immediate store.environment.mainQueue = .immediate @@ -67,6 +68,8 @@ final class HomeFeatureTests: XCTestCase { store.environment.messenger.isLoggedIn.run = { false } store.environment.messenger.isRegistered.run = { true } store.environment.messenger.logIn.run = { messengerDidLogIn += 1 } + store.environment.messenger.isBackupRunning.run = { false } + store.environment.messenger.resumeBackup.run = { messengerDidResumeBackup += 1 } store.environment.messenger.cMix.get = { var cMix: CMix = .unimplemented cMix.addHealthCallback.run = { _ in Cancellable {} } @@ -83,6 +86,7 @@ final class HomeFeatureTests: XCTestCase { XCTAssertNoDifference(messengerDidConnect, 1) XCTAssertNoDifference(messengerDidListenForMessages, 1) XCTAssertNoDifference(messengerDidLogIn, 1) + XCTAssertNoDifference(messengerDidResumeBackup, 1) store.receive(.networkMonitor(.stop)) store.receive(.messenger(.didStartRegistered)) @@ -111,6 +115,7 @@ final class HomeFeatureTests: XCTestCase { store.environment.messenger.isLoggedIn.run = { false } store.environment.messenger.isRegistered.run = { true } store.environment.messenger.logIn.run = { messengerDidLogIn += 1 } + store.environment.messenger.isBackupRunning.run = { true } store.environment.messenger.cMix.get = { var cMix: CMix = .unimplemented cMix.addHealthCallback.run = { _ in Cancellable {} } -- GitLab From 90d0e9a0c1ca322f15035edc6cb9b6fa74ef4403 Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Thu, 29 Sep 2022 19:36:58 +0200 Subject: [PATCH 14/15] Test CI --- .gitlab-ci.yml | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9f8233b4..72409c60 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,26 +13,10 @@ before_script: stages: - test -test-package-macos: - stage: test - tags: - - ios - script: - - ./run-tests.sh macos - retry: 1 - test-package-ios: stage: test tags: - ios script: + - ./xcode-remove-caches.sh - ./run-tests.sh ios - retry: 1 - -test-examples-ios: - stage: test - tags: - - ios - script: - - ./run-tests.sh examples-ios - retry: 1 -- GitLab From 0070b7bbc450d97e6b306dbcbef18363c0811eeb Mon Sep 17 00:00:00 2001 From: Dariusz Rybicki <dariusz@elixxir.io> Date: Thu, 29 Sep 2022 19:39:44 +0200 Subject: [PATCH 15/15] Revert "Test CI" This reverts commit 90d0e9a0c1ca322f15035edc6cb9b6fa74ef4403. --- .gitlab-ci.yml | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 72409c60..9f8233b4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,10 +13,26 @@ before_script: stages: - test +test-package-macos: + stage: test + tags: + - ios + script: + - ./run-tests.sh macos + retry: 1 + test-package-ios: stage: test tags: - ios script: - - ./xcode-remove-caches.sh - ./run-tests.sh ios + retry: 1 + +test-examples-ios: + stage: test + tags: + - ios + script: + - ./run-tests.sh examples-ios + retry: 1 -- GitLab