diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 10f45cd3ff6b3f9359973783412c08b883c300c3..9f8233b4902e5d7047d7011f3948f29aa559418a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,14 @@ before_script: - - for ip in $(dig @8.8.8.8 github.com +short); do ssh-keyscan github.com,$ip; ssh-keyscan $ip; done 2>/dev/null >> ~/.ssh/known_hosts - - for ip in $(dig @8.8.8.8 git.xx.network +short); do ssh-keyscan git.xx.network,$ip; ssh-keyscan $ip; done 2>/dev/null >> ~/.ssh/known_hosts + - echo $CI_BUILD_REF + - echo $CI_PROJECT_DIR + - echo $PWD + - swift --version - xcodebuild -version + - eval $(ssh-agent -s) + - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null + - mkdir -p ~/.ssh + - chmod 700 ~/.ssh + - ssh-keyscan -t rsa $GITLAB_SERVER > ~/.ssh/known_hosts stages: - test diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/elixxir-dapps-sdk-swift-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/elixxir-dapps-sdk-swift-Package.xcscheme index 7818f7363eeee23304e1aece2235ce6bbaa19fb8..4dad91ad903da2bc783bca223adbadda48954c7d 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/elixxir-dapps-sdk-swift-Package.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/elixxir-dapps-sdk-swift-Package.xcscheme @@ -36,10 +36,10 @@ </BuildActionEntry> <BuildActionEntry buildForTesting = "YES" - buildForRunning = "YES" + buildForRunning = "NO" buildForProfiling = "NO" buildForArchiving = "NO" - buildForAnalyzing = "YES"> + buildForAnalyzing = "NO"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "XXClientTests" @@ -50,10 +50,10 @@ </BuildActionEntry> <BuildActionEntry buildForTesting = "YES" - buildForRunning = "YES" + buildForRunning = "NO" buildForProfiling = "NO" buildForArchiving = "NO" - buildForAnalyzing = "YES"> + buildForAnalyzing = "NO"> <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "XXMessengerClientTests" diff --git a/Docs/XXMessengerClient.md b/Docs/XXMessengerClient.md index 05afa4aaa33cb74d114d0548faf8c157e00678ce..efbb9de768ea89616473b3df51837785ee880dd4 100644 --- a/Docs/XXMessengerClient.md +++ b/Docs/XXMessengerClient.md @@ -4,7 +4,7 @@ ## â–¶ï¸ Instantiate messenger -Example: +### Example ```swift // setup environment: @@ -24,7 +24,7 @@ let messenger: Messenger = .live(environment) ## 🚀 Start messenger -Example: +### Example ```swift // allow cancellation of callbacks: @@ -84,7 +84,7 @@ func start(messenger: Messenger) throws { ## 🛠Use client components directly -Example: +### Example ```swift // get cMix: @@ -95,4 +95,64 @@ let e2e = messenger.e2e() // get UserDicovery: let ud = messenger.ud() -``` \ No newline at end of file + +// get Backup: +let backup = messenger.backup() +``` + +## 💾 Backup + +### Make backup + +```swift +// start receiving backup data before starting or resuming backup: +let cancellable = messenger.registerBackupCallback(.init { data in + // handle backup data, save on disk, upload to cloud, etc. +}) + +// check if backup is already running: +if messenger.isBackupRunning() == false { + do { + // try to resume previous backup: + try messenger.resumeBackup() + } catch { + // try to start a new backup: + let params: BackupParams = ... + try messenger.startBackup( + password: "backup-passphrase", + params: params + ) + } +} + +// update params in the backup: +let params: BackupParams = ... +try messenger.backupParams(params) + +// stop the backup: +try messenger.stopBackup() + +// optionally stop receiving backup data +cancellable.cancel() +``` + +When starting a new backup you must provide `BackupParams` to prevent creating backups that does not contain it. + +The registered backup callback can be reused later when a new backup is started. There is no need to cancel it and register a new callback in such a case. + +### Restore from backup + +```swift +let result = try messenger.restoreBackup( + backupData: ..., + backupPassphrase: "backup-passphrase" +) + +// handle restoration result: +let restoredUsername = result.restoredParams.username +let facts = try messenger.ud.tryGet().getFacts() +let restoredEmail = facts.get(.email)?.value +let restoredPhone = facts.get(.phone)?.value +``` + +If no error was thrown during restoration, the `Messenger` is already loaded, started, connected, and logged in. \ No newline at end of file diff --git a/Examples/Package.swift b/Examples/Package.swift index d0a31fe81dd861a3d33bf4eaeb4f59fb7c9f4947..3cc048d33a65f27d26fa50d1f2779fc77b510dfe 100644 --- a/Examples/Package.swift +++ b/Examples/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.6 +// swift-tools-version:5.7 // This file makes Xcode doesn't display this directory inside swift package. import PackageDescription let package = Package(name: "", products: [], targets: []) 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 0000000000000000000000000000000000000000..61d3cc5840af204a0b1ac7d0f8fc6ea574d0bce0 --- /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/.swiftpm/xcode/xcshareddata/xcschemes/ContactLookupFeature.xcscheme b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactLookupFeature.xcscheme new file mode 100644 index 0000000000000000000000000000000000000000..e0070c64629dce6e785c4bb8590d0b330d522f5b --- /dev/null +++ b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ContactLookupFeature.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 = "ContactLookupFeature" + BuildableName = "ContactLookupFeature" + BlueprintName = "ContactLookupFeature" + 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 = "ContactLookupFeatureTests" + BuildableName = "ContactLookupFeatureTests" + BlueprintName = "ContactLookupFeatureTests" + 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 = "ContactLookupFeature" + BuildableName = "ContactLookupFeature" + BlueprintName = "ContactLookupFeature" + ReferencedContainer = "container:"> + </BuildableReference> + </MacroExpansion> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/MyContactFeature.xcscheme b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/MyContactFeature.xcscheme new file mode 100644 index 0000000000000000000000000000000000000000..4eb2a43343a9c76a7ff475789759efbed4b3c270 --- /dev/null +++ b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/MyContactFeature.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 = "MyContactFeature" + BuildableName = "MyContactFeature" + BlueprintName = "MyContactFeature" + 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 = "MyContactFeatureTests" + BuildableName = "MyContactFeatureTests" + BlueprintName = "MyContactFeatureTests" + 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 = "MyContactFeature" + BuildableName = "MyContactFeature" + BlueprintName = "MyContactFeature" + ReferencedContainer = "container:"> + </BuildableReference> + </MacroExpansion> + </ProfileAction> + <AnalyzeAction + buildConfiguration = "Debug"> + </AnalyzeAction> + <ArchiveAction + buildConfiguration = "Release" + revealArchiveInOrganizer = "YES"> + </ArchiveAction> +</Scheme> diff --git a/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ResetAuthFeature.xcscheme b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ResetAuthFeature.xcscheme new file mode 100644 index 0000000000000000000000000000000000000000..dded8f8def9e1bf44393cece148486be88e940d6 --- /dev/null +++ b/Examples/xx-messenger/.swiftpm/xcode/xcshareddata/xcschemes/ResetAuthFeature.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 = "ResetAuthFeature" + BuildableName = "ResetAuthFeature" + BlueprintName = "ResetAuthFeature" + 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 = "ResetAuthFeatureTests" + BuildableName = "ResetAuthFeatureTests" + BlueprintName = "ResetAuthFeatureTests" + 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 = "ResetAuthFeature" + BuildableName = "ResetAuthFeature" + BlueprintName = "ResetAuthFeature" + 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 c5bcba6a09c4d828a7506a531d429f9d9ab8a92d..2b2d10391ee64f2b70f06b607c67437e78f3d55a 100644 --- a/Examples/xx-messenger/Package.swift +++ b/Examples/xx-messenger/Package.swift @@ -1,15 +1,10 @@ -// swift-tools-version: 5.6 +// swift-tools-version: 5.7 import PackageDescription let swiftSettings: [SwiftSetting] = [ - .unsafeFlags( - [ - // "-Xfrontend", "-warn-concurrency", - // "-Xfrontend", "-debug-time-function-bodies", - // "-Xfrontend", "-debug-time-expression-type-checking", - ], - .when(configuration: .debug) - ), + //.unsafeFlags(["-Xfrontend", "-warn-concurrency"], .when(configuration: .debug)), + //.unsafeFlags(["-Xfrontend", "-debug-time-function-bodies"], .when(configuration: .debug)), + //.unsafeFlags(["-Xfrontend", "-debug-time-expression-type-checking"], .when(configuration: .debug)), ] let package = Package( @@ -20,13 +15,17 @@ 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"]), .library(name: "ContactFeature", targets: ["ContactFeature"]), + .library(name: "ContactLookupFeature", targets: ["ContactLookupFeature"]), .library(name: "ContactsFeature", targets: ["ContactsFeature"]), .library(name: "HomeFeature", targets: ["HomeFeature"]), + .library(name: "MyContactFeature", targets: ["MyContactFeature"]), .library(name: "RegisterFeature", targets: ["RegisterFeature"]), + .library(name: "ResetAuthFeature", targets: ["ResetAuthFeature"]), .library(name: "RestoreFeature", targets: ["RestoreFeature"]), .library(name: "SendRequestFeature", targets: ["SendRequestFeature"]), .library(name: "UserSearchFeature", targets: ["UserSearchFeature"]), @@ -39,11 +38,11 @@ let package = Package( ), .package( url: "https://github.com/pointfreeco/swift-composable-architecture.git", - .upToNextMajor(from: "0.40.1") + .upToNextMajor(from: "0.40.2") ), .package( url: "https://git.xx.network/elixxir/client-ios-db.git", - .upToNextMajor(from: "1.1.0") + .upToNextMajor(from: "1.2.0") ), .package( url: "https://github.com/darrarski/swift-composable-presentation.git", @@ -51,13 +50,26 @@ let package = Package( ), .package( url: "https://github.com/pointfreeco/xctest-dynamic-overlay.git", - .upToNextMajor(from: "0.4.0") + .upToNextMajor(from: "0.4.1") + ), + .package( + url: "https://github.com/pointfreeco/swift-custom-dump.git", + .upToNextMajor(from: "0.5.2") + ), + .package( + url: "https://github.com/apple/swift-log.git", + .upToNextMajor(from: "1.4.4") + ), + .package( + url: "https://github.com/kean/Pulse.git", + .upToNextMajor(from: "2.1.2") ), ], targets: [ .target( name: "AppCore", dependencies: [ + .product(name: "Logging", package: "swift-log"), .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), .product(name: "XXDatabase", package: "client-ios-db"), @@ -69,7 +81,8 @@ let package = Package( .testTarget( name: "AppCoreTests", dependencies: [ - .target(name: "AppCore") + .target(name: "AppCore"), + .product(name: "CustomDump", package: "swift-custom-dump"), ], swiftSettings: swiftSettings ), @@ -77,13 +90,17 @@ let package = Package( name: "AppFeature", dependencies: [ .target(name: "AppCore"), + .target(name: "BackupFeature"), .target(name: "ChatFeature"), .target(name: "CheckContactAuthFeature"), .target(name: "ConfirmRequestFeature"), .target(name: "ContactFeature"), + .target(name: "ContactLookupFeature"), .target(name: "ContactsFeature"), .target(name: "HomeFeature"), + .target(name: "MyContactFeature"), .target(name: "RegisterFeature"), + .target(name: "ResetAuthFeature"), .target(name: "RestoreFeature"), .target(name: "SendRequestFeature"), .target(name: "UserSearchFeature"), @@ -91,6 +108,9 @@ let package = Package( .target(name: "WelcomeFeature"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .product(name: "ComposablePresentation", package: "swift-composable-presentation"), + .product(name: "Logging", package: "swift-log"), + .product(name: "PulseLogHandler", package: "Pulse"), + .product(name: "PulseUI", package: "Pulse"), .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), .product(name: "XXModels", package: "client-ios-db"), ], @@ -100,6 +120,22 @@ let package = Package( name: "AppFeatureTests", dependencies: [ .target(name: "AppFeature"), + .product(name: "CustomDump", package: "swift-custom-dump"), + ], + 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 ), @@ -118,6 +154,7 @@ let package = Package( name: "ChatFeatureTests", dependencies: [ .target(name: "ChatFeature"), + .product(name: "CustomDump", package: "swift-custom-dump"), ], swiftSettings: swiftSettings ), @@ -135,6 +172,7 @@ let package = Package( name: "CheckContactAuthFeatureTests", dependencies: [ .target(name: "CheckContactAuthFeature"), + .product(name: "CustomDump", package: "swift-custom-dump"), ] ), .target( @@ -151,6 +189,7 @@ let package = Package( name: "ConfirmRequestFeatureTests", dependencies: [ .target(name: "ConfirmRequestFeature"), + .product(name: "CustomDump", package: "swift-custom-dump"), ] ), .target( @@ -160,6 +199,8 @@ let package = Package( .target(name: "ChatFeature"), .target(name: "CheckContactAuthFeature"), .target(name: "ConfirmRequestFeature"), + .target(name: "ContactLookupFeature"), + .target(name: "ResetAuthFeature"), .target(name: "SendRequestFeature"), .target(name: "VerifyContactFeature"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), @@ -173,6 +214,25 @@ let package = Package( name: "ContactFeatureTests", dependencies: [ .target(name: "ContactFeature"), + .product(name: "CustomDump", package: "swift-custom-dump"), + ], + swiftSettings: swiftSettings + ), + .target( + name: "ContactLookupFeature", + 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 + ), + .testTarget( + name: "ContactLookupFeatureTests", + dependencies: [ + .target(name: "ContactLookupFeature"), + .product(name: "CustomDump", package: "swift-custom-dump"), ], swiftSettings: swiftSettings ), @@ -181,6 +241,7 @@ let package = Package( dependencies: [ .target(name: "AppCore"), .target(name: "ContactFeature"), + .target(name: "MyContactFeature"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .product(name: "ComposablePresentation", package: "swift-composable-presentation"), .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), @@ -193,6 +254,7 @@ let package = Package( name: "ContactsFeatureTests", dependencies: [ .target(name: "ContactsFeature"), + .product(name: "CustomDump", package: "swift-custom-dump"), ], swiftSettings: swiftSettings ), @@ -200,11 +262,13 @@ let package = Package( name: "HomeFeature", dependencies: [ .target(name: "AppCore"), + .target(name: "BackupFeature"), .target(name: "ContactsFeature"), .target(name: "RegisterFeature"), .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 @@ -213,6 +277,26 @@ let package = Package( name: "HomeFeatureTests", dependencies: [ .target(name: "HomeFeature"), + .product(name: "CustomDump", package: "swift-custom-dump"), + ], + swiftSettings: swiftSettings + ), + .target( + name: "MyContactFeature", + dependencies: [ + .target(name: "AppCore"), + .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), + .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), + .product(name: "XXModels", package: "client-ios-db"), + ], + swiftSettings: swiftSettings + ), + .testTarget( + name: "MyContactFeatureTests", + dependencies: [ + .target(name: "MyContactFeature"), + .product(name: "CustomDump", package: "swift-custom-dump"), ], swiftSettings: swiftSettings ), @@ -231,13 +315,34 @@ let package = Package( name: "RegisterFeatureTests", dependencies: [ .target(name: "RegisterFeature"), + .product(name: "CustomDump", package: "swift-custom-dump"), + ], + swiftSettings: swiftSettings + ), + .target( + name: "ResetAuthFeature", + dependencies: [ + .target(name: "AppCore"), + .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + .product(name: "XXClient", package: "elixxir-dapps-sdk-swift"), + .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), + ], + swiftSettings: swiftSettings + ), + .testTarget( + name: "ResetAuthFeatureTests", + dependencies: [ + .target(name: "ResetAuthFeature"), ], swiftSettings: swiftSettings ), .target( name: "RestoreFeature", 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 ), @@ -245,6 +350,7 @@ let package = Package( name: "RestoreFeatureTests", dependencies: [ .target(name: "RestoreFeature"), + .product(name: "CustomDump", package: "swift-custom-dump"), ], swiftSettings: swiftSettings ), @@ -263,6 +369,7 @@ let package = Package( name: "SendRequestFeatureTests", dependencies: [ .target(name: "SendRequestFeature"), + .product(name: "CustomDump", package: "swift-custom-dump"), ], swiftSettings: swiftSettings ), @@ -282,6 +389,7 @@ let package = Package( name: "UserSearchFeatureTests", dependencies: [ .target(name: "UserSearchFeature"), + .product(name: "CustomDump", package: "swift-custom-dump"), ], swiftSettings: swiftSettings ), @@ -299,11 +407,13 @@ let package = Package( name: "VerifyContactFeatureTests", dependencies: [ .target(name: "VerifyContactFeature"), + .product(name: "CustomDump", package: "swift-custom-dump"), ] ), .target( name: "WelcomeFeature", dependencies: [ + .target(name: "AppCore"), .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .product(name: "XXMessengerClient", package: "elixxir-dapps-sdk-swift"), ], @@ -313,6 +423,7 @@ let package = Package( name: "WelcomeFeatureTests", dependencies: [ .target(name: "WelcomeFeature"), + .product(name: "CustomDump", package: "swift-custom-dump"), ], swiftSettings: swiftSettings ), diff --git a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/project.pbxproj b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/project.pbxproj index f0505c12949959289b70b0fc9e45c8f1d1f7079c..1d244c566f5a1ce89a6ff2df5a69710f63b5165c 100644 --- a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/project.pbxproj +++ b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/project.pbxproj @@ -12,8 +12,9 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 31964B8A28A6D37100BBDC17 /* XXMessenger.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = XXMessenger.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 31964B8A28A6D37100BBDC17 /* XXME.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = XXME.app; sourceTree = BUILT_PRODUCTS_DIR; }; 31964B9128A6D37200BBDC17 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; + 31EF69BC28F035DE00BD83FC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -39,7 +40,7 @@ 31964B8B28A6D37100BBDC17 /* Products */ = { isa = PBXGroup; children = ( - 31964B8A28A6D37100BBDC17 /* XXMessenger.app */, + 31964B8A28A6D37100BBDC17 /* XXME.app */, ); name = Products; sourceTree = "<group>"; @@ -48,6 +49,7 @@ isa = PBXGroup; children = ( 31964B9128A6D37200BBDC17 /* Assets.xcassets */, + 31EF69BC28F035DE00BD83FC /* Info.plist */, ); path = XXMessenger; sourceTree = "<group>"; @@ -72,7 +74,7 @@ 313CFFF928B632E40050B10D /* AppFeature */, ); productName = XXMessenger; - productReference = 31964B8A28A6D37100BBDC17 /* XXMessenger.app */; + productReference = 31964B8A28A6D37100BBDC17 /* XXME.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -163,7 +165,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -226,7 +228,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 2; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -256,9 +258,11 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = XXMessenger/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = ""; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -270,7 +274,11 @@ "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = xx.network.XXMessengerExample; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; + PRODUCT_NAME = XXME; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; @@ -283,9 +291,11 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = S6JDM2WW29; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = XXMessenger/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = ""; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; @@ -297,7 +307,11 @@ "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = xx.network.XXMessengerExample; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; + PRODUCT_NAME = XXME; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; 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 65400dff681f98714be5c8db74bc82b6558dfe17..aef30c316ef7aa40b1b1c36e37057d4284222bf4 100644 --- a/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme +++ b/Examples/xx-messenger/Project/XXMessenger.xcodeproj/xcshareddata/xcschemes/XXMessenger.xcscheme @@ -15,7 +15,7 @@ <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "31964B8928A6D37100BBDC17" - BuildableName = "XXMessenger.app" + BuildableName = "XXME.app" BlueprintName = "XXMessenger" ReferencedContainer = "container:XXMessenger.xcodeproj"> </BuildableReference> @@ -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 @@ -89,6 +99,16 @@ ReferencedContainer = "container:.."> </BuildableReference> </TestableReference> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "ContactLookupFeatureTests" + BuildableName = "ContactLookupFeatureTests" + BlueprintName = "ContactLookupFeatureTests" + ReferencedContainer = "container:.."> + </BuildableReference> + </TestableReference> <TestableReference skipped = "NO"> <BuildableReference @@ -109,6 +129,16 @@ ReferencedContainer = "container:.."> </BuildableReference> </TestableReference> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "MyContactFeatureTests" + BuildableName = "MyContactFeatureTests" + BlueprintName = "MyContactFeatureTests" + ReferencedContainer = "container:.."> + </BuildableReference> + </TestableReference> <TestableReference skipped = "NO"> <BuildableReference @@ -119,6 +149,16 @@ ReferencedContainer = "container:.."> </BuildableReference> </TestableReference> + <TestableReference + skipped = "NO"> + <BuildableReference + BuildableIdentifier = "primary" + BlueprintIdentifier = "ResetAuthFeatureTests" + BuildableName = "ResetAuthFeatureTests" + BlueprintName = "ResetAuthFeatureTests" + ReferencedContainer = "container:.."> + </BuildableReference> + </TestableReference> <TestableReference skipped = "NO"> <BuildableReference @@ -186,7 +226,7 @@ <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "31964B8928A6D37100BBDC17" - BuildableName = "XXMessenger.app" + BuildableName = "XXME.app" BlueprintName = "XXMessenger" ReferencedContainer = "container:XXMessenger.xcodeproj"> </BuildableReference> @@ -203,7 +243,7 @@ <BuildableReference BuildableIdentifier = "primary" BlueprintIdentifier = "31964B8928A6D37100BBDC17" - BuildableName = "XXMessenger.app" + BuildableName = "XXME.app" BlueprintName = "XXMessenger" ReferencedContainer = "container:XXMessenger.xcodeproj"> </BuildableReference> diff --git a/Examples/xx-messenger/Project/XXMessenger/Info.plist b/Examples/xx-messenger/Project/XXMessenger/Info.plist new file mode 100644 index 0000000000000000000000000000000000000000..1114a1ec787c979c3753d26ee5bce26ea246d836 --- /dev/null +++ b/Examples/xx-messenger/Project/XXMessenger/Info.plist @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>ITSAppUsesNonExemptEncryption</key> + <false/> + <key>NSLocalNetworkUsageDescription</key> + <string>Network usage required for debugging purposes </string> + <key>NSBonjourServices</key> + <array> + <string>_pulse._tcp</string> + </array> +</dict> +</plist> diff --git a/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerReset.swift b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerReset.swift index 729fe13eff8ee42ed1a12ba2ce03d082e5b2cfa8..a894b5e79200fb13b3986840410492050510e768 100644 --- a/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerReset.swift +++ b/Examples/xx-messenger/Sources/AppCore/AuthCallbackHandler/AuthCallbackHandlerReset.swift @@ -18,7 +18,7 @@ extension AuthCallbackHandlerReset { guard var dbContact = try db().fetchContacts(.init(id: [id])).first else { return } - dbContact.authStatus = .stranger + dbContact.authStatus = .friend dbContact = try db().saveContact(dbContact) } } 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/AppCore/Logger/Logger.swift b/Examples/xx-messenger/Sources/AppCore/Logger/Logger.swift new file mode 100644 index 0000000000000000000000000000000000000000..128d31124ce1e51ab89c023c3cac258c2d6a9182 --- /dev/null +++ b/Examples/xx-messenger/Sources/AppCore/Logger/Logger.swift @@ -0,0 +1,43 @@ +import Foundation +import Logging +import XCTestDynamicOverlay + +public struct Logger { + public enum Message: Equatable { + case error(NSError) + } + + public var run: (Message, String, String, UInt) -> Void + + public func callAsFunction( + _ msg: Message, + file: String = #file, + function: String = #function, + line: UInt = #line + ) { + run(msg, file, function, line) + } +} + +extension Logger { + public static func live() -> Logger { + let logger = Logging.Logger(label: "xx.network.XXMessengerExample") + return Logger { msg, file, function, line in + switch msg { + case .error(let error): + logger.error( + .init(stringLiteral: error.localizedDescription), + file: file, + function: function, + line: line + ) + } + } + } +} + +extension Logger { + public static let unimplemented = Logger( + run: XCTUnimplemented("\(Self.self).error") + ) +} diff --git a/Examples/xx-messenger/Sources/AppCore/SharedUI/AppVersionText.swift b/Examples/xx-messenger/Sources/AppCore/SharedUI/AppVersionText.swift new file mode 100644 index 0000000000000000000000000000000000000000..47c76873d1cd0253972566c05a525acea32cc509 --- /dev/null +++ b/Examples/xx-messenger/Sources/AppCore/SharedUI/AppVersionText.swift @@ -0,0 +1,31 @@ +import SwiftUI + +public struct AppVersionText: View { + public init() {} + + public var body: some View { + Text("v\(version) (\(build))") + } + + var version: String = Bundle.main.shortVersionString ?? "0.0.0" + var build: String = Bundle.main.versionString ?? "0" +} + +private extension Bundle { + var shortVersionString: String? { + infoDictionary?["CFBundleShortVersionString"] as? String + } + var versionString: String? { + infoDictionary?["CFBundleVersion"] as? String + } +} + +#if DEBUG +struct AppVersionText_Previews: PreviewProvider { + static var previews: some View { + AppVersionText() + .padding() + .previewLayout(.sizeThatFits) + } +} +#endif diff --git a/Examples/xx-messenger/Sources/AppCore/SharedUI/Data+hexString.swift b/Examples/xx-messenger/Sources/AppCore/SharedUI/Data+hexString.swift new file mode 100644 index 0000000000000000000000000000000000000000..e8010b95c6fbeea66049228f1129f929559bdf86 --- /dev/null +++ b/Examples/xx-messenger/Sources/AppCore/SharedUI/Data+hexString.swift @@ -0,0 +1,7 @@ +import Foundation + +extension Data { + public func hexString(bytesSeparator: String = " ") -> String { + map { String(format: "%02hhx\(bytesSeparator)", $0) }.joined() + } +} diff --git a/Examples/xx-messenger/Sources/AppCore/SharedUI/ShakeViewModifier.swift b/Examples/xx-messenger/Sources/AppCore/SharedUI/ShakeViewModifier.swift new file mode 100644 index 0000000000000000000000000000000000000000..d46cdc6d1a61adde2ace4c7fd6aebb3a1cfedd16 --- /dev/null +++ b/Examples/xx-messenger/Sources/AppCore/SharedUI/ShakeViewModifier.swift @@ -0,0 +1,42 @@ +import SwiftUI + +struct ShakeViewModifier: ViewModifier { + var action: () -> Void + + func body(content: Content) -> some View { + content.onReceive( + NotificationCenter.default.publisher( + for: UIDevice.deviceDidShakeNotification + ), + perform: { _ in + action() + } + ) + } +} + +extension View { + public func onShake(perform action: @escaping () -> Void) -> some View { + modifier(ShakeViewModifier(action: action)) + } +} + +extension UIDevice { + static let deviceDidShakeNotification = Notification.Name( + rawValue: "deviceDidShakeNotification" + ) +} + +extension UIWindow { + open override func motionEnded( + _ motion: UIEvent.EventSubtype, + with event: UIEvent? + ) { + super.motionEnded(motion, with: event) + guard motion == .motionShake else { return } + NotificationCenter.default.post( + name: UIDevice.deviceDidShakeNotification, + object: nil + ) + } +} diff --git a/Examples/xx-messenger/Sources/AppFeature/App.swift b/Examples/xx-messenger/Sources/AppFeature/App.swift index d6b0b722c4d610d823b5313aae797e3f72dc10a3..7c3f0e82b5b00ee3aa4ff601b50556dbc8a148d2 100644 --- a/Examples/xx-messenger/Sources/AppFeature/App.swift +++ b/Examples/xx-messenger/Sources/AppFeature/App.swift @@ -1,8 +1,14 @@ import ComposableArchitecture +import Logging +import PulseLogHandler import SwiftUI @main struct App: SwiftUI.App { + init() { + LoggingSystem.bootstrap(PersistentLogHandler.init) + } + var body: some Scene { WindowGroup { AppView(store: Store( diff --git a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift index 611123e0334c6078250bf0e782153975d6891799..c4ff9b27f027a0eef0d02eaeab6e8e7db519025c 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppEnvironment+Live.swift @@ -1,12 +1,16 @@ import AppCore +import BackupFeature import ChatFeature import CheckContactAuthFeature import ConfirmRequestFeature import ContactFeature +import ContactLookupFeature import ContactsFeature import Foundation import HomeFeature +import MyContactFeature import RegisterFeature +import ResetAuthFeature import RestoreFeature import SendRequestFeature import UserSearchFeature @@ -26,14 +30,27 @@ extension AppEnvironment { handleConfirm: .live(db: dbManager.getDB), handleReset: .live(db: dbManager.getDB) ) + let backupStorage = BackupStorage.onDisk() let mainQueue = DispatchQueue.main.eraseToAnyScheduler() let bgQueue = DispatchQueue.global(qos: .background).eraseToAnyScheduler() + defer { + _ = try! messenger.setLogLevel(.debug) + messenger.startLogging() + } + let contactEnvironment = ContactEnvironment( messenger: messenger, db: dbManager.getDB, mainQueue: mainQueue, bgQueue: bgQueue, + lookup: { + ContactLookupEnvironment( + messenger: messenger, + mainQueue: mainQueue, + bgQueue: bgQueue + ) + }, sendRequest: { SendRequestEnvironment( messenger: messenger, @@ -66,6 +83,13 @@ extension AppEnvironment { bgQueue: bgQueue ) }, + resetAuth: { + ResetAuthEnvironment( + messenger: messenger, + mainQueue: mainQueue, + bgQueue: bgQueue + ) + }, chat: { ChatEnvironment( messenger: messenger, @@ -84,6 +108,13 @@ extension AppEnvironment { return AppEnvironment( dbManager: dbManager, messenger: messenger, + authHandler: authHandler, + messageListener: .live( + messenger: messenger, + db: dbManager.getDB + ), + backupStorage: backupStorage, + log: .live(), mainQueue: mainQueue, bgQueue: bgQueue, welcome: { @@ -94,17 +125,19 @@ extension AppEnvironment { ) }, restore: { - RestoreEnvironment() + RestoreEnvironment( + messenger: messenger, + db: dbManager.getDB, + loadData: .live, + now: Date.init, + mainQueue: mainQueue, + bgQueue: bgQueue + ) }, home: { HomeEnvironment( messenger: messenger, dbManager: dbManager, - authHandler: authHandler, - messageListener: .live( - messenger: messenger, - db: dbManager.getDB - ), mainQueue: mainQueue, bgQueue: bgQueue, register: { @@ -122,7 +155,15 @@ extension AppEnvironment { db: dbManager.getDB, mainQueue: mainQueue, bgQueue: bgQueue, - contact: { contactEnvironment } + contact: { contactEnvironment }, + myContact: { + MyContactEnvironment( + messenger: messenger, + db: dbManager.getDB, + mainQueue: mainQueue, + bgQueue: bgQueue + ) + } ) }, userSearch: { @@ -132,6 +173,14 @@ extension AppEnvironment { bgQueue: bgQueue, contact: { contactEnvironment } ) + }, + backup: { + BackupEnvironment( + messenger: messenger, + backupStorage: backupStorage, + mainQueue: mainQueue, + bgQueue: bgQueue + ) } ) } diff --git a/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift b/Examples/xx-messenger/Sources/AppFeature/AppFeature.swift index 43cede697423359094d9b0186350abbc01e79d2e..7796f592bcd5c795c20d4a281d243cd03a7b8df9 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,10 @@ enum AppAction: Equatable, BindableAction { struct AppEnvironment { var dbManager: DBManager var messenger: Messenger + var authHandler: AuthCallbackHandler + var messageListener: MessageListenerHandler + var backupStorage: BackupStorage + var log: Logger var mainQueue: AnySchedulerOf<DispatchQueue> var bgQueue: AnySchedulerOf<DispatchQueue> var welcome: () -> WelcomeEnvironment @@ -53,10 +59,15 @@ struct AppEnvironment { var home: () -> HomeEnvironment } +#if DEBUG extension AppEnvironment { static let unimplemented = AppEnvironment( dbManager: .unimplemented, messenger: .unimplemented, + authHandler: .unimplemented, + messageListener: .unimplemented, + backupStorage: .unimplemented, + log: .unimplemented, mainQueue: .unimplemented, bgQueue: .unimplemented, welcome: { .unimplemented }, @@ -64,37 +75,57 @@ extension AppEnvironment { home: { .unimplemented } ) } +#endif 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 + env.log(.error(error as NSError)) + })) + cancellables.append(env.messageListener(onError: { error in + env.log(.error(error as NSError)) + })) + cancellables.append(env.messenger.registerBackupCallback(.init { data in + try? env.backupStorage.store(data) + })) + + 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/AppFeature/AppView.swift b/Examples/xx-messenger/Sources/AppFeature/AppView.swift index 64a8411df29fe65ebab6b403ae9f2c88438d140c..57983b1dd0b826321f3dac8c120637736ff64b60 100644 --- a/Examples/xx-messenger/Sources/AppFeature/AppView.swift +++ b/Examples/xx-messenger/Sources/AppFeature/AppView.swift @@ -1,11 +1,13 @@ import ComposableArchitecture import HomeFeature +import PulseUI import RestoreFeature import SwiftUI import WelcomeFeature struct AppView: View { let store: Store<AppState, AppAction> + @State var isPresentingPulse = false enum ViewState: Equatable { case loading @@ -119,6 +121,15 @@ struct AppView: View { .animation(.default, value: viewStore.state) .task { viewStore.send(.start) } } + .onShake { + isPresentingPulse = true + } + .fullScreenCover(isPresented: $isPresentingPulse) { + PulseUI.MainView( + store: .shared, + onDismiss: { isPresentingPulse = false } + ) + } } } diff --git a/Examples/xx-messenger/Sources/BackupFeature/Alerts.swift b/Examples/xx-messenger/Sources/BackupFeature/Alerts.swift new file mode 100644 index 0000000000000000000000000000000000000000..2bd95f770ff5ce1be9e808284d1d50f3d111af1d --- /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 new file mode 100644 index 0000000000000000000000000000000000000000..8010f17d1d0727c4621841dd124a93dcda0e0b5d --- /dev/null +++ b/Examples/xx-messenger/Sources/BackupFeature/BackupFeature.swift @@ -0,0 +1,228 @@ +import Combine +import ComposableArchitecture +import Foundation +import XXClient +import XXMessengerClient + +public struct BackupState: Equatable { + public enum Field: String, Hashable { + case passphrase + } + + public enum Error: String, Swift.Error, Equatable { + case contactUsernameMissing + } + + public init( + isRunning: Bool = false, + isStarting: Bool = false, + isResuming: Bool = false, + isStopping: Bool = false, + backup: BackupStorage.Backup? = nil, + alert: AlertState<BackupAction>? = nil, + focusedField: Field? = 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.focusedField = focusedField + 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 focusedField: Field? + @BindableState public var passphrase: String + @BindableState public var isExporting: Bool + public var exportData: Data? +} + +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( + messenger: Messenger, + backupStorage: BackupStorage, + mainQueue: AnySchedulerOf<DispatchQueue>, + bgQueue: AnySchedulerOf<DispatchQueue> + ) { + self.messenger = messenger + self.backupStorage = backupStorage + self.mainQueue = mainQueue + self.bgQueue = bgQueue + } + + public var messenger: Messenger + public var backupStorage: BackupStorage + public var mainQueue: AnySchedulerOf<DispatchQueue> + public var bgQueue: AnySchedulerOf<DispatchQueue> +} + +#if DEBUG +extension BackupEnvironment { + public static let unimplemented = BackupEnvironment( + messenger: .unimplemented, + backupStorage: .unimplemented, + mainQueue: .unimplemented, + bgQueue: .unimplemented + ) +} +#endif + +public let backupReducer = Reducer<BackupState, BackupAction, BackupEnvironment> +{ state, action, env in + enum TaskEffectId {} + + switch action { + case .task: + state.isRunning = env.messenger.isBackupRunning() + return Effect.run { subscriber in + subscriber.send(.backupUpdated(env.backupStorage.stored())) + 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 + state.focusedField = nil + return Effect.run { [state] subscriber in + do { + let contact = try env.messenger.myContact(includeFacts: .types([.username])) + guard let username = try contact.getFact(.username)?.value else { + throw BackupState.Error.contactUsernameMissing + } + 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() + try 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 new file mode 100644 index 0000000000000000000000000000000000000000..89510b2fbf2c993afa1438fd62408dc925d1c30e --- /dev/null +++ b/Examples/xx-messenger/Sources/BackupFeature/BackupView.swift @@ -0,0 +1,250 @@ +import ComposableArchitecture +import SwiftUI +import UniformTypeIdentifiers + +public struct BackupView: View { + public init(store: Store<BackupState, BackupAction>) { + self.store = store + } + + let store: Store<BackupState, BackupAction> + @FocusState var focusedField: BackupState.Field? + + struct ViewState: Equatable { + 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) + } + focusedField = state.focusedField + 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 focusedField: BackupState.Field? + 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 || viewStore.backup != nil { + backupSection(viewStore) + } + if !viewStore.isRunning { + newBackupSection(viewStore) + } + } + .disabled(viewStore.isLoading) + .alert( + store.scope(state: \.alert), + dismiss: .alertDismissed + ) + } + .navigationTitle("Backup") + .task { await viewStore.send(.task).finish() } + .onChange(of: viewStore.focusedField) { focusedField = $0 } + .onChange(of: focusedField) { viewStore.send(.set(\.$focusedField, $0)) } + } + } + + @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") } + ) + .textContentType(.password) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .focused($focusedField, equals: .passphrase) + + Button { + viewStore.send(.startTapped) + } label: { + HStack { + Text("Start") + Spacer() + if viewStore.isStarting { + ProgressView() + } else { + Image(systemName: "play.fill") + } + } + } + } header: { + Text("New backup") + } + .disabled(viewStore.isStarting) + } + + @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 +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/Sources/ContactFeature/ContactFeature.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift index 993796e4bf4f286a29fdb7319fb39332074c68da..be66fc8dfc8f59f7f2e20e1a1899a23da10106f6 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactFeature.swift @@ -4,7 +4,9 @@ import CheckContactAuthFeature import ComposableArchitecture import ComposablePresentation import ConfirmRequestFeature +import ContactLookupFeature import Foundation +import ResetAuthFeature import SendRequestFeature import VerifyContactFeature import XCTestDynamicOverlay @@ -20,10 +22,12 @@ public struct ContactState: Equatable { importUsername: Bool = true, importEmail: Bool = true, importPhone: Bool = true, + lookup: ContactLookupState? = nil, sendRequest: SendRequestState? = nil, verifyContact: VerifyContactState? = nil, confirmRequest: ConfirmRequestState? = nil, checkAuth: CheckContactAuthState? = nil, + resetAuth: ResetAuthState? = nil, chat: ChatState? = nil ) { self.id = id @@ -32,10 +36,12 @@ public struct ContactState: Equatable { self.importUsername = importUsername self.importEmail = importEmail self.importPhone = importPhone + self.lookup = lookup self.sendRequest = sendRequest self.verifyContact = verifyContact self.confirmRequest = confirmRequest self.checkAuth = checkAuth + self.resetAuth = resetAuth self.chat = chat } @@ -45,10 +51,12 @@ public struct ContactState: Equatable { @BindableState public var importUsername: Bool @BindableState public var importEmail: Bool @BindableState public var importPhone: Bool + public var lookup: ContactLookupState? public var sendRequest: SendRequestState? public var verifyContact: VerifyContactState? public var confirmRequest: ConfirmRequestState? public var checkAuth: CheckContactAuthState? + public var resetAuth: ResetAuthState? public var chat: ChatState? } @@ -56,6 +64,9 @@ public enum ContactAction: Equatable, BindableAction { case start case dbContactFetched(XXModels.Contact?) case importFactsTapped + case lookupTapped + case lookupDismissed + case lookup(ContactLookupAction) case sendRequestTapped case sendRequestDismissed case sendRequest(SendRequestAction) @@ -68,6 +79,9 @@ public enum ContactAction: Equatable, BindableAction { case confirmRequestTapped case confirmRequestDismissed case confirmRequest(ConfirmRequestAction) + case resetAuthTapped + case resetAuthDismissed + case resetAuth(ResetAuthAction) case chatTapped case chatDismissed case chat(ChatAction) @@ -80,20 +94,24 @@ public struct ContactEnvironment { db: DBManagerGetDB, mainQueue: AnySchedulerOf<DispatchQueue>, bgQueue: AnySchedulerOf<DispatchQueue>, + lookup: @escaping () -> ContactLookupEnvironment, sendRequest: @escaping () -> SendRequestEnvironment, verifyContact: @escaping () -> VerifyContactEnvironment, confirmRequest: @escaping () -> ConfirmRequestEnvironment, checkAuth: @escaping () -> CheckContactAuthEnvironment, + resetAuth: @escaping () -> ResetAuthEnvironment, chat: @escaping () -> ChatEnvironment ) { self.messenger = messenger self.db = db self.mainQueue = mainQueue self.bgQueue = bgQueue + self.lookup = lookup self.sendRequest = sendRequest self.verifyContact = verifyContact self.confirmRequest = confirmRequest self.checkAuth = checkAuth + self.resetAuth = resetAuth self.chat = chat } @@ -101,10 +119,12 @@ public struct ContactEnvironment { public var db: DBManagerGetDB public var mainQueue: AnySchedulerOf<DispatchQueue> public var bgQueue: AnySchedulerOf<DispatchQueue> + public var lookup: () -> ContactLookupEnvironment public var sendRequest: () -> SendRequestEnvironment public var verifyContact: () -> VerifyContactEnvironment public var confirmRequest: () -> ConfirmRequestEnvironment public var checkAuth: () -> CheckContactAuthEnvironment + public var resetAuth: () -> ResetAuthEnvironment public var chat: () -> ChatEnvironment } @@ -115,10 +135,12 @@ extension ContactEnvironment { db: .unimplemented, mainQueue: .unimplemented, bgQueue: .unimplemented, + lookup: { .unimplemented }, sendRequest: { .unimplemented }, verifyContact: { .unimplemented }, confirmRequest: { .unimplemented }, checkAuth: { .unimplemented }, + resetAuth: { .unimplemented }, chat: { .unimplemented } ) } @@ -163,6 +185,19 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm .receive(on: env.mainQueue) .eraseToEffect() + case .lookupTapped: + state.lookup = ContactLookupState(id: state.id) + return .none + + case .lookupDismissed: + state.lookup = nil + return .none + + case .lookup(.didLookup(let xxContact)): + state.xxContact = xxContact + state.lookup = nil + return .none + case .sendRequestTapped: if let xxContact = state.xxContact { state.sendRequest = SendRequestState(contact: xxContact) @@ -223,11 +258,32 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm state.chat = nil return .none - case .binding(_), .sendRequest(_), .verifyContact(_), .confirmRequest(_), .checkAuth(_), .chat(_): + case .resetAuthTapped: + if let marshaled = state.dbContact?.marshaled { + state.resetAuth = ResetAuthState( + partner: .live(marshaled) + ) + } + return .none + + case .resetAuthDismissed: + state.resetAuth = nil + return .none + + case .binding(_), .lookup(_), .sendRequest(_), + .verifyContact(_), .confirmRequest(_), + .checkAuth(_), .resetAuth(_), .chat(_): return .none } } .binding() +.presenting( + contactLookupReducer, + state: .keyPath(\.lookup), + id: .notNil(), + action: /ContactAction.lookup, + environment: { $0.lookup() } +) .presenting( sendRequestReducer, state: .keyPath(\.sendRequest), @@ -256,6 +312,13 @@ public let contactReducer = Reducer<ContactState, ContactAction, ContactEnvironm action: /ContactAction.checkAuth, environment: { $0.checkAuth() } ) +.presenting( + resetAuthReducer, + state: .keyPath(\.resetAuth), + id: .notNil(), + action: /ContactAction.resetAuth, + environment: { $0.resetAuth() } +) .presenting( chatReducer, state: .keyPath(\.chat), diff --git a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift index d775df01eeed7ee6175b00d231149a351be84882..7da763e03748536aabb0cc99698c2edd0ee0fba4 100644 --- a/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift +++ b/Examples/xx-messenger/Sources/ContactFeature/ContactView.swift @@ -4,6 +4,8 @@ import CheckContactAuthFeature import ComposableArchitecture import ComposablePresentation import ConfirmRequestFeature +import ContactLookupFeature +import ResetAuthFeature import SendRequestFeature import SwiftUI import VerifyContactFeature @@ -26,6 +28,12 @@ public struct ContactView: View { var importUsername: Bool var importEmail: Bool var importPhone: Bool + var canLookup: Bool + var canSendRequest: Bool + var canVerifyContact: Bool + var canConfirmRequest: Bool + var canCheckAuthorization: Bool + var canResetAuthorization: Bool init(state: ContactState) { dbContact = state.dbContact @@ -36,6 +44,12 @@ public struct ContactView: View { importUsername = state.importUsername importEmail = state.importEmail importPhone = state.importPhone + canLookup = state.dbContact?.id != nil + canSendRequest = state.xxContact != nil || state.dbContact?.marshaled != nil + canVerifyContact = state.dbContact?.marshaled != nil + canConfirmRequest = state.dbContact?.marshaled != nil + canCheckAuthorization = state.dbContact?.marshaled != nil + canResetAuthorization = state.dbContact?.marshaled != nil } } @@ -100,15 +114,30 @@ public struct ContactView: View { if let dbContact = viewStore.dbContact { Section { + Label(dbContact.id.hexString(), systemImage: "number") + .font(.footnote.monospaced()) Label(dbContact.username ?? "", systemImage: "person") Label(dbContact.email ?? "", systemImage: "envelope") Label(dbContact.phone ?? "", systemImage: "phone") } header: { Text("Contact") } + .textSelection(.enabled) Section { ContactAuthStatusView(dbContact.authStatus) + + Button { + viewStore.send(.lookupTapped) + } label: { + HStack { + Text("Lookup") + Spacer() + Image(systemName: "chevron.forward") + } + } + .disabled(!viewStore.canLookup) + Button { viewStore.send(.sendRequestTapped) } label: { @@ -118,6 +147,8 @@ public struct ContactView: View { Image(systemName: "chevron.forward") } } + .disabled(!viewStore.canSendRequest) + Button { viewStore.send(.verifyContactTapped) } label: { @@ -127,6 +158,8 @@ public struct ContactView: View { Image(systemName: "chevron.forward") } } + .disabled(!viewStore.canVerifyContact) + Button { viewStore.send(.confirmRequestTapped) } label: { @@ -136,6 +169,8 @@ public struct ContactView: View { Image(systemName: "chevron.forward") } } + .disabled(!viewStore.canConfirmRequest) + Button { viewStore.send(.checkAuthTapped) } label: { @@ -145,6 +180,18 @@ public struct ContactView: View { Image(systemName: "chevron.forward") } } + .disabled(!viewStore.canCheckAuthorization) + + Button { + viewStore.send(.resetAuthTapped) + } label: { + HStack { + Text("Reset authorization") + Spacer() + Image(systemName: "chevron.forward") + } + } + .disabled(!viewStore.canResetAuthorization) } header: { Text("Auth") } @@ -167,6 +214,15 @@ public struct ContactView: View { } .navigationTitle("Contact") .task { viewStore.send(.start) } + .background(NavigationLinkWithStore( + store.scope( + state: \.lookup, + action: ContactAction.lookup + ), + mapState: replayNonNil(), + onDeactivate: { viewStore.send(.lookupDismissed) }, + destination: ContactLookupView.init(store:) + )) .background(NavigationLinkWithStore( store.scope( state: \.sendRequest, @@ -200,6 +256,14 @@ public struct ContactView: View { onDeactivate: { viewStore.send(.checkAuthDismissed) }, destination: CheckContactAuthView.init(store:) )) + .background(NavigationLinkWithStore( + store.scope( + state: \.resetAuth, + action: ContactAction.resetAuth + ), + onDeactivate: { viewStore.send(.resetAuthDismissed) }, + destination: ResetAuthView.init(store:) + )) .background(NavigationLinkWithStore( store.scope( state: \.chat, diff --git a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift new file mode 100644 index 0000000000000000000000000000000000000000..0b6f92dd46b351f6d10f78ef4b50961dad3e1fae --- /dev/null +++ b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupFeature.swift @@ -0,0 +1,83 @@ +import ComposableArchitecture +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient + +public struct ContactLookupState: Equatable { + public init( + id: Data, + isLookingUp: Bool = false, + failure: String? = nil + ) { + self.id = id + self.isLookingUp = isLookingUp + self.failure = failure + } + + public var id: Data + public var isLookingUp: Bool + public var failure: String? +} + +public enum ContactLookupAction: Equatable { + case lookupTapped + case didLookup(XXClient.Contact) + case didFail(NSError) +} + +public struct ContactLookupEnvironment { + public init( + messenger: Messenger, + mainQueue: AnySchedulerOf<DispatchQueue>, + bgQueue: AnySchedulerOf<DispatchQueue> + ) { + self.messenger = messenger + self.mainQueue = mainQueue + self.bgQueue = bgQueue + } + + public var messenger: Messenger + public var mainQueue: AnySchedulerOf<DispatchQueue> + public var bgQueue: AnySchedulerOf<DispatchQueue> +} + +#if DEBUG +extension ContactLookupEnvironment { + public static let unimplemented = ContactLookupEnvironment( + messenger: .unimplemented, + mainQueue: .unimplemented, + bgQueue: .unimplemented + ) +} +#endif + +public let contactLookupReducer = Reducer<ContactLookupState, ContactLookupAction, ContactLookupEnvironment> +{ state, action, env in + switch action { + case .lookupTapped: + state.isLookingUp = true + state.failure = nil + return Effect.result { [state] in + do { + let contact = try env.messenger.lookupContact(id: state.id) + return .success(.didLookup(contact)) + } catch { + return .success(.didFail(error as NSError)) + } + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + + case .didLookup(_): + state.isLookingUp = false + state.failure = nil + return .none + + case .didFail(let error): + state.isLookingUp = false + state.failure = error.localizedDescription + return .none + } +} diff --git a/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift new file mode 100644 index 0000000000000000000000000000000000000000..6ce83eda6ba5dacfcf15c2fca4e8c202087ef42d --- /dev/null +++ b/Examples/xx-messenger/Sources/ContactLookupFeature/ContactLookupView.swift @@ -0,0 +1,76 @@ +import AppCore +import ComposableArchitecture +import SwiftUI + +public struct ContactLookupView: View { + public init(store: Store<ContactLookupState, ContactLookupAction>) { + self.store = store + } + + let store: Store<ContactLookupState, ContactLookupAction> + + struct ViewState: Equatable { + init(state: ContactLookupState) { + id = state.id + isLookingUp = state.isLookingUp + failure = state.failure + } + + var id: Data + var isLookingUp: Bool + var failure: String? + } + + public var body: some View { + WithViewStore(store, observe: ViewState.init) { viewStore in + Form { + Section { + Label(viewStore.id.hexString(), systemImage: "number") + .font(.footnote.monospaced()) + + Button { + viewStore.send(.lookupTapped) + } label: { + HStack { + Text("Lookup") + Spacer() + if viewStore.isLookingUp { + ProgressView() + } else { + Image(systemName: "magnifyingglass") + } + } + } + .disabled(viewStore.isLookingUp) + } header: { + Text("Contact ID") + } + + if let failure = viewStore.failure { + Section { + Text(failure) + } header: { + Text("Error") + } + } + } + .navigationTitle("Lookup") + } + } +} + +#if DEBUG +public struct ContactLookupView_Previews: PreviewProvider { + public static var previews: some View { + NavigationView { + ContactLookupView(store: Store( + initialState: ContactLookupState( + id: "1234".data(using: .utf8)! + ), + reducer: .empty, + environment: () + )) + } + } +} +#endif diff --git a/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift b/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift index 1ded89de4cbf9dd44014395e3e79bf6ff2fdcc20..680a231ec8cc9e6e13487a849a65941bda7a7428 100644 --- a/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift +++ b/Examples/xx-messenger/Sources/ContactsFeature/ContactsFeature.swift @@ -3,6 +3,7 @@ import ComposableArchitecture import ComposablePresentation import ContactFeature import Foundation +import MyContactFeature import XCTestDynamicOverlay import XXClient import XXMessengerClient @@ -12,16 +13,19 @@ public struct ContactsState: Equatable { public init( myId: Data? = nil, contacts: IdentifiedArrayOf<XXModels.Contact> = [], - contact: ContactState? = nil + contact: ContactState? = nil, + myContact: MyContactState? = nil ) { self.myId = myId self.contacts = contacts self.contact = contact + self.myContact = myContact } public var myId: Data? public var contacts: IdentifiedArrayOf<XXModels.Contact> public var contact: ContactState? + public var myContact: MyContactState? } public enum ContactsAction: Equatable { @@ -30,6 +34,9 @@ public enum ContactsAction: Equatable { case contactSelected(XXModels.Contact) case contactDismissed case contact(ContactAction) + case myContactSelected + case myContactDismissed + case myContact(MyContactAction) } public struct ContactsEnvironment { @@ -38,13 +45,15 @@ public struct ContactsEnvironment { db: DBManagerGetDB, mainQueue: AnySchedulerOf<DispatchQueue>, bgQueue: AnySchedulerOf<DispatchQueue>, - contact: @escaping () -> ContactEnvironment + contact: @escaping () -> ContactEnvironment, + myContact: @escaping () -> MyContactEnvironment ) { self.messenger = messenger self.db = db self.mainQueue = mainQueue self.bgQueue = bgQueue self.contact = contact + self.myContact = myContact } public var messenger: Messenger @@ -52,6 +61,7 @@ public struct ContactsEnvironment { public var mainQueue: AnySchedulerOf<DispatchQueue> public var bgQueue: AnySchedulerOf<DispatchQueue> public var contact: () -> ContactEnvironment + public var myContact: () -> MyContactEnvironment } #if DEBUG @@ -61,7 +71,8 @@ extension ContactsEnvironment { db: .unimplemented, mainQueue: .unimplemented, bgQueue: .unimplemented, - contact: { .unimplemented } + contact: { .unimplemented }, + myContact: { .unimplemented } ) } #endif @@ -96,7 +107,15 @@ public let contactsReducer = Reducer<ContactsState, ContactsAction, ContactsEnvi state.contact = nil return .none - case .contact(_): + case .myContactSelected: + state.myContact = MyContactState() + return .none + + case .myContactDismissed: + state.myContact = nil + return .none + + case .contact(_), .myContact(_): return .none } } @@ -107,3 +126,10 @@ public let contactsReducer = Reducer<ContactsState, ContactsAction, ContactsEnvi action: /ContactsAction.contact, environment: { $0.contact() } ) +.presenting( + myContactReducer, + state: .keyPath(\.myContact), + id: .notNil(), + action: /ContactsAction.myContact, + environment: { $0.myContact() } +) diff --git a/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift b/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift index ce811f9d7bab2d22cb530cc6f2725be004a603e4..e09725d92c84feeb7f8cb7da5dbc38cf9d998087 100644 --- a/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift +++ b/Examples/xx-messenger/Sources/ContactsFeature/ContactsView.swift @@ -2,6 +2,7 @@ import AppCore import ComposableArchitecture import ComposablePresentation import ContactFeature +import MyContactFeature import SwiftUI import XXModels @@ -28,13 +29,21 @@ public struct ContactsView: View { ForEach(viewStore.contacts) { contact in if contact.id == viewStore.myId { Section { - VStack(alignment: .leading, spacing: 8) { - Label(contact.username ?? "", systemImage: "person") - Label(contact.email ?? "", systemImage: "envelope") - Label(contact.phone ?? "", systemImage: "phone") + Button { + viewStore.send(.myContactSelected) + } label: { + HStack { + VStack(alignment: .leading, spacing: 8) { + Label(contact.username ?? "", systemImage: "person") + Label(contact.email ?? "", systemImage: "envelope") + Label(contact.phone ?? "", systemImage: "phone") + } + .font(.callout) + .tint(Color.primary) + Spacer() + Image(systemName: "chevron.forward") + } } - .font(.callout) - .tint(Color.primary) } header: { Text("My contact") } @@ -70,6 +79,14 @@ public struct ContactsView: View { onDeactivate: { viewStore.send(.contactDismissed) }, destination: ContactView.init(store:) )) + .background(NavigationLinkWithStore( + store.scope( + state: \.myContact, + action: ContactsAction.myContact + ), + onDeactivate: { viewStore.send(.myContactDismissed) }, + destination: MyContactView.init(store:) + )) } } } diff --git a/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift b/Examples/xx-messenger/Sources/HomeFeature/HomeFeature.swift index 894c5aca32ca1170d0acd47ae89a8242f2e73cb8..f2015ebc2434ecaada1ba9abd4d345cea91bd03e 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 @@ -14,30 +15,26 @@ 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, alert: AlertState<HomeAction>? = nil, register: RegisterState? = nil, contacts: ContactsState? = nil, - userSearch: UserSearchState? = nil + userSearch: UserSearchState? = nil, + backup: BackupState? = nil ) { self.failure = failure - self.authFailure = authFailure - self.messageListenerFailure = messageListenerFailure self.isNetworkHealthy = isNetworkHealthy self.isDeletingAccount = isDeletingAccount self.alert = alert self.register = register self.contacts = contacts self.userSearch = userSearch + self.backup = backup } public var failure: String? - public var authFailure: String? - public var messageListenerFailure: String? public var isNetworkHealthy: Bool? public var networkNodesReport: NodeRegistrationReport? public var isDeletingAccount: Bool @@ -45,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 { @@ -55,20 +53,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 +68,6 @@ public enum HomeAction: Equatable { } case messenger(Messenger) - case authHandler(AuthHandler) - case messageListener(MessageListener) case networkMonitor(NetworkMonitor) case deleteAccount(DeleteAccount) case didDismissAlert @@ -94,71 +76,68 @@ 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 { public init( messenger: Messenger, dbManager: DBManager, - authHandler: AuthCallbackHandler, - messageListener: MessageListenerHandler, mainQueue: AnySchedulerOf<DispatchQueue>, bgQueue: AnySchedulerOf<DispatchQueue>, register: @escaping () -> RegisterEnvironment, contacts: @escaping () -> ContactsEnvironment, - userSearch: @escaping () -> UserSearchEnvironment + userSearch: @escaping () -> UserSearchEnvironment, + backup: @escaping () -> BackupEnvironment ) { self.messenger = messenger self.dbManager = dbManager - self.authHandler = authHandler - self.messageListener = messageListener self.mainQueue = mainQueue self.bgQueue = bgQueue self.register = register self.contacts = contacts self.userSearch = userSearch + self.backup = backup } 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 public var contacts: () -> ContactsEnvironment public var userSearch: () -> UserSearchEnvironment + public var backup: () -> BackupEnvironment } +#if DEBUG extension HomeEnvironment { public static let unimplemented = HomeEnvironment( messenger: .unimplemented, dbManager: .unimplemented, - authHandler: .unimplemented, - messageListener: .unimplemented, mainQueue: .unimplemented, bgQueue: .unimplemented, register: { .unimplemented }, contacts: { .unimplemented }, - userSearch: { .unimplemented } + userSearch: { .unimplemented }, + backup: { .unimplemented } ) } +#endif 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 +145,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() } @@ -176,6 +158,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))) @@ -197,52 +183,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 @@ -344,7 +284,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 } } @@ -369,3 +317,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 8cd7259b6c57a7054b1bae658df8772b0a7efa3d..8a1775d6a84ffdfc6b5ac1b2b816a8ee686b039c 100644 --- a/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift +++ b/Examples/xx-messenger/Sources/HomeFeature/HomeView.swift @@ -1,3 +1,5 @@ +import AppCore +import BackupFeature import ComposableArchitecture import ComposablePresentation import ContactsFeature @@ -15,16 +17,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 +46,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") @@ -141,6 +113,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: { @@ -156,6 +138,12 @@ public struct HomeView: View { } header: { Text("Account") } + + Section { + AppVersionText() + } header: { + Text("App version") + } } .navigationTitle("Home") .alert( @@ -182,6 +170,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/Sources/MyContactFeature/Alerts.swift b/Examples/xx-messenger/Sources/MyContactFeature/Alerts.swift new file mode 100644 index 0000000000000000000000000000000000000000..321139aec18ce7ae51b9be8097878a9133798236 --- /dev/null +++ b/Examples/xx-messenger/Sources/MyContactFeature/Alerts.swift @@ -0,0 +1,11 @@ +import ComposableArchitecture + +extension AlertState { + public static func error(_ message: String) -> AlertState<MyContactAction> { + AlertState<MyContactAction>( + title: TextState("Error"), + message: TextState(message), + buttons: [] + ) + } +} diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift new file mode 100644 index 0000000000000000000000000000000000000000..434a1aca3c7cdc74b340c54a22558fa67a90a0eb --- /dev/null +++ b/Examples/xx-messenger/Sources/MyContactFeature/MyContactFeature.swift @@ -0,0 +1,316 @@ +import AppCore +import Combine +import ComposableArchitecture +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient +import XXModels + +public struct MyContactState: Equatable { + public enum Field: String, Hashable { + case email + case emailCode + case phone + case phoneCode + } + + public init( + contact: XXModels.Contact? = nil, + focusedField: Field? = nil, + email: String = "", + emailConfirmationID: String? = nil, + emailConfirmationCode: String = "", + isRegisteringEmail: Bool = false, + isConfirmingEmail: Bool = false, + isUnregisteringEmail: Bool = false, + phone: String = "", + phoneConfirmationID: String? = nil, + phoneConfirmationCode: String = "", + isRegisteringPhone: Bool = false, + isConfirmingPhone: Bool = false, + isUnregisteringPhone: Bool = false, + isLoadingFacts: Bool = false, + alert: AlertState<MyContactAction>? = nil + ) { + self.contact = contact + self.focusedField = focusedField + self.email = email + self.emailConfirmationID = emailConfirmationID + self.emailConfirmationCode = emailConfirmationCode + self.isRegisteringEmail = isRegisteringEmail + self.isConfirmingEmail = isConfirmingEmail + self.isUnregisteringEmail = isUnregisteringEmail + self.phone = phone + self.phoneConfirmationID = phoneConfirmationID + self.phoneConfirmationCode = phoneConfirmationCode + self.isRegisteringPhone = isRegisteringPhone + self.isConfirmingPhone = isConfirmingPhone + self.isUnregisteringPhone = isUnregisteringPhone + self.isLoadingFacts = isLoadingFacts + self.alert = alert + } + + public var contact: XXModels.Contact? + @BindableState public var focusedField: Field? + @BindableState public var email: String + @BindableState public var emailConfirmationID: String? + @BindableState public var emailConfirmationCode: String + @BindableState public var isRegisteringEmail: Bool + @BindableState public var isConfirmingEmail: Bool + @BindableState public var isUnregisteringEmail: Bool + @BindableState public var phone: String + @BindableState public var phoneConfirmationID: String? + @BindableState public var phoneConfirmationCode: String + @BindableState public var isRegisteringPhone: Bool + @BindableState public var isConfirmingPhone: Bool + @BindableState public var isUnregisteringPhone: Bool + @BindableState public var isLoadingFacts: Bool + public var alert: AlertState<MyContactAction>? +} + +public enum MyContactAction: Equatable, BindableAction { + case start + case contactFetched(XXModels.Contact?) + case registerEmailTapped + case confirmEmailTapped + case unregisterEmailTapped + case registerPhoneTapped + case confirmPhoneTapped + case unregisterPhoneTapped + case loadFactsTapped + case didFail(String) + case alertDismissed + case binding(BindingAction<MyContactState>) +} + +public struct MyContactEnvironment { + public init( + messenger: Messenger, + db: DBManagerGetDB, + mainQueue: AnySchedulerOf<DispatchQueue>, + bgQueue: AnySchedulerOf<DispatchQueue> + ) { + self.messenger = messenger + self.db = db + self.mainQueue = mainQueue + self.bgQueue = bgQueue + } + + public var messenger: Messenger + public var db: DBManagerGetDB + public var mainQueue: AnySchedulerOf<DispatchQueue> + public var bgQueue: AnySchedulerOf<DispatchQueue> +} + +#if DEBUG +extension MyContactEnvironment { + public static let unimplemented = MyContactEnvironment( + messenger: .unimplemented, + db: .unimplemented, + mainQueue: .unimplemented, + bgQueue: .unimplemented + ) +} +#endif + +public let myContactReducer = Reducer<MyContactState, MyContactAction, MyContactEnvironment> +{ state, action, env in + enum DBFetchEffectID {} + + switch action { + case .start: + return Effect + .catching { try env.messenger.e2e.tryGet().getContact().getId() } + .tryMap { try env.db().fetchContactsPublisher(.init(id: [$0])) } + .flatMap { $0 } + .assertNoFailure() + .map(\.first) + .map(MyContactAction.contactFetched) + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + .cancellable(id: DBFetchEffectID.self, cancelInFlight: true) + + case .contactFetched(let contact): + state.contact = contact + return .none + + case .registerEmailTapped: + state.focusedField = nil + state.isRegisteringEmail = true + return Effect.run { [state] subscriber in + do { + let ud = try env.messenger.ud.tryGet() + let fact = Fact(type: .email, value: state.email) + let confirmationID = try ud.sendRegisterFact(fact) + subscriber.send(.set(\.$emailConfirmationID, confirmationID)) + } catch { + subscriber.send(.didFail(error.localizedDescription)) + } + subscriber.send(.set(\.$isRegisteringEmail, false)) + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + + case .confirmEmailTapped: + guard let confirmationID = state.emailConfirmationID else { return .none } + state.focusedField = nil + state.isConfirmingEmail = true + return Effect.run { [state] subscriber in + do { + let ud = try env.messenger.ud.tryGet() + try ud.confirmFact(confirmationId: confirmationID, code: state.emailConfirmationCode) + let contactId = try env.messenger.e2e.tryGet().getContact().getId() + if var dbContact = try env.db().fetchContacts(.init(id: [contactId])).first { + dbContact.email = state.email + try env.db().saveContact(dbContact) + } + subscriber.send(.set(\.$email, "")) + subscriber.send(.set(\.$emailConfirmationID, nil)) + subscriber.send(.set(\.$emailConfirmationCode, "")) + } catch { + subscriber.send(.didFail(error.localizedDescription)) + } + subscriber.send(.set(\.$isConfirmingEmail, false)) + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + + case .unregisterEmailTapped: + guard let email = state.contact?.email else { return .none } + state.isUnregisteringEmail = true + return Effect.run { [state] subscriber in + do { + let ud: UserDiscovery = try env.messenger.ud.tryGet() + let fact = Fact(type: .email, value: email) + try ud.removeFact(fact) + let contactId = try env.messenger.e2e.tryGet().getContact().getId() + if var dbContact = try env.db().fetchContacts(.init(id: [contactId])).first { + dbContact.email = nil + try env.db().saveContact(dbContact) + } + } catch { + subscriber.send(.didFail(error.localizedDescription)) + } + subscriber.send(.set(\.$isUnregisteringEmail, false)) + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + + case .registerPhoneTapped: + state.focusedField = nil + state.isRegisteringPhone = true + return Effect.run { [state] subscriber in + do { + let ud = try env.messenger.ud.tryGet() + let fact = Fact(type: .phone, value: state.phone) + let confirmationID = try ud.sendRegisterFact(fact) + subscriber.send(.set(\.$phoneConfirmationID, confirmationID)) + } catch { + subscriber.send(.didFail(error.localizedDescription)) + } + subscriber.send(.set(\.$isRegisteringPhone, false)) + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + + case .confirmPhoneTapped: + guard let confirmationID = state.phoneConfirmationID else { return .none } + state.focusedField = nil + state.isConfirmingPhone = true + return Effect.run { [state] subscriber in + do { + let ud = try env.messenger.ud.tryGet() + try ud.confirmFact(confirmationId: confirmationID, code: state.phoneConfirmationCode) + let contactId = try env.messenger.e2e.tryGet().getContact().getId() + if var dbContact = try env.db().fetchContacts(.init(id: [contactId])).first { + dbContact.phone = state.phone + try env.db().saveContact(dbContact) + } + subscriber.send(.set(\.$phone, "")) + subscriber.send(.set(\.$phoneConfirmationID, nil)) + subscriber.send(.set(\.$phoneConfirmationCode, "")) + } catch { + subscriber.send(.didFail(error.localizedDescription)) + } + subscriber.send(.set(\.$isConfirmingPhone, false)) + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + + case .unregisterPhoneTapped: + guard let phone = state.contact?.phone else { return .none } + state.isUnregisteringPhone = true + return Effect.run { [state] subscriber in + do { + let ud: UserDiscovery = try env.messenger.ud.tryGet() + let fact = Fact(type: .phone, value: phone) + try ud.removeFact(fact) + let contactId = try env.messenger.e2e.tryGet().getContact().getId() + if var dbContact = try env.db().fetchContacts(.init(id: [contactId])).first { + dbContact.phone = nil + try env.db().saveContact(dbContact) + } + } catch { + subscriber.send(.didFail(error.localizedDescription)) + } + subscriber.send(.set(\.$isUnregisteringPhone, false)) + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + + case .loadFactsTapped: + state.isLoadingFacts = true + return Effect.run { subscriber in + do { + let contactId = try env.messenger.e2e.tryGet().getContact().getId() + if var dbContact = try env.db().fetchContacts(.init(id: [contactId])).first { + let facts = try env.messenger.ud.tryGet().getFacts() + dbContact.username = facts.get(.username)?.value + dbContact.email = facts.get(.email)?.value + dbContact.phone = facts.get(.phone)?.value + try env.db().saveContact(dbContact) + } + } catch { + subscriber.send(.didFail(error.localizedDescription)) + } + subscriber.send(.set(\.$isLoadingFacts, false)) + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + + case .didFail(let failure): + state.alert = .error(failure) + return .none + + case .alertDismissed: + state.alert = nil + return .none + + case .binding(_): + return .none + } +} +.binding() diff --git a/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift new file mode 100644 index 0000000000000000000000000000000000000000..d32a6f68848e89ad7eeb4a9bf5f25b543d379f40 --- /dev/null +++ b/Examples/xx-messenger/Sources/MyContactFeature/MyContactView.swift @@ -0,0 +1,260 @@ +import AppCore +import ComposableArchitecture +import SwiftUI +import XXModels + +public struct MyContactView: View { + public init(store: Store<MyContactState, MyContactAction>) { + self.store = store + } + + let store: Store<MyContactState, MyContactAction> + @FocusState var focusedField: MyContactState.Field? + + struct ViewState: Equatable { + init(state: MyContactState) { + contact = state.contact + focusedField = state.focusedField + email = state.email + emailConfirmation = state.emailConfirmationID != nil + emailCode = state.emailConfirmationCode + isRegisteringEmail = state.isRegisteringEmail + isConfirmingEmail = state.isConfirmingEmail + isUnregisteringEmail = state.isUnregisteringEmail + phone = state.phone + phoneConfirmation = state.phoneConfirmationID != nil + phoneCode = state.phoneConfirmationCode + isRegisteringPhone = state.isRegisteringPhone + isConfirmingPhone = state.isConfirmingPhone + isUnregisteringPhone = state.isUnregisteringPhone + isLoadingFacts = state.isLoadingFacts + } + + var contact: XXModels.Contact? + var focusedField: MyContactState.Field? + var email: String + var emailConfirmation: Bool + var emailCode: String + var isRegisteringEmail: Bool + var isConfirmingEmail: Bool + var isUnregisteringEmail: Bool + var phone: String + var phoneConfirmation: Bool + var phoneCode: String + var isRegisteringPhone: Bool + var isConfirmingPhone: Bool + var isUnregisteringPhone: Bool + var isLoadingFacts: Bool + } + + public var body: some View { + WithViewStore(store, observe: ViewState.init) { viewStore in + Form { + Section { + Text(viewStore.contact?.id.hexString() ?? "") + .font(.footnote.monospaced()) + .textSelection(.enabled) + } header: { + Label("ID", systemImage: "number") + } + + Section { + Text(viewStore.contact?.username ?? "") + .textSelection(.enabled) + } header: { + Label("Username", systemImage: "person") + } + + Section { + if let contact = viewStore.contact { + if let email = contact.email { + Text(email) + .textSelection(.enabled) + Button(role: .destructive) { + viewStore.send(.unregisterEmailTapped) + } label: { + HStack { + Text("Unregister") + Spacer() + if viewStore.isUnregisteringEmail { + ProgressView() + } + } + } + .disabled(viewStore.isUnregisteringEmail) + } else { + TextField( + text: viewStore.binding( + get: \.email, + send: { MyContactAction.set(\.$email, $0) } + ), + prompt: Text("Enter email"), + label: { Text("Email") } + ) + .focused($focusedField, equals: .email) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .disabled(viewStore.isRegisteringEmail || viewStore.emailConfirmation) + if viewStore.emailConfirmation { + TextField( + text: viewStore.binding( + get: \.emailCode, + send: { MyContactAction.set(\.$emailConfirmationCode, $0) } + ), + prompt: Text("Enter confirmation code"), + label: { Text("Confirmation code") } + ) + .focused($focusedField, equals: .emailCode) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .disabled(viewStore.isConfirmingEmail) + Button { + viewStore.send(.confirmEmailTapped) + } label: { + HStack { + Text("Confirm") + Spacer() + if viewStore.isConfirmingEmail { + ProgressView() + } + } + } + .disabled(viewStore.isConfirmingEmail) + } else { + Button { + viewStore.send(.registerEmailTapped) + } label: { + HStack { + Text("Register") + Spacer() + if viewStore.isRegisteringEmail { + ProgressView() + } + } + } + .disabled(viewStore.isRegisteringEmail) + } + } + } else { + Text("") + } + } header: { + Label("Email", systemImage: "envelope") + } + + Section { + if let contact = viewStore.contact { + if let phone = contact.phone { + Text(phone) + .textSelection(.enabled) + Button(role: .destructive) { + viewStore.send(.unregisterPhoneTapped) + } label: { + HStack { + Text("Unregister") + Spacer() + if viewStore.isUnregisteringPhone { + ProgressView() + } + } + } + .disabled(viewStore.isUnregisteringPhone) + } else { + TextField( + text: viewStore.binding( + get: \.phone, + send: { MyContactAction.set(\.$phone, $0) } + ), + prompt: Text("Enter phone"), + label: { Text("Phone") } + ) + .focused($focusedField, equals: .phone) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .disabled(viewStore.isRegisteringPhone || viewStore.phoneConfirmation) + if viewStore.phoneConfirmation { + TextField( + text: viewStore.binding( + get: \.phoneCode, + send: { MyContactAction.set(\.$phoneConfirmationCode, $0) } + ), + prompt: Text("Enter confirmation code"), + label: { Text("Confirmation code") } + ) + .focused($focusedField, equals: .phoneCode) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .disabled(viewStore.isConfirmingPhone) + Button { + viewStore.send(.confirmPhoneTapped) + } label: { + HStack { + Text("Confirm") + Spacer() + if viewStore.isConfirmingPhone { + ProgressView() + } + } + } + .disabled(viewStore.isConfirmingPhone) + } else { + Button { + viewStore.send(.registerPhoneTapped) + } label: { + HStack { + Text("Register") + Spacer() + if viewStore.isRegisteringPhone { + ProgressView() + } + } + } + .disabled(viewStore.isRegisteringPhone) + } + } + } else { + Text("") + } + } header: { + Label("Phone", systemImage: "phone") + } + + Section { + Button { + viewStore.send(.loadFactsTapped) + } label: { + HStack { + Text("Reload facts") + Spacer() + if viewStore.isLoadingFacts { + ProgressView() + } + } + } + .disabled(viewStore.isLoadingFacts) + } header: { + Text("Actions") + } + } + .navigationTitle("My Contact") + .task { viewStore.send(.start) } + .onChange(of: viewStore.focusedField) { focusedField = $0 } + .onChange(of: focusedField) { viewStore.send(.set(\.$focusedField, $0)) } + .alert(store.scope(state: \.alert), dismiss: .alertDismissed) + } + } +} + +#if DEBUG +public struct MyContactView_Previews: PreviewProvider { + public static var previews: some View { + NavigationView { + MyContactView(store: Store( + initialState: MyContactState(), + reducer: .empty, + environment: () + )) + } + } +} +#endif diff --git a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift b/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift index cb43c43056f6d3e53c2b73b4bdfc10189f052b21..f8fdabefea7d859b3153c6716133d3351bff2fdd 100644 --- a/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift +++ b/Examples/xx-messenger/Sources/RegisterFeature/RegisterFeature.swift @@ -7,6 +7,10 @@ import XXMessengerClient import XXModels public struct RegisterState: Equatable { + public enum Error: Swift.Error, Equatable { + case usernameMismatch(registering: String, registered: String?) + } + public enum Field: String, Hashable { case username } @@ -58,6 +62,7 @@ public struct RegisterEnvironment { public var bgQueue: AnySchedulerOf<DispatchQueue> } +#if DEBUG extension RegisterEnvironment { public static let unimplemented = RegisterEnvironment( messenger: .unimplemented, @@ -67,6 +72,7 @@ extension RegisterEnvironment { bgQueue: .unimplemented ) } +#endif public let registerReducer = Reducer<RegisterState, RegisterAction, RegisterEnvironment> { state, action, env in @@ -82,14 +88,22 @@ public let registerReducer = Reducer<RegisterState, RegisterAction, RegisterEnvi do { let db = try env.db() try env.messenger.register(username: username) - var contact = try env.messenger.e2e.tryGet().getContact() - try contact.setFact(.username, username) + let contact = try env.messenger.myContact() + let facts = try contact.getFacts() try db.saveContact(Contact( id: try contact.getId(), marshaled: contact.data, - username: username, + username: facts.get(.username)?.value, + email: facts.get(.email)?.value, + phone: facts.get(.phone)?.value, createdAt: env.now() )) + guard facts.get(.username)?.value == username else { + throw RegisterState.Error.usernameMismatch( + registering: username, + registered: facts.get(.username)?.value + ) + } fulfill(.success(.finished)) } catch { @@ -106,6 +120,7 @@ public let registerReducer = Reducer<RegisterState, RegisterAction, RegisterEnvi return .none case .finished: + state.isRegistering = false return .none } } diff --git a/Examples/xx-messenger/Sources/ResetAuthFeature/ResetAuthFeature.swift b/Examples/xx-messenger/Sources/ResetAuthFeature/ResetAuthFeature.swift new file mode 100644 index 0000000000000000000000000000000000000000..d4acb74002902f9b9a82da2981f78cd3312deb9e --- /dev/null +++ b/Examples/xx-messenger/Sources/ResetAuthFeature/ResetAuthFeature.swift @@ -0,0 +1,90 @@ +import ComposableArchitecture +import Foundation +import XCTestDynamicOverlay +import XXClient +import XXMessengerClient + +public struct ResetAuthState: Equatable { + public init( + partner: Contact, + isResetting: Bool = false, + failure: String? = nil, + didReset: Bool = false + ) { + self.partner = partner + self.isResetting = isResetting + self.failure = failure + self.didReset = didReset + } + + public var partner: Contact + public var isResetting: Bool + public var failure: String? + public var didReset: Bool +} + +public enum ResetAuthAction: Equatable { + case resetTapped + case didReset + case didFail(String) +} + +public struct ResetAuthEnvironment { + public init( + messenger: Messenger, + mainQueue: AnySchedulerOf<DispatchQueue>, + bgQueue: AnySchedulerOf<DispatchQueue> + ) { + self.messenger = messenger + self.mainQueue = mainQueue + self.bgQueue = bgQueue + } + + public var messenger: Messenger + public var mainQueue: AnySchedulerOf<DispatchQueue> + public var bgQueue: AnySchedulerOf<DispatchQueue> +} + +#if DEBUG +extension ResetAuthEnvironment { + public static let unimplemented = ResetAuthEnvironment( + messenger: .unimplemented, + mainQueue: .unimplemented, + bgQueue: .unimplemented + ) +} +#endif + +public let resetAuthReducer = Reducer<ResetAuthState, ResetAuthAction, ResetAuthEnvironment> +{ state, action, env in + switch action { + case .resetTapped: + state.isResetting = true + state.didReset = false + state.failure = nil + return Effect.result { [state] in + do { + let e2e = try env.messenger.e2e.tryGet() + _ = try e2e.resetAuthenticatedChannel(partner: state.partner) + return .success(.didReset) + } catch { + return .success(.didFail(error.localizedDescription)) + } + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + + case .didReset: + state.isResetting = false + state.didReset = true + state.failure = nil + return .none + + case .didFail(let failure): + state.isResetting = false + state.didReset = false + state.failure = failure + return .none + } +} diff --git a/Examples/xx-messenger/Sources/ResetAuthFeature/ResetAuthView.swift b/Examples/xx-messenger/Sources/ResetAuthFeature/ResetAuthView.swift new file mode 100644 index 0000000000000000000000000000000000000000..7b384efb74d841b4edf04a61cf55aa02a7c0d725 --- /dev/null +++ b/Examples/xx-messenger/Sources/ResetAuthFeature/ResetAuthView.swift @@ -0,0 +1,80 @@ +import AppCore +import ComposableArchitecture +import SwiftUI + +public struct ResetAuthView: View { + public init(store: Store<ResetAuthState, ResetAuthAction>) { + self.store = store + } + + let store: Store<ResetAuthState, ResetAuthAction> + + struct ViewState: Equatable { + init(state: ResetAuthState) { + contactID = try? state.partner.getId() + isResetting = state.isResetting + failure = state.failure + didReset = state.didReset + } + + var contactID: Data? + var isResetting: Bool + var failure: String? + var didReset: Bool + } + + public var body: some View { + WithViewStore(store, observe: ViewState.init) { viewStore in + Form { + Section { + Text(viewStore.contactID?.hexString() ?? "") + .font(.footnote.monospaced()) + .textSelection(.enabled) + } header: { + Label("ID", systemImage: "number") + } + + Button { + viewStore.send(.resetTapped) + } label: { + HStack { + Text("Reset authenticated channel") + Spacer() + if viewStore.isResetting { + ProgressView() + } else if viewStore.didReset { + Image(systemName: "checkmark") + .foregroundColor(.green) + } + } + } + .disabled(viewStore.isResetting) + + if let failure = viewStore.failure { + Section { + Text(failure) + } header: { + Text("Error") + } + } + } + .navigationTitle("Reset auth") + } + } +} + +#if DEBUG +public struct ResetAuthView_Previews: PreviewProvider { + public static var previews: some View { + NavigationView { + ResetAuthView(store: Store( + initialState: ResetAuthState( + partner: .unimplemented(Data()) + ), + reducer: .empty, + environment: () + )) + } + } +} +#endif diff --git a/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift b/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift index 6ce31e5d67acfde939a1bc9ba03e28226f4e0761..6b3d61d340a7932b6367f71894a0efd707ae81d2 100644 --- a/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift +++ b/Examples/xx-messenger/Sources/RestoreFeature/RestoreFeature.swift @@ -1,19 +1,181 @@ +import AppCore +import Combine import ComposableArchitecture +import Foundation +import XCTestDynamicOverlay +import XXMessengerClient +import XXModels 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, + restoreFailures: [String] = [], + focusedField: Field? = nil, + isImportingFile: Bool = false, + passphrase: String = "", + isRestoring: Bool = false + ) { + self.file = file + self.fileImportFailure = fileImportFailure + self.restoreFailures = restoreFailures + self.focusedField = focusedField + self.isImportingFile = isImportingFile + self.passphrase = passphrase + self.isRestoring = isRestoring + } + + public var file: File? + public var fileImportFailure: String? + public var restoreFailures: [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, + db: DBManagerGetDB, + loadData: URLDataLoader, + now: @escaping () -> Date, + mainQueue: AnySchedulerOf<DispatchQueue>, + bgQueue: AnySchedulerOf<DispatchQueue> + ) { + self.messenger = messenger + self.db = db + self.loadData = loadData + self.now = now + self.mainQueue = mainQueue + self.bgQueue = bgQueue + } + + public var messenger: Messenger + public var db: DBManagerGetDB + public var loadData: URLDataLoader + public var now: () -> Date + public var mainQueue: AnySchedulerOf<DispatchQueue> + public var bgQueue: AnySchedulerOf<DispatchQueue> } +#if DEBUG extension RestoreEnvironment { - public static let unimplemented = RestoreEnvironment() + public static let unimplemented = RestoreEnvironment( + messenger: .unimplemented, + db: .unimplemented, + loadData: .unimplemented, + now: XCTUnimplemented("\(Self.self).now"), + mainQueue: .unimplemented, + bgQueue: .unimplemented + ) } +#endif + +public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironment> +{ state, action, env in + switch action { + case .importFileTapped: + state.isImportingFile = true + state.fileImportFailure = nil + return .none -public let restoreReducer = Reducer<RestoreState, RestoreAction, RestoreEnvironment>.empty + 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.restoreFailures = [] + return Effect.result { + do { + let result = try env.messenger.restoreBackup( + backupData: backupData, + backupPassphrase: backupPassphrase + ) + let facts = try env.messenger.ud.tryGet().getFacts() + try env.db().saveContact(Contact( + id: try env.messenger.e2e.tryGet().getContact().getId(), + username: facts.get(.username)?.value, + email: facts.get(.email)?.value, + phone: facts.get(.phone)?.value, + createdAt: env.now() + )) + try result.restoredContacts.forEach { contactId in + if try env.db().fetchContacts(.init(id: [contactId])).isEmpty { + try env.db().saveContact(Contact( + id: contactId, + createdAt: env.now() + )) + } + } + return .success(.finished) + } catch { + var errors = [error as NSError] + do { + try env.messenger.destroy() + } catch { + errors.append(error as NSError) + } + return .success(.failed(errors)) + } + } + .subscribe(on: env.bgQueue) + .receive(on: env.mainQueue) + .eraseToEffect() + + case .finished: + state.isRestoring = false + return .none + + case .failed(let errors): + state.isRestoring = false + state.restoreFailures = errors.map(\.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..281f3f061e4a3ff5ccc1913bb45e45a03cff6576 100644 --- a/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift +++ b/Examples/xx-messenger/Sources/RestoreFeature/RestoreView.swift @@ -7,40 +7,164 @@ 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 restoreFailures: [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 + restoreFailures = state.restoreFailures + } } public var body: some View { WithViewStore(store, observe: ViewState.init) { viewStore in NavigationView { Form { - Section { - Text("Not implemented") + fileSection(viewStore) + if viewStore.file != nil { + restoreSection(viewStore) } - - Section { + } + .toolbar { + ToolbarItem(placement: .cancellationAction) { Button { viewStore.send(.finished) } label: { - Text("OK") - .frame(maxWidth: .infinity) + Text("Cancel") } + .disabled(viewStore.isImportingFile || viewStore.isRestoring) } } .navigationTitle("Restore") + .onChange(of: viewStore.focusedField) { focusedField = $0 } + .onChange(of: focusedField) { viewStore.send(.set(\.$focusedField, $0)) } } .navigationViewStyle(.stack) } } + + @ViewBuilder func fileSection(_ viewStore: ViewStore<ViewState, RestoreAction>) -> some View { + Section { + if let file = viewStore.file { + HStack(alignment: .bottom) { + Text(file.name) + Spacer() + Text(format(byteCount: file.size)) + } + } else { + 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 })) + } + ) + .disabled(viewStore.isRestoring) + } + } header: { + Text("File") + } + + if let failure = viewStore.fileImportFailure { + Section { + Text(failure) + } header: { + Text("Error") + } + } + } + + @ViewBuilder func restoreSection(_ viewStore: ViewStore<ViewState, RestoreAction>) -> some View { + Section { + SecureField("Passphrase", text: viewStore.binding( + get: \.passphrase, + send: { .set(\.$passphrase, $0) } + )) + .textContentType(.password) + .textInputAutocapitalization(.never) + .disableAutocorrection(true) + .focused($focusedField, equals: .passphrase) + .disabled(viewStore.isRestoring) + + Button { + viewStore.send(.restoreTapped) + } label: { + HStack { + Text("Restore") + Spacer() + if viewStore.isRestoring { + ProgressView() + } + } + } + } header: { + Text("Restore") + } + .disabled(viewStore.isRestoring) + + if !viewStore.restoreFailures.isEmpty { + Section { + ForEach(Array(viewStore.restoreFailures.enumerated()), id: \.offset) { _, failure in + Text(failure) + } + .font(.footnote) + } header: { + Text("Error") + } + } + } + + func format(byteCount: Int) -> String { + let formatter = ByteCountFormatter() + formatter.allowedUnits = [.useMB, .useKB, .useBytes] + formatter.countStyle = .binary + return formatter.string(fromByteCount: Int64(byteCount)) + } } #if DEBUG public struct RestoreView_Previews: PreviewProvider { public static var previews: some View { RestoreView(store: Store( - initialState: RestoreState(), + initialState: RestoreState( + file: .init(name: "preview", data: Data()), + fileImportFailure: nil, + restoreFailures: [ + "Preview failure 1", + "Preview failure 2", + "Preview failure 3", + ], + focusedField: nil, + isImportingFile: false, + passphrase: "", + isRestoring: true + ), reducer: .empty, environment: () )) 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/SendRequestFeature/SendRequestFeature.swift b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift index f2625b91e6a4042624ad68141515ab34ace7c90c..18075179aedc62daff46847052e2bc0071f76b0a 100644 --- a/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift +++ b/Examples/xx-messenger/Sources/SendRequestFeature/SendRequestFeature.swift @@ -1,4 +1,5 @@ import AppCore +import Combine import ComposableArchitecture import Foundation import XCTestDynamicOverlay @@ -40,7 +41,8 @@ public enum SendRequestAction: Equatable, BindableAction { case sendSucceeded case sendFailed(String) case binding(BindingAction<SendRequestState>) - case myContactFetched(XXClient.Contact?) + case myContactFetched(XXClient.Contact) + case myContactFetchFailed(NSError) } public struct SendRequestEnvironment { @@ -75,25 +77,30 @@ extension SendRequestEnvironment { public let sendRequestReducer = Reducer<SendRequestState, SendRequestAction, SendRequestEnvironment> { state, action, env in - enum DBFetchEffectID {} - switch action { case .start: - return Effect - .catching { try env.messenger.e2e.tryGet().getContact().getId() } - .tryMap { try env.db().fetchContactsPublisher(.init(id: [$0])) } - .flatMap { $0 } - .assertNoFailure() - .map(\.first) - .map { $0?.marshaled.map { XXClient.Contact.live($0) } } - .map(SendRequestAction.myContactFetched) - .subscribe(on: env.bgQueue) - .receive(on: env.mainQueue) - .eraseToEffect() - .cancellable(id: DBFetchEffectID.self, cancelInFlight: true) + return Effect.run { subscriber in + do { + let contact = try env.messenger.myContact() + subscriber.send(.myContactFetched(contact)) + } catch { + subscriber.send(.myContactFetchFailed(error as NSError)) + } + subscriber.send(completion: .finished) + return AnyCancellable {} + } + .receive(on: env.mainQueue) + .subscribe(on: env.bgQueue) + .eraseToEffect() case .myContactFetched(let contact): state.myContact = contact + state.failure = nil + return .none + + case .myContactFetchFailed(let failure): + state.myContact = nil + state.failure = failure.localizedDescription return .none case .sendTapped: diff --git a/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeFeature.swift b/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeFeature.swift index 6c6f2e22aa36e73ff0fc4acc441eb63b91f56e66..66e9ef1b3492c1d8277fac263a9d76523769030c 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 { @@ -35,6 +38,7 @@ public struct WelcomeEnvironment { public var bgQueue: AnySchedulerOf<DispatchQueue> } +#if DEBUG extension WelcomeEnvironment { public static let unimplemented = WelcomeEnvironment( messenger: .unimplemented, @@ -42,12 +46,14 @@ extension WelcomeEnvironment { bgQueue: .unimplemented ) } +#endif public let welcomeReducer = Reducer<WelcomeState, WelcomeAction, WelcomeEnvironment> { state, action, env in switch action { case .newAccountTapped: state.isCreatingAccount = true + state.failure = nil return .future { fulfill in do { try env.messenger.create() @@ -66,10 +72,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..64396fe1b6392267dc61efae6a8a73997698cd0c 100644 --- a/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeView.swift +++ b/Examples/xx-messenger/Sources/WelcomeFeature/WelcomeView.swift @@ -1,3 +1,4 @@ +import AppCore import ComposableArchitecture import SwiftUI @@ -11,9 +12,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 { @@ -21,7 +24,17 @@ public struct WelcomeView: View { NavigationView { Form { Section { - Text("xx messenger") + AppVersionText() + } header: { + Text("App version") + } + + if let failure = viewStore.failure { + Section { + Text(failure) + } header: { + Text("Error") + } } Section { diff --git a/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerConfirmTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerConfirmTests.swift index 1a1b5f19573669403a069bd4e3cecd2e11347e73..0f5043ab3b50a18c3b9c50d2615c35c7c159f853 100644 --- a/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerConfirmTests.swift +++ b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerConfirmTests.swift @@ -15,7 +15,7 @@ final class AuthCallbackHandlerConfirmTests: XCTestCase { ) let confirm = AuthCallbackHandlerConfirm.live( db: .init { - var db: Database = .failing + var db: Database = .unimplemented db.fetchContacts.run = { query in didFetchContacts.append(query) return [dbContact] @@ -41,7 +41,7 @@ final class AuthCallbackHandlerConfirmTests: XCTestCase { func testConfirmWhenContactNotInDatabase() throws { let confirm = AuthCallbackHandlerConfirm.live( db: .init { - var db: Database = .failing + var db: Database = .unimplemented db.fetchContacts.run = { _ in [] } return db } diff --git a/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerRequestTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerRequestTests.swift index 3a7cb6fbf2d475eadfdc10aa854938fad247167b..d8e19ce7a8d7441f63f93ec0263468adad1d2443 100644 --- a/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerRequestTests.swift +++ b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerRequestTests.swift @@ -14,7 +14,7 @@ final class AuthCallbackHandlerRequestTests: XCTestCase { let request = AuthCallbackHandlerRequest.live( db: .init { - var db: Database = .failing + var db: Database = .unimplemented db.fetchContacts.run = { query in didFetchContacts.append(query) return [] @@ -54,7 +54,7 @@ final class AuthCallbackHandlerRequestTests: XCTestCase { func testRequestWhenContactInDatabase() throws { let request = AuthCallbackHandlerRequest.live( db: .init { - var db: Database = .failing + var db: Database = .unimplemented db.fetchContacts.run = { _ in [.init(id: "id".data(using: .utf8)!)] } return db }, diff --git a/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerResetTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerResetTests.swift index 9a4407bf758a0c0ba922c154d199b758dea701be..a6273fba250c73de2e0cfe6700879fb5a1f15d58 100644 --- a/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerResetTests.swift +++ b/Examples/xx-messenger/Tests/AppCoreTests/AuthCallbackHandler/AuthCallbackHandlerResetTests.swift @@ -10,12 +10,11 @@ final class AuthCallbackHandlerResetTests: XCTestCase { var didSaveContact: [XXModels.Contact] = [] let dbContact = XXModels.Contact( - id: "id".data(using: .utf8)!, - authStatus: .friend + id: "id".data(using: .utf8)! ) let reset = AuthCallbackHandlerReset.live( db: .init { - var db: Database = .failing + var db: Database = .unimplemented db.fetchContacts.run = { query in didFetchContacts.append(query) return [dbContact] @@ -34,14 +33,14 @@ final class AuthCallbackHandlerResetTests: XCTestCase { XCTAssertNoDifference(didFetchContacts, [.init(id: ["id".data(using: .utf8)!])]) var expectedSavedContact = dbContact - expectedSavedContact.authStatus = .stranger + expectedSavedContact.authStatus = .friend XCTAssertNoDifference(didSaveContact, [expectedSavedContact]) } func testResetWhenContactNotInDatabase() throws { let reset = AuthCallbackHandlerReset.live( db: .init { - var db: Database = .failing + var db: Database = .unimplemented db.fetchContacts.run = { _ in [] } return db } diff --git a/Examples/xx-messenger/Tests/AppCoreTests/MessageListenerHandler/MessageListenerHandlerTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/MessageListenerHandler/MessageListenerHandlerTests.swift index 4038cb0d36019822347e358c1139809d4877f059..eb1aa60f0f5e9b43601ce2ea5f0c759b865520fb 100644 --- a/Examples/xx-messenger/Tests/AppCoreTests/MessageListenerHandler/MessageListenerHandlerTests.swift +++ b/Examples/xx-messenger/Tests/AppCoreTests/MessageListenerHandler/MessageListenerHandlerTests.swift @@ -18,7 +18,7 @@ final class MessageListenerHandlerTests: XCTestCase { } var db: DBManagerGetDB = .unimplemented db.run = { - var db: Database = .failing + var db: Database = .unimplemented db.saveMessage.run = { message in didSaveMessage.append(message) return message diff --git a/Examples/xx-messenger/Tests/AppCoreTests/SendMessage/SendMessageTests.swift b/Examples/xx-messenger/Tests/AppCoreTests/SendMessage/SendMessageTests.swift index 0ff8536f453530bba378b8279686d344f8cace74..ae4e6f9e0a55e8819b21c0a326e95b73a6c50056 100644 --- a/Examples/xx-messenger/Tests/AppCoreTests/SendMessage/SendMessageTests.swift +++ b/Examples/xx-messenger/Tests/AppCoreTests/SendMessage/SendMessageTests.swift @@ -55,7 +55,7 @@ final class SendMessageTests: XCTestCase { } var db: DBManagerGetDB = .unimplemented db.run = { - var db: Database = .failing + var db: Database = .unimplemented db.saveMessage.run = { message in dbDidSaveMessage.append(message) var message = message @@ -238,7 +238,7 @@ final class SendMessageTests: XCTestCase { } var db: DBManagerGetDB = .unimplemented db.run = { - var db: Database = .failing + var db: Database = .unimplemented db.saveMessage.run = { $0 } db.fetchMessages.run = { _ in [] } db.bulkUpdateMessages.run = { _, _ in throw error } diff --git a/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift b/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift index 5a013b28b16126b422ef3d26b19407d0af2bff5c..098a026c2c665c803b9b1ef00d4c6dbbe9250f0a 100644 --- a/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift +++ b/Examples/xx-messenger/Tests/AppFeatureTests/AppFeatureTests.swift @@ -1,77 +1,113 @@ +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.environment.messenger.registerBackupCallback.run = { _ in + actions.append(.didRegisterBackupCallback) + return Cancellable {} + } + actions = [] 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, + .didRegisterBackupCallback, + ]) + + 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.environment.messenger.registerBackupCallback.run = { _ in + actions.append(.didRegisterBackupCallback) + return Cancellable {} + } + actions = [] 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, + .didRegisterBackupCallback, + .didLoadMessenger, + ]) + + store.send(.stop) } func testWelcomeFinished() { + var actions: [Action]! + let store = TestStore( initialState: AppState( screen: .welcome(WelcomeState()) @@ -80,33 +116,48 @@ 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.environment.messenger.registerBackupCallback.run = { _ in + actions.append(.didRegisterBackupCallback) + return Cancellable {} + } + actions = [] 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, + .didRegisterBackupCallback, + .didLoadMessenger, + ]) + + store.send(.stop) } func testRestoreFinished() { + var actions: [Action]! + let store = TestStore( initialState: AppState( screen: .restore(RestoreState()) @@ -115,33 +166,48 @@ 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.environment.messenger.registerBackupCallback.run = { _ in + actions.append(.didRegisterBackupCallback) + return Cancellable {} + } + actions = [] 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, + .didRegisterBackupCallback, + .didLoadMessenger, + ]) + + store.send(.stop) } func testHomeDidDeleteAccount() { + var actions: [Action]! + let store = TestStore( initialState: AppState( screen: .home(HomeState()) @@ -150,25 +216,39 @@ 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.environment.messenger.registerBackupCallback.run = { _ in + actions.append(.didRegisterBackupCallback) + return Cancellable {} + } + actions = [] 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, + .didRegisterBackupCallback, + ]) + + store.send(.stop) } func testWelcomeRestoreTapped() { @@ -186,6 +266,8 @@ final class AppFeatureTests: XCTestCase { } func testWelcomeFailed() { + let failure = "Something went wrong" + let store = TestStore( initialState: AppState( screen: .welcome(WelcomeState()) @@ -194,23 +276,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 } @@ -221,29 +301,177 @@ 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.environment.messenger.registerBackupCallback.run = { _ in + actions.append(.didRegisterBackupCallback) + return Cancellable {} + } + actions = [] store.send(.start) store.receive(.set(\.$screen, .failure(error.localizedDescription))) { $0.screen = .failure(error.localizedDescription) } + + XCTAssertNoDifference(actions, [ + .didStartAuthHandler, + .didStartMessageListener, + .didRegisterBackupCallback, + ]) + + store.send(.stop) + } + + func testStartHandlersAndListeners() { + var actions: [Action]! + var authHandlerOnError: [AuthCallbackHandler.OnError] = [] + var messageListenerOnError: [MessageListenerHandler.OnError] = [] + var backupCallback: [UpdateBackupFunc] = [] + + 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.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 = [] + store.send(.start) { + $0.screen = .loading + } + + store.receive(.set(\.$screen, .home(HomeState()))) { + $0.screen = .home(HomeState()) + } + XCTAssertNoDifference(actions, [ + .didCancelAuthHandler, + .didCancelMessageListener, + .didCancelBackupCallback, + .didStartAuthHandler, + .didStartMessageListener, + .didRegisterBackupCallback, + ]) + + actions = [] + struct AuthError: Error {} + let authError = AuthError() + authHandlerOnError.first?(authError) + + XCTAssertNoDifference(actions, [ + .didLog(.error(authError as NSError)) + ]) + + actions = [] + struct MessageError: Error {} + let messageError = MessageError() + messageListenerOnError.first?(messageError) + + 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, + ]) } } + +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) +} diff --git a/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift b/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..d0c69eb87e2efea29f635fce84656b04d2e35cba --- /dev/null +++ b/Examples/xx-messenger/Tests/BackupFeatureTests/BackupFeatureTests.swift @@ -0,0 +1,416 @@ +import ComposableArchitecture +import XCTest +import XXClient +import XXMessengerClient +@testable import BackupFeature + +final class BackupFeatureTests: XCTestCase { + func testTask() { + var isBackupRunning: [Bool] = [false] + var observers: [UUID: BackupStorage.Observer] = [:] + let storedBackup = BackupStorage.Backup( + date: .init(timeIntervalSince1970: 1), + data: "stored".data(using: .utf8)! + ) + + 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.stored = { + storedBackup + } + store.environment.backupStorage.observe = { + let id = UUID() + observers[id] = $0 + return Cancellable { observers[id] = nil } + } + + store.send(.task) + + XCTAssertNoDifference(observers.count, 1) + + store.receive(.backupUpdated(storedBackup)) { + $0.backup = storedBackup + } + + let observedBackup = BackupStorage.Backup( + date: .init(timeIntervalSince1970: 2), + data: "observed".data(using: .utf8)! + ) + observers.values.forEach { $0(observedBackup) } + + store.receive(.backupUpdated(observedBackup)) { + $0.backup = observedBackup + } + + 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 username = "test-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.myContact.run = { includeFacts in + actions.append(.didGetMyContact(includingFacts: includeFacts)) + var contact = Contact.unimplemented("contact-data".data(using: .utf8)!) + contact.getFactsFromContact.run = { _ in [Fact(type: .username, value: username)] } + return contact + } + store.environment.messenger.startBackup.run = { passphrase, params in + actions.append(.didStartBackup(passphrase: passphrase, params: params)) + } + store.environment.messenger.isBackupRunning.run = { + isBackupRunning.removeFirst() + } + + actions = [] + store.send(.set(\.$focusedField, .passphrase)) { + $0.focusedField = .passphrase + } + store.send(.set(\.$passphrase, passphrase)) { + $0.passphrase = passphrase + } + + XCTAssertNoDifference(actions, []) + + actions = [] + store.send(.startTapped) { + $0.isStarting = true + $0.focusedField = nil + } + + XCTAssertNoDifference(actions, [ + .didGetMyContact( + includingFacts: .types([.username]) + ), + .didStartBackup( + passphrase: passphrase, + params: .init(username: username) + ) + ]) + + store.receive(.didStart(failure: nil)) { + $0.isRunning = true + $0.isStarting = false + $0.passphrase = "" + } + } + + func testStartBackupWithoutContactUsername() { + var isBackupRunning: [Bool] = [false] + + let store = TestStore( + initialState: BackupState( + passphrase: "1234" + ), + reducer: backupReducer, + environment: .unimplemented + ) + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.myContact.run = { _ in + var contact = Contact.unimplemented("contact-data".data(using: .utf8)!) + contact.getFactsFromContact.run = { _ in [] } + return contact + } + store.environment.messenger.isBackupRunning.run = { + isBackupRunning.removeFirst() + } + + store.send(.startTapped) { + $0.isStarting = true + } + + let failure = BackupState.Error.contactUsernameMissing + store.receive(.didStart(failure: failure as NSError)) { + $0.isRunning = false + $0.isStarting = false + $0.alert = .error(failure) + } + } + + func testStartBackupMyContactFailure() { + struct Failure: Error {} + let failure = Failure() + var isBackupRunning: [Bool] = [false] + + let store = TestStore( + initialState: BackupState( + passphrase: "1234" + ), + reducer: backupReducer, + environment: .unimplemented + ) + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.myContact.run = { _ in throw failure } + store.environment.messenger.isBackupRunning.run = { + isBackupRunning.removeFirst() + } + + store.send(.startTapped) { + $0.isStarting = true + } + + store.receive(.didStart(failure: failure as NSError)) { + $0.isRunning = false + $0.isStarting = false + $0.alert = .error(failure) + } + } + + func testStartBackupStartFailure() { + struct Failure: Error {} + let failure = Failure() + var isBackupRunning: [Bool] = [false] + + let store = TestStore( + initialState: BackupState( + passphrase: "1234" + ), + reducer: backupReducer, + environment: .unimplemented + ) + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.myContact.run = { _ in + var contact = Contact.unimplemented("data".data(using: .utf8)!) + contact.getFactsFromContact.run = { _ in [Fact(type: .username, value: "username")] } + return contact + } + store.environment.messenger.startBackup.run = { _, _ in + throw failure + } + store.environment.messenger.isBackupRunning.run = { + isBackupRunning.removeFirst() + } + + 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(.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 didGetMyContact(includingFacts: MessengerMyContact.IncludeFacts?) +} diff --git a/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift b/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift index 1a513af07195076dabb987bee120111411a6c633..7f0633021b525d1e26517db1ffc87ed073fae3d6 100644 --- a/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ChatFeatureTests/ChatFeatureTests.swift @@ -34,7 +34,7 @@ final class ChatFeatureTests: XCTestCase { return e2e } store.environment.db.run = { - var db: Database = .failing + var db: Database = .unimplemented db.fetchMessagesPublisher.run = { query in didFetchMessagesWithQuery.append(query) return messagesPublisher.eraseToAnyPublisher() diff --git a/Examples/xx-messenger/Tests/CheckContactAuthFeatureTests/CheckContactAuthFeatureTests.swift b/Examples/xx-messenger/Tests/CheckContactAuthFeatureTests/CheckContactAuthFeatureTests.swift index 95f5a80773838e9b72d4f8c6e380e0f7ce8799fd..a13645f2f29d6fd2ec3fe87353105cf3adf8df86 100644 --- a/Examples/xx-messenger/Tests/CheckContactAuthFeatureTests/CheckContactAuthFeatureTests.swift +++ b/Examples/xx-messenger/Tests/CheckContactAuthFeatureTests/CheckContactAuthFeatureTests.swift @@ -34,7 +34,7 @@ final class CheckContactAuthFeatureTests: XCTestCase { return e2e } store.environment.db.run = { - var db: Database = .failing + var db: Database = .unimplemented db.bulkUpdateContacts.run = { query, assignments in didBulkUpdateContactsWithQuery.append(query) didBulkUpdateContactsWithAssignments.append(assignments) @@ -86,7 +86,7 @@ final class CheckContactAuthFeatureTests: XCTestCase { return e2e } store.environment.db.run = { - var db: Database = .failing + var db: Database = .unimplemented db.bulkUpdateContacts.run = { query, assignments in didBulkUpdateContactsWithQuery.append(query) didBulkUpdateContactsWithAssignments.append(assignments) diff --git a/Examples/xx-messenger/Tests/ConfirmRequestFeatureTests/ConfirmRequestFeatureTests.swift b/Examples/xx-messenger/Tests/ConfirmRequestFeatureTests/ConfirmRequestFeatureTests.swift index 8dea06e119b266534d4b5c6919023ce69789a762..bc84224a8671219aeb7dbed33f5f8552b3302d59 100644 --- a/Examples/xx-messenger/Tests/ConfirmRequestFeatureTests/ConfirmRequestFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ConfirmRequestFeatureTests/ConfirmRequestFeatureTests.swift @@ -34,7 +34,7 @@ final class ConfirmRequestFeatureTests: XCTestCase { return e2e } store.environment.db.run = { - var db: Database = .failing + var db: Database = .unimplemented db.bulkUpdateContacts.run = { query, assignments in didBulkUpdateContactsWithQuery.append(query) didBulkUpdateContactsWithAssignments.append(assignments) @@ -91,7 +91,7 @@ final class ConfirmRequestFeatureTests: XCTestCase { return e2e } store.environment.db.run = { - var db: Database = .failing + var db: Database = .unimplemented db.bulkUpdateContacts.run = { query, assignments in didBulkUpdateContactsWithQuery.append(query) didBulkUpdateContactsWithAssignments.append(assignments) diff --git a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift index 11bfe8a7743ee597706a25f62a0c8f819aea17a8..eba41b7ab57d1dddfc0827747760a9bce224bdab 100644 --- a/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ContactFeatureTests/ContactFeatureTests.swift @@ -3,7 +3,9 @@ import CheckContactAuthFeature import Combine import ComposableArchitecture import ConfirmRequestFeature +import ContactLookupFeature import CustomDump +import ResetAuthFeature import SendRequestFeature import VerifyContactFeature import XCTest @@ -27,7 +29,7 @@ final class ContactFeatureTests: XCTestCase { store.environment.mainQueue = .immediate store.environment.bgQueue = .immediate store.environment.db.run = { - var db: Database = .failing + var db: Database = .unimplemented db.fetchContactsPublisher.run = { query in dbDidFetchContacts.append(query) return dbContactsPublisher.eraseToAnyPublisher() @@ -80,7 +82,7 @@ final class ContactFeatureTests: XCTestCase { store.environment.mainQueue = .immediate store.environment.bgQueue = .immediate store.environment.db.run = { - var db: Database = .failing + var db: Database = .unimplemented db.saveContact.run = { contact in dbDidSaveContact.append(contact) return contact @@ -99,6 +101,55 @@ final class ContactFeatureTests: XCTestCase { XCTAssertNoDifference(dbDidSaveContact, [expectedSavedContact]) } + func testLookupTapped() { + let contactId = "contact-id".data(using: .utf8)! + let store = TestStore( + initialState: ContactState( + id: contactId + ), + reducer: contactReducer, + environment: .unimplemented + ) + + store.send(.lookupTapped) { + $0.lookup = ContactLookupState(id: contactId) + } + } + + func testLookupDismissed() { + let contactId = "contact-id".data(using: .utf8)! + let store = TestStore( + initialState: ContactState( + id: contactId, + lookup: ContactLookupState(id: contactId) + ), + reducer: contactReducer, + environment: .unimplemented + ) + + store.send(.lookupDismissed) { + $0.lookup = nil + } + } + + func testLookupDidLookup() { + let contactId = "contact-id".data(using: .utf8)! + let contact = Contact.unimplemented("contact-data".data(using: .utf8)!) + let store = TestStore( + initialState: ContactState( + id: contactId, + lookup: ContactLookupState(id: contactId) + ), + reducer: contactReducer, + environment: .unimplemented + ) + + store.send(.lookup(.didLookup(contact))) { + $0.xxContact = contact + $0.lookup = nil + } + } + func testSendRequestWithDBContact() { var dbContact = XXModels.Contact(id: "contact-id".data(using: .utf8)!) dbContact.marshaled = "contact-data".data(using: .utf8)! @@ -244,6 +295,44 @@ final class ContactFeatureTests: XCTestCase { } } + func testResetAuthTapped() { + let contactData = "contact-data".data(using: .utf8)! + let store = TestStore( + initialState: ContactState( + id: Data(), + dbContact: XXModels.Contact( + id: Data(), + marshaled: contactData + ) + ), + reducer: contactReducer, + environment: .unimplemented + ) + + store.send(.resetAuthTapped) { + $0.resetAuth = ResetAuthState( + partner: .unimplemented(contactData) + ) + } + } + + func testResetAuthDismissed() { + let store = TestStore( + initialState: ContactState( + id: Data(), + resetAuth: ResetAuthState( + partner: .unimplemented(Data()) + ) + ), + reducer: contactReducer, + environment: .unimplemented + ) + + store.send(.resetAuthDismissed) { + $0.resetAuth = nil + } + } + func testConfirmRequestTapped() { let contactData = "contact-data".data(using: .utf8)! let store = TestStore( diff --git a/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift b/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..76dde8d07350e240ccf86acdbef175834851b56f --- /dev/null +++ b/Examples/xx-messenger/Tests/ContactLookupFeatureTests/ContactLookupFeatureTests.swift @@ -0,0 +1,60 @@ +import ComposableArchitecture +import XCTest +import XXClient +@testable import ContactLookupFeature + +final class ContactLookupFeatureTests: XCTestCase { + func testLookup() { + let id: Data = "1234".data(using: .utf8)! + var didLookupId: [Data] = [] + let lookedUpContact = Contact.unimplemented("123data".data(using: .utf8)!) + + let store = TestStore( + initialState: ContactLookupState(id: id), + reducer: contactLookupReducer, + environment: .unimplemented + ) + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.lookupContact.run = { id in + didLookupId.append(id) + return lookedUpContact + } + + store.send(.lookupTapped) { + $0.isLookingUp = true + $0.failure = nil + } + + XCTAssertEqual(didLookupId, [id]) + + store.receive(.didLookup(lookedUpContact)) { + $0.isLookingUp = false + $0.failure = nil + } + } + + func testLookupFailure() { + let id: Data = "1234".data(using: .utf8)! + let failure = NSError(domain: "test", code: 0) + + let store = TestStore( + initialState: ContactLookupState(id: id), + reducer: contactLookupReducer, + environment: .unimplemented + ) + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.lookupContact.run = { _ in throw failure } + + store.send(.lookupTapped) { + $0.isLookingUp = true + $0.failure = nil + } + + store.receive(.didFail(failure)) { + $0.isLookingUp = false + $0.failure = failure.localizedDescription + } + } +} diff --git a/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift b/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift index a0c0291ef79865fa6c1f3d1ffeb25235bd520acf..9b3ac0d080ad8e23b8359ab8b3cf073433bed7d8 100644 --- a/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift +++ b/Examples/xx-messenger/Tests/ContactsFeatureTests/ContactsFeatureTests.swift @@ -2,6 +2,7 @@ import Combine import ComposableArchitecture import ContactFeature import CustomDump +import MyContactFeature import XCTest import XXClient import XXMessengerClient @@ -32,7 +33,7 @@ final class ContactsFeatureTests: XCTestCase { return e2e } store.environment.db.run = { - var db: Database = .failing + var db: Database = .unimplemented db.fetchContactsPublisher.run = { query in didFetchContacts.append(query) return contactsPublisher.eraseToAnyPublisher() @@ -94,4 +95,30 @@ final class ContactsFeatureTests: XCTestCase { $0.contact = nil } } + + func testSelectMyContact() { + let store = TestStore( + initialState: ContactsState(), + reducer: contactsReducer, + environment: .unimplemented + ) + + store.send(.myContactSelected) { + $0.myContact = MyContactState() + } + } + + func testDismissMyContact() { + let store = TestStore( + initialState: ContactsState( + myContact: MyContactState() + ), + reducer: contactsReducer, + environment: .unimplemented + ) + + store.send(.myContactDismissed) { + $0.myContact = nil + } + } } diff --git a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift index fc671b1ec7406823bd903d07307c8146e948ed39..3a57414bb46df0058870adb5078aae2dfc41bf7c 100644 --- a/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift +++ b/Examples/xx-messenger/Tests/HomeFeatureTests/HomeFeatureTests.swift @@ -1,6 +1,8 @@ import AppCore +import BackupFeature import ComposableArchitecture import ContactsFeature +import CustomDump import RegisterFeature import UserSearchFeature import XCTest @@ -23,11 +25,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 } @@ -38,15 +39,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() { @@ -60,18 +56,20 @@ final class HomeFeatureTests: XCTestCase { var messengerDidConnect = 0 var messengerDidListenForMessages = 0 var messengerDidLogIn = 0 + var messengerDidResumeBackup = 0 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 } 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 {} } @@ -88,16 +86,13 @@ final class HomeFeatureTests: XCTestCase { XCTAssertNoDifference(messengerDidConnect, 1) XCTAssertNoDifference(messengerDidListenForMessages, 1) XCTAssertNoDifference(messengerDidLogIn, 1) + XCTAssertNoDifference(messengerDidResumeBackup, 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() { @@ -114,13 +109,13 @@ 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 } + store.environment.messenger.isBackupRunning.run = { true } store.environment.messenger.cMix.get = { var cMix: CMix = .unimplemented cMix.addHealthCallback.run = { _ in Cancellable {} } @@ -140,15 +135,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() { @@ -163,21 +154,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() { @@ -192,23 +176,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() { @@ -223,24 +200,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() { @@ -255,25 +226,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() { @@ -385,7 +350,7 @@ final class HomeFeatureTests: XCTestCase { return e2e } store.environment.dbManager.getDB.run = { - var db: Database = .failing + var db: Database = .unimplemented db.fetchContacts.run = { query in dbDidFetchContacts.append(query) return [ @@ -546,85 +511,29 @@ final class HomeFeatureTests: XCTestCase { } } - func testAuthCallbacks() { + func testBackupButtonTapped() { 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(.backupButtonTapped) { + $0.backup = BackupState() } - - 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() { + func testDidDismissBackup() { let store = TestStore( - initialState: HomeState(), + initialState: HomeState( + backup: BackupState() + ), 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(.didDismissBackup) { + $0.backup = nil } - - 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/MyContactFeatureTests/MyContactFeatureTests.swift b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..830e97156363b151fbff21276169d8dfc2095720 --- /dev/null +++ b/Examples/xx-messenger/Tests/MyContactFeatureTests/MyContactFeatureTests.swift @@ -0,0 +1,764 @@ +import Combine +import ComposableArchitecture +import CustomDump +import XCTest +import XXClient +import XXMessengerClient +import XXModels +@testable import MyContactFeature + +final class MyContactFeatureTests: XCTestCase { + func testStart() { + let contactId = "contact-id".data(using: .utf8)! + + let store = TestStore( + initialState: MyContactState(), + reducer: myContactReducer, + environment: .unimplemented + ) + + var dbDidFetchContacts: [XXModels.Contact.Query] = [] + let dbContactsPublisher = PassthroughSubject<[XXModels.Contact], Error>() + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + 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.fetchContactsPublisher.run = { query in + dbDidFetchContacts.append(query) + return dbContactsPublisher.eraseToAnyPublisher() + } + return db + } + + store.send(.start) + + XCTAssertNoDifference(dbDidFetchContacts, [.init(id: [contactId])]) + + dbContactsPublisher.send([]) + + store.receive(.contactFetched(nil)) + + let contact = XXModels.Contact(id: contactId) + dbContactsPublisher.send([contact]) + + store.receive(.contactFetched(contact)) { + $0.contact = contact + } + + dbContactsPublisher.send(completion: .finished) + } + + func testRegisterEmail() { + let email = "test@email.com" + let confirmationID = "123" + + var didSendRegisterFact: [Fact] = [] + + let store = TestStore( + initialState: MyContactState(), + reducer: myContactReducer, + environment: .unimplemented + ) + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.ud.get = { + var ud: UserDiscovery = .unimplemented + ud.sendRegisterFact.run = { fact in + didSendRegisterFact.append(fact) + return confirmationID + } + return ud + } + + store.send(.set(\.$focusedField, .email)) { + $0.focusedField = .email + } + + store.send(.set(\.$email, email)) { + $0.email = email + } + + store.send(.registerEmailTapped) { + $0.focusedField = nil + $0.isRegisteringEmail = true + } + + XCTAssertNoDifference(didSendRegisterFact, [.init(type: .email, value: email)]) + + store.receive(.set(\.$emailConfirmationID, confirmationID)) { + $0.emailConfirmationID = confirmationID + } + + store.receive(.set(\.$isRegisteringEmail, false)) { + $0.isRegisteringEmail = false + } + } + + func testRegisterEmailFailure() { + struct Failure: Error {} + let failure = Failure() + + let store = TestStore( + initialState: MyContactState(), + reducer: myContactReducer, + environment: .unimplemented + ) + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.ud.get = { + var ud: UserDiscovery = .unimplemented + ud.sendRegisterFact.run = { _ in throw failure } + return ud + } + + store.send(.registerEmailTapped) { + $0.isRegisteringEmail = true + } + + store.receive(.didFail(failure.localizedDescription)) { + $0.alert = .error(failure.localizedDescription) + } + + store.receive(.set(\.$isRegisteringEmail, false)) { + $0.isRegisteringEmail = false + } + } + + func testConfirmEmail() { + let contactID = "contact-id".data(using: .utf8)! + let email = "test@email.com" + let confirmationID = "123" + let confirmationCode = "321" + let dbContact = XXModels.Contact(id: contactID) + + var didConfirmWithID: [String] = [] + var didConfirmWithCode: [String] = [] + var didFetchContacts: [XXModels.Contact.Query] = [] + var didSaveContact: [XXModels.Contact] = [] + + let store = TestStore( + initialState: MyContactState( + email: email, + emailConfirmationID: confirmationID + ), + reducer: myContactReducer, + environment: .unimplemented + ) + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.ud.get = { + var ud: UserDiscovery = .unimplemented + ud.confirmFact.run = { id, code in + didConfirmWithID.append(id) + didConfirmWithCode.append(code) + } + return ud + } + 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 = { query in + didFetchContacts.append(query) + return [dbContact] + } + db.saveContact.run = { contact in + didSaveContact.append(contact) + return contact + } + return db + } + + store.send(.set(\.$focusedField, .emailCode)) { + $0.focusedField = .emailCode + } + + store.send(.set(\.$emailConfirmationCode, confirmationCode)) { + $0.emailConfirmationCode = confirmationCode + } + + store.send(.confirmEmailTapped) { + $0.focusedField = nil + $0.isConfirmingEmail = true + } + + XCTAssertNoDifference(didConfirmWithID, [confirmationID]) + XCTAssertNoDifference(didConfirmWithCode, [confirmationCode]) + XCTAssertNoDifference(didFetchContacts, [.init(id: [contactID])]) + var expectedSavedContact = dbContact + expectedSavedContact.email = email + XCTAssertNoDifference(didSaveContact, [expectedSavedContact]) + + store.receive(.set(\.$email, "")) { + $0.email = "" + } + store.receive(.set(\.$emailConfirmationID, nil)) { + $0.emailConfirmationID = nil + } + store.receive(.set(\.$emailConfirmationCode, "")) { + $0.emailConfirmationCode = "" + } + store.receive(.set(\.$isConfirmingEmail, false)) { + $0.isConfirmingEmail = false + } + } + + func testConfirmEmailFailure() { + struct Failure: Error {} + let failure = Failure() + + let store = TestStore( + initialState: MyContactState( + emailConfirmationID: "123" + ), + reducer: myContactReducer, + environment: .unimplemented + ) + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.ud.get = { + var ud: UserDiscovery = .unimplemented + ud.confirmFact.run = { _, _ in throw failure } + return ud + } + + store.send(.confirmEmailTapped) { + $0.isConfirmingEmail = true + } + + store.receive(.didFail(failure.localizedDescription)) { + $0.alert = .error(failure.localizedDescription) + } + + store.receive(.set(\.$isConfirmingEmail, false)) { + $0.isConfirmingEmail = false + } + } + + func testUnregisterEmail() { + let contactID = "contact-id".data(using: .utf8)! + let email = "test@email.com" + let dbContact = XXModels.Contact(id: contactID, email: email) + + var didRemoveFact: [Fact] = [] + var didFetchContacts: [XXModels.Contact.Query] = [] + var didSaveContact: [XXModels.Contact] = [] + + let store = TestStore( + initialState: MyContactState( + contact: dbContact + ), + reducer: myContactReducer, + environment: .unimplemented + ) + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.ud.get = { + var ud: UserDiscovery = .unimplemented + ud.removeFact.run = { didRemoveFact.append($0) } + return ud + } + 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 = { query in + didFetchContacts.append(query) + return [dbContact] + } + db.saveContact.run = { contact in + didSaveContact.append(contact) + return contact + } + return db + } + + store.send(.unregisterEmailTapped) { + $0.isUnregisteringEmail = true + } + + XCTAssertNoDifference(didRemoveFact, [.init(type: .email, value: email)]) + XCTAssertNoDifference(didFetchContacts, [.init(id: [contactID])]) + var expectedSavedContact = dbContact + expectedSavedContact.email = nil + XCTAssertNoDifference(didSaveContact, [expectedSavedContact]) + + store.receive(.set(\.$isUnregisteringEmail, false)) { + $0.isUnregisteringEmail = false + } + } + + func testUnregisterEmailFailure() { + struct Failure: Error {} + let failure = Failure() + + let store = TestStore( + initialState: MyContactState( + contact: .init(id: Data(), email: "test@email.com") + ), + reducer: myContactReducer, + environment: .unimplemented + ) + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.ud.get = { + var ud: UserDiscovery = .unimplemented + ud.removeFact.run = { _ in throw failure } + return ud + } + + store.send(.unregisterEmailTapped) { + $0.isUnregisteringEmail = true + } + + store.receive(.didFail(failure.localizedDescription)) { + $0.alert = .error(failure.localizedDescription) + } + + store.receive(.set(\.$isUnregisteringEmail, false)) { + $0.isUnregisteringEmail = false + } + } + + func testRegisterPhone() { + let phone = "+123456789" + let confirmationID = "123" + + var didSendRegisterFact: [Fact] = [] + + let store = TestStore( + initialState: MyContactState(), + reducer: myContactReducer, + environment: .unimplemented + ) + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.ud.get = { + var ud: UserDiscovery = .unimplemented + ud.sendRegisterFact.run = { fact in + didSendRegisterFact.append(fact) + return confirmationID + } + return ud + } + + store.send(.set(\.$focusedField, .phone)) { + $0.focusedField = .phone + } + + store.send(.set(\.$phone, phone)) { + $0.phone = phone + } + + store.send(.registerPhoneTapped) { + $0.focusedField = nil + $0.isRegisteringPhone = true + } + + XCTAssertNoDifference(didSendRegisterFact, [.init(type: .phone, value: phone)]) + + store.receive(.set(\.$phoneConfirmationID, confirmationID)) { + $0.phoneConfirmationID = confirmationID + } + + store.receive(.set(\.$isRegisteringPhone, false)) { + $0.isRegisteringPhone = false + } + } + + func testRegisterPhoneFailure() { + struct Failure: Error {} + let failure = Failure() + + let store = TestStore( + initialState: MyContactState(), + reducer: myContactReducer, + environment: .unimplemented + ) + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.ud.get = { + var ud: UserDiscovery = .unimplemented + ud.sendRegisterFact.run = { _ in throw failure } + return ud + } + + store.send(.registerPhoneTapped) { + $0.isRegisteringPhone = true + } + + store.receive(.didFail(failure.localizedDescription)) { + $0.alert = .error(failure.localizedDescription) + } + + store.receive(.set(\.$isRegisteringPhone, false)) { + $0.isRegisteringPhone = false + } + } + + func testConfirmPhone() { + let contactID = "contact-id".data(using: .utf8)! + let phone = "+123456789" + let confirmationID = "123" + let confirmationCode = "321" + let dbContact = XXModels.Contact(id: contactID) + + var didConfirmWithID: [String] = [] + var didConfirmWithCode: [String] = [] + var didFetchContacts: [XXModels.Contact.Query] = [] + var didSaveContact: [XXModels.Contact] = [] + + let store = TestStore( + initialState: MyContactState( + phone: phone, + phoneConfirmationID: confirmationID + ), + reducer: myContactReducer, + environment: .unimplemented + ) + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.ud.get = { + var ud: UserDiscovery = .unimplemented + ud.confirmFact.run = { id, code in + didConfirmWithID.append(id) + didConfirmWithCode.append(code) + } + return ud + } + 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 = { query in + didFetchContacts.append(query) + return [dbContact] + } + db.saveContact.run = { contact in + didSaveContact.append(contact) + return contact + } + return db + } + + store.send(.set(\.$focusedField, .phoneCode)) { + $0.focusedField = .phoneCode + } + + store.send(.set(\.$phoneConfirmationCode, confirmationCode)) { + $0.phoneConfirmationCode = confirmationCode + } + + store.send(.confirmPhoneTapped) { + $0.focusedField = nil + $0.isConfirmingPhone = true + } + + XCTAssertNoDifference(didConfirmWithID, [confirmationID]) + XCTAssertNoDifference(didConfirmWithCode, [confirmationCode]) + XCTAssertNoDifference(didFetchContacts, [.init(id: [contactID])]) + var expectedSavedContact = dbContact + expectedSavedContact.phone = phone + XCTAssertNoDifference(didSaveContact, [expectedSavedContact]) + + store.receive(.set(\.$phone, "")) { + $0.phone = "" + } + store.receive(.set(\.$phoneConfirmationID, nil)) { + $0.phoneConfirmationID = nil + } + store.receive(.set(\.$phoneConfirmationCode, "")) { + $0.phoneConfirmationCode = "" + } + store.receive(.set(\.$isConfirmingPhone, false)) { + $0.isConfirmingPhone = false + } + } + + func testConfirmPhoneFailure() { + struct Failure: Error {} + let failure = Failure() + + let store = TestStore( + initialState: MyContactState( + phoneConfirmationID: "123" + ), + reducer: myContactReducer, + environment: .unimplemented + ) + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.ud.get = { + var ud: UserDiscovery = .unimplemented + ud.confirmFact.run = { _, _ in throw failure } + return ud + } + + store.send(.confirmPhoneTapped) { + $0.isConfirmingPhone = true + } + + store.receive(.didFail(failure.localizedDescription)) { + $0.alert = .error(failure.localizedDescription) + } + + store.receive(.set(\.$isConfirmingPhone, false)) { + $0.isConfirmingPhone = false + } + } + + func testUnregisterPhone() { + let contactID = "contact-id".data(using: .utf8)! + let phone = "+123456789" + let dbContact = XXModels.Contact(id: contactID, phone: phone) + + var didRemoveFact: [Fact] = [] + var didFetchContacts: [XXModels.Contact.Query] = [] + var didSaveContact: [XXModels.Contact] = [] + + let store = TestStore( + initialState: MyContactState( + contact: dbContact + ), + reducer: myContactReducer, + environment: .unimplemented + ) + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.ud.get = { + var ud: UserDiscovery = .unimplemented + ud.removeFact.run = { didRemoveFact.append($0) } + return ud + } + 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 = { query in + didFetchContacts.append(query) + return [dbContact] + } + db.saveContact.run = { contact in + didSaveContact.append(contact) + return contact + } + return db + } + + store.send(.unregisterPhoneTapped) { + $0.isUnregisteringPhone = true + } + + XCTAssertNoDifference(didRemoveFact, [.init(type: .phone, value: phone)]) + XCTAssertNoDifference(didFetchContacts, [.init(id: [contactID])]) + var expectedSavedContact = dbContact + expectedSavedContact.phone = nil + XCTAssertNoDifference(didSaveContact, [expectedSavedContact]) + + store.receive(.set(\.$isUnregisteringPhone, false)) { + $0.isUnregisteringPhone = false + } + } + + func testUnregisterPhoneFailure() { + struct Failure: Error {} + let failure = Failure() + + let store = TestStore( + initialState: MyContactState( + contact: .init(id: Data(), phone: "+123456789") + ), + reducer: myContactReducer, + environment: .unimplemented + ) + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.ud.get = { + var ud: UserDiscovery = .unimplemented + ud.removeFact.run = { _ in throw failure } + return ud + } + + store.send(.unregisterPhoneTapped) { + $0.isUnregisteringPhone = true + } + + store.receive(.didFail(failure.localizedDescription)) { + $0.alert = .error(failure.localizedDescription) + } + + store.receive(.set(\.$isUnregisteringPhone, false)) { + $0.isUnregisteringPhone = false + } + } + + func testLoadFactsFromClient() { + let contactId = "contact-id".data(using: .utf8)! + let dbContact = XXModels.Contact(id: contactId) + let username = "user234" + let email = "test@email.com" + let phone = "123456789" + + var didFetchContacts: [XXModels.Contact.Query] = [] + var didSaveContact: [XXModels.Contact] = [] + + let store = TestStore( + initialState: MyContactState(), + reducer: myContactReducer, + environment: .unimplemented + ) + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + 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.messenger.ud.get = { + var ud: UserDiscovery = .unimplemented + ud.getFacts.run = { + [ + Fact(type: .username, value: username), + Fact(type: .email, value: email), + Fact(type: .phone, value: phone), + ] + } + return ud + } + store.environment.db.run = { + var db: Database = .unimplemented + db.fetchContacts.run = { query in + didFetchContacts.append(query) + return [dbContact] + } + db.saveContact.run = { contact in + didSaveContact.append(contact) + return contact + } + return db + } + + store.send(.loadFactsTapped) { + $0.isLoadingFacts = true + } + + XCTAssertNoDifference(didFetchContacts, [.init(id: [contactId])]) + var expectedSavedContact = dbContact + expectedSavedContact.username = username + expectedSavedContact.email = email + expectedSavedContact.phone = phone + XCTAssertNoDifference(didSaveContact, [expectedSavedContact]) + + store.receive(.set(\.$isLoadingFacts, false)) { + $0.isLoadingFacts = false + } + } + + func testLoadFactsFromClientFailure() { + struct Failure: Error {} + let failure = Failure() + + let store = TestStore( + initialState: MyContactState(), + reducer: myContactReducer, + environment: .unimplemented + ) + + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.e2e.get = { + var e2e: E2E = .unimplemented + e2e.getContact.run = { + var contact: XXClient.Contact = .unimplemented(Data()) + contact.getIdFromContact.run = { _ in throw failure } + return contact + } + return e2e + } + + store.send(.loadFactsTapped) { + $0.isLoadingFacts = true + } + + store.receive(.didFail(failure.localizedDescription)) { + $0.alert = .error(failure.localizedDescription) + } + + store.receive(.set(\.$isLoadingFacts, false)) { + $0.isLoadingFacts = false + } + } + + func testErrorAlert() { + let store = TestStore( + initialState: MyContactState(), + reducer: myContactReducer, + environment: .unimplemented + ) + + let failure = "Something went wrong" + + store.send(.didFail(failure)) { + $0.alert = .error(failure) + } + + store.send(.alertDismissed) { + $0.alert = nil + } + } +} diff --git a/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift b/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift index 6c802874d64bbd0dcf7e0ac5b9fee0de7e136d8f..e0ffc5ef4fe9a3bc5f9d9f91d288025cd1065c85 100644 --- a/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift +++ b/Examples/xx-messenger/Tests/RegisterFeatureTests/RegisterFeatureTests.swift @@ -1,4 +1,5 @@ import ComposableArchitecture +import CustomDump import XCTest import XXClient import XXMessengerClient @@ -7,41 +8,43 @@ import XXModels final class RegisterFeatureTests: XCTestCase { func testRegister() throws { + let now = Date() + let username = "registering-username" + let myContactId = "my-contact-id".data(using: .utf8)! + let myContactData = "my-contact-data".data(using: .utf8)! + let myContactUsername = username + let myContactEmail = "my-contact-email" + let myContactPhone = "my-contact-phone" + let myContactFacts = [ + Fact(type: .username, value: myContactUsername), + Fact(type: .email, value: myContactEmail), + Fact(type: .phone, value: myContactPhone), + ] + + var messengerDidRegisterUsername: [String] = [] + var didGetMyContact: [MessengerMyContact.IncludeFacts?] = [] + var dbDidSaveContact: [XXModels.Contact] = [] + let store = TestStore( initialState: RegisterState(), reducer: registerReducer, environment: .unimplemented ) - - let now = Date() - let mainQueue = DispatchQueue.test - let bgQueue = DispatchQueue.test - var didSetFactsOnContact: [[XXClient.Fact]] = [] - var dbDidSaveContact: [XXModels.Contact] = [] - var messengerDidRegisterUsername: [String] = [] - store.environment.now = { now } - store.environment.mainQueue = mainQueue.eraseToAnyScheduler() - store.environment.bgQueue = bgQueue.eraseToAnyScheduler() + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate store.environment.messenger.register.run = { username in messengerDidRegisterUsername.append(username) } - store.environment.messenger.e2e.get = { - var e2e: E2E = .unimplemented - e2e.getContact.run = { - var contact = XXClient.Contact.unimplemented("contact-data".data(using: .utf8)!) - contact.getIdFromContact.run = { _ in "contact-id".data(using: .utf8)! } - contact.getFactsFromContact.run = { _ in [] } - contact.setFactsOnContact.run = { data, facts in - didSetFactsOnContact.append(facts) - return data - } - return contact - } - return e2e + store.environment.messenger.myContact.run = { includeFacts in + didGetMyContact.append(includeFacts) + var contact = XXClient.Contact.unimplemented(myContactData) + contact.getIdFromContact.run = { _ in myContactId } + contact.getFactsFromContact.run = { _ in myContactFacts } + return contact } store.environment.db.run = { - var db: Database = .failing + var db: Database = .unimplemented db.saveContact.run = { contact in dbDidSaveContact.append(contact) return contact @@ -49,33 +52,34 @@ final class RegisterFeatureTests: XCTestCase { return db } - store.send(.set(\.$username, "NewUser")) { - $0.username = "NewUser" + store.send(.set(\.$focusedField, .username)) { + $0.focusedField = .username + } + + store.send(.set(\.$username, myContactUsername)) { + $0.username = myContactUsername } store.send(.registerTapped) { + $0.focusedField = nil $0.isRegistering = true } - XCTAssertNoDifference(messengerDidRegisterUsername, []) - XCTAssertNoDifference(dbDidSaveContact, []) - - bgQueue.advance() - - XCTAssertNoDifference(messengerDidRegisterUsername, ["NewUser"]) - XCTAssertNoDifference(didSetFactsOnContact, [[Fact(type: .username, value: "NewUser")]]) + XCTAssertNoDifference(messengerDidRegisterUsername, [username]) XCTAssertNoDifference(dbDidSaveContact, [ XXModels.Contact( - id: "contact-id".data(using: .utf8)!, - marshaled: "contact-data".data(using: .utf8)!, - username: "NewUser", + id: myContactId, + marshaled: myContactData, + username: myContactUsername, + email: myContactEmail, + phone: myContactPhone, createdAt: now ) ]) - mainQueue.advance() - - store.receive(.finished) + store.receive(.finished) { + $0.isRegistering = false + } } func testGetDbFailure() throws { @@ -123,7 +127,7 @@ final class RegisterFeatureTests: XCTestCase { store.environment.mainQueue = mainQueue.eraseToAnyScheduler() store.environment.bgQueue = bgQueue.eraseToAnyScheduler() - store.environment.db.run = { .failing } + store.environment.db.run = { .unimplemented } store.environment.messenger.register.run = { _ in throw error } store.send(.registerTapped) { @@ -138,4 +142,78 @@ final class RegisterFeatureTests: XCTestCase { $0.failure = error.localizedDescription } } + + func testRegisterUsernameMismatchFailure() throws { + let now = Date() + let username = "registering-username" + let myContactId = "my-contact-id".data(using: .utf8)! + let myContactData = "my-contact-data".data(using: .utf8)! + let myContactUsername = "my-contact-username" + let myContactEmail = "my-contact-email" + let myContactPhone = "my-contact-phone" + let myContactFacts = [ + Fact(type: .username, value: myContactUsername), + Fact(type: .email, value: myContactEmail), + Fact(type: .phone, value: myContactPhone), + ] + + var messengerDidRegisterUsername: [String] = [] + var didGetMyContact: [MessengerMyContact.IncludeFacts?] = [] + var dbDidSaveContact: [XXModels.Contact] = [] + + let store = TestStore( + initialState: RegisterState( + username: username + ), + reducer: registerReducer, + environment: .unimplemented + ) + store.environment.now = { now } + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.register.run = { username in + messengerDidRegisterUsername.append(username) + } + store.environment.messenger.myContact.run = { includeFacts in + didGetMyContact.append(includeFacts) + var contact = XXClient.Contact.unimplemented(myContactData) + contact.getIdFromContact.run = { _ in myContactId } + contact.getFactsFromContact.run = { _ in myContactFacts } + return contact + } + store.environment.db.run = { + var db: Database = .unimplemented + db.saveContact.run = { contact in + dbDidSaveContact.append(contact) + return contact + } + return db + } + + store.send(.registerTapped) { + $0.focusedField = nil + $0.isRegistering = true + } + + XCTAssertNoDifference(messengerDidRegisterUsername, [username]) + XCTAssertNoDifference(dbDidSaveContact, [ + XXModels.Contact( + id: myContactId, + marshaled: myContactData, + username: myContactUsername, + email: myContactEmail, + phone: myContactPhone, + createdAt: now + ) + ]) + + let failure = RegisterState.Error.usernameMismatch( + registering: username, + registered: myContactUsername + ) + store.receive(.failed(failure.localizedDescription)) { + $0.isRegistering = false + $0.failure = failure.localizedDescription + } + } } diff --git a/Examples/xx-messenger/Tests/ResetAuthFeatureTests/ResetAuthFeatureTests.swift b/Examples/xx-messenger/Tests/ResetAuthFeatureTests/ResetAuthFeatureTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..46b5dc9e402d5c2cb71a796790b205732876a658 --- /dev/null +++ b/Examples/xx-messenger/Tests/ResetAuthFeatureTests/ResetAuthFeatureTests.swift @@ -0,0 +1,72 @@ +import ComposableArchitecture +import CustomDump +import XCTest +import XXClient +@testable import ResetAuthFeature + +final class ResetAuthFeatureTests: XCTestCase { + func testReset() { + let partnerData = "contact-data".data(using: .utf8)! + let partner = Contact.unimplemented(partnerData) + + var didResetAuthChannel: [Contact] = [] + + let store = TestStore( + initialState: ResetAuthState( + partner: partner + ), + reducer: resetAuthReducer, + environment: .unimplemented + ) + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.e2e.get = { + var e2e: E2E = .unimplemented + e2e.resetAuthenticatedChannel.run = { contact in + didResetAuthChannel.append(contact) + return 0 + } + return e2e + } + + store.send(.resetTapped) { + $0.isResetting = true + } + + XCTAssertNoDifference(didResetAuthChannel, [partner]) + + store.receive(.didReset) { + $0.isResetting = false + $0.didReset = true + } + } + + func testResetFailure() { + struct Failure: Error {} + let failure = Failure() + + let store = TestStore( + initialState: ResetAuthState( + partner: .unimplemented(Data()) + ), + reducer: resetAuthReducer, + environment: .unimplemented + ) + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.e2e.get = { + var e2e: E2E = .unimplemented + e2e.resetAuthenticatedChannel.run = { _ in throw failure } + return e2e + } + + store.send(.resetTapped) { + $0.isResetting = true + } + + store.receive(.didFail(failure.localizedDescription)) { + $0.isResetting = false + $0.failure = failure.localizedDescription + } + } +} diff --git a/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift b/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift index c3e77edbf64de2d8df6e3cfb518b610055928397..716d7650255b277cad37fb40ad5a73c4aee5bace 100644 --- a/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift +++ b/Examples/xx-messenger/Tests/RestoreFeatureTests/RestoreFeatureTests.swift @@ -1,15 +1,233 @@ import ComposableArchitecture +import CustomDump import XCTest +import XXClient +import XXMessengerClient +import XXModels @testable import RestoreFeature 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 restoredFacts = [ + Fact(type: .username, value: "restored-fact-username"), + Fact(type: .email, value: "restored-fact-email"), + Fact(type: .phone, value: "restored-fact-phone"), + ] + let restoreResult = MessengerRestoreBackup.Result( + restoredParams: BackupParams(username: "restored-param-username"), + restoredContacts: [ + "contact-1-id".data(using: .utf8)!, + "contact-2-id".data(using: .utf8)!, + "contact-3-id".data(using: .utf8)!, + ] + ) + let now = Date() + let contactId = "contact-id".data(using: .utf8)! + + var udFacts: [Fact] = [] + var didRestoreWithData: [Data] = [] + var didRestoreWithPassphrase: [String] = [] + var didFetchContacts: [XXModels.Contact.Query] = [] + var didSaveContact: [XXModels.Contact] = [] + + 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.now = { now } + store.environment.messenger.restoreBackup.run = { data, passphrase in + didRestoreWithData.append(data) + didRestoreWithPassphrase.append(passphrase) + udFacts = restoredFacts + return restoreResult + } + 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.messenger.ud.get = { + var ud: UserDiscovery = .unimplemented + ud.getFacts.run = { udFacts } + return ud + } + store.environment.db.run = { + var db: Database = .unimplemented + db.fetchContacts.run = { query in + didFetchContacts.append(query) + return [] + } + db.saveContact.run = { contact in + didSaveContact.append(contact) + return contact + } + return db + } + + store.send(.set(\.$passphrase, backupPassphrase)) { + $0.passphrase = backupPassphrase + } + + store.send(.restoreTapped) { + $0.isRestoring = true + } + + XCTAssertNoDifference(didRestoreWithData, [backupData]) + XCTAssertNoDifference(didRestoreWithPassphrase, [backupPassphrase]) + XCTAssertNoDifference(didFetchContacts, [ + .init(id: [restoreResult.restoredContacts[0]]), + .init(id: [restoreResult.restoredContacts[1]]), + .init(id: [restoreResult.restoredContacts[2]]), + ]) + XCTAssertNoDifference(didSaveContact, [ + Contact( + id: contactId, + username: restoredFacts.get(.username)?.value, + email: restoredFacts.get(.email)?.value, + phone: restoredFacts.get(.phone)?.value, + createdAt: now + ), + Contact( + id: restoreResult.restoredContacts[0], + createdAt: now + ), + Contact( + id: restoreResult.restoredContacts[1], + createdAt: now + ), + Contact( + id: restoreResult.restoredContacts[2], + createdAt: now + ), + ]) + + store.receive(.finished) { + $0.isRestoring = false + } + } + + func testRestoreWithoutFile() { + let store = TestStore( + initialState: RestoreState( + file: nil + ), + reducer: restoreReducer, + environment: .unimplemented + ) + + store.send(.restoreTapped) + } + + func testRestoreFailure() { + enum Failure: Error { + case restore + case destroy + } + + 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.restore } + store.environment.messenger.destroy.run = { throw Failure.destroy } + + store.send(.restoreTapped) { + $0.isRestoring = true + } + + store.receive(.failed([Failure.restore as NSError, Failure.destroy as NSError])) { + $0.isRestoring = false + $0.restoreFailures = [ + Failure.restore.localizedDescription, + Failure.destroy.localizedDescription, + ] + } } } diff --git a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift index cec3587e845624edc68ef537b00604b8a577c13c..4c68abab06a609346b47b4fe472d5f377417c2f5 100644 --- a/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift +++ b/Examples/xx-messenger/Tests/SendRequestFeatureTests/SendRequestFeatureTests.swift @@ -1,12 +1,18 @@ import Combine import ComposableArchitecture +import CustomDump import XCTest import XXClient +import XXMessengerClient import XXModels @testable import SendRequestFeature final class SendRequestFeatureTests: XCTestCase { func testStart() { + let myContact = XXClient.Contact.unimplemented("my-contact-data".data(using: .utf8)!) + + var didGetMyContact: [MessengerMyContact.IncludeFacts?] = [] + let store = TestStore( initialState: SendRequestState( contact: .unimplemented("contact-data".data(using: .utf8)!) @@ -14,47 +20,41 @@ final class SendRequestFeatureTests: XCTestCase { reducer: sendRequestReducer, environment: .unimplemented ) - - var dbDidFetchContacts: [XXModels.Contact.Query] = [] - let dbContactsPublisher = PassthroughSubject<[XXModels.Contact], Error>() - store.environment.mainQueue = .immediate store.environment.bgQueue = .immediate - store.environment.messenger.e2e.get = { - var e2e: E2E = .unimplemented - e2e.getContact.run = { - var contact: XXClient.Contact = .unimplemented("my-contact-data".data(using: .utf8)!) - contact.getIdFromContact.run = { _ in "my-contact-id".data(using: .utf8)! } - return contact - } - return e2e - } - store.environment.db.run = { - var db: Database = .failing - db.fetchContactsPublisher.run = { query in - dbDidFetchContacts.append(query) - return dbContactsPublisher.eraseToAnyPublisher() - } - return db + store.environment.messenger.myContact.run = { includeFacts in + didGetMyContact.append(includeFacts) + return myContact } store.send(.start) - XCTAssertNoDifference(dbDidFetchContacts, [.init(id: ["my-contact-id".data(using: .utf8)!])]) + store.receive(.myContactFetched(myContact)) { + $0.myContact = myContact + } + } - dbContactsPublisher.send([]) + func testMyContactFailure() { + struct Failure: Error {} + let failure = Failure() - store.receive(.myContactFetched(nil)) + let store = TestStore( + initialState: SendRequestState( + contact: .unimplemented("contact-data".data(using: .utf8)!) + ), + reducer: sendRequestReducer, + environment: .unimplemented + ) + store.environment.mainQueue = .immediate + store.environment.bgQueue = .immediate + store.environment.messenger.myContact.run = { _ in throw failure } - var myDbContact = XXModels.Contact(id: "my-contact-id".data(using: .utf8)!) - myDbContact.marshaled = "my-contact-data".data(using: .utf8)! - dbContactsPublisher.send([myDbContact]) + store.send(.start) - store.receive(.myContactFetched(.live("my-contact-data".data(using: .utf8)!))) { - $0.myContact = .live("my-contact-data".data(using: .utf8)!) + store.receive(.myContactFetchFailed(failure as NSError)) { + $0.myContact = nil + $0.failure = failure.localizedDescription } - - dbContactsPublisher.send(completion: .finished) } func testSendRequest() { @@ -93,7 +93,7 @@ final class SendRequestFeatureTests: XCTestCase { store.environment.mainQueue = .immediate store.environment.bgQueue = .immediate store.environment.db.run = { - var db: Database = .failing + var db: Database = .unimplemented db.bulkUpdateContacts.run = { query, assignments in didBulkUpdateContacts.append(.init(query: query, assignments: assignments)) return 0 @@ -163,7 +163,7 @@ final class SendRequestFeatureTests: XCTestCase { store.environment.mainQueue = .immediate store.environment.bgQueue = .immediate store.environment.db.run = { - var db: Database = .failing + var db: Database = .unimplemented db.bulkUpdateContacts.run = { _, _ in return 0 } return db } diff --git a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift index 44731c1f16d41e0b95ab3ff1675f654d1ab2fcd5..33f1edb9d61d7186503b63f90c1f73f11251185f 100644 --- a/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift +++ b/Examples/xx-messenger/Tests/UserSearchFeatureTests/UserSearchFeatureTests.swift @@ -1,5 +1,6 @@ import ComposableArchitecture import ContactFeature +import CustomDump import XCTest import XXClient import XXMessengerClient @@ -64,6 +65,8 @@ final class UserSearchFeatureTests: XCTestCase { $0.failure = nil } + XCTAssertNoDifference(didSearchWithQuery, [.init(username: "Username")]) + store.receive(.didSucceed(contacts)) { $0.isSearching = false $0.failure = nil diff --git a/Examples/xx-messenger/Tests/VerifyContactFeatureTests/VerifyContactFeatureTests.swift b/Examples/xx-messenger/Tests/VerifyContactFeatureTests/VerifyContactFeatureTests.swift index 97f8b428e53ffc63ae123bf9f8510594cc0f5daf..ceaa61cc3f215234aea66381b4c7dc17965bb1a4 100644 --- a/Examples/xx-messenger/Tests/VerifyContactFeatureTests/VerifyContactFeatureTests.swift +++ b/Examples/xx-messenger/Tests/VerifyContactFeatureTests/VerifyContactFeatureTests.swift @@ -30,7 +30,7 @@ final class VerifyContactFeatureTests: XCTestCase { return true } store.environment.db.run = { - var db: Database = .failing + var db: Database = .unimplemented db.bulkUpdateContacts.run = { query, assignments in didBulkUpdateContactsWithQuery.append(query) didBulkUpdateContactsWithAssignments.append(assignments) @@ -84,7 +84,7 @@ final class VerifyContactFeatureTests: XCTestCase { return false } store.environment.db.run = { - var db: Database = .failing + var db: Database = .unimplemented db.bulkUpdateContacts.run = { query, assignments in didBulkUpdateContactsWithQuery.append(query) didBulkUpdateContactsWithAssignments.append(assignments) @@ -137,7 +137,7 @@ final class VerifyContactFeatureTests: XCTestCase { store.environment.bgQueue = .immediate store.environment.messenger.verifyContact.run = { _ in throw error } store.environment.db.run = { - var db: Database = .failing + var db: Database = .unimplemented db.bulkUpdateContacts.run = { query, assignments in didBulkUpdateContactsWithQuery.append(query) didBulkUpdateContactsWithAssignments.append(assignments) 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/Examples/xx-messenger/XXMessenger.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/xx-messenger/XXMessenger.xcworkspace/xcshareddata/swiftpm/Package.resolved index 45de0ce85881cbcd30f418b3ac71b0983257de07..51664bd603e976bab27b158f1ef45ac937dd18b7 100644 --- a/Examples/xx-messenger/XXMessenger.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/xx-messenger/XXMessenger.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://git.xx.network/elixxir/client-ios-db.git", "state" : { - "revision" : "f8e3e0088de8301d6c4816e12f0aca1d6f02a280", - "version" : "1.1.0" + "revision" : "16480177beaa6bc80a1c6be25812abd9bcb850ea", + "version" : "1.2.0" } }, { @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/groue/GRDB.swift", "state" : { - "revision" : "dd7e7f39e8e4d7a22d258d9809a882f914690b01", - "version" : "5.26.1" + "revision" : "0ac435744a4c67c4ec23a4a671c0d53ce1fee7c6", + "version" : "6.0.0" } }, { @@ -36,6 +36,15 @@ "version" : "4.2.2" } }, + { + "identity" : "pulse", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kean/Pulse.git", + "state" : { + "revision" : "6b682c529d98a38e6fdffee2a8bfa40c8de30821", + "version" : "2.1.3" + } + }, { "identity" : "swift-case-paths", "kind" : "remoteSourceControl", @@ -59,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-composable-architecture.git", "state" : { - "revision" : "cbe013b42b3c368957f8f882c960b93845e1589d", - "version" : "0.40.1" + "revision" : "9ea8c763061287052a68d5e6723fed45e898b7d9", + "version" : "0.40.2" } }, { @@ -75,10 +84,10 @@ { "identity" : "swift-custom-dump", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-custom-dump", + "location" : "https://github.com/pointfreeco/swift-custom-dump.git", "state" : { - "revision" : "21ec1d717c07cea5a026979cb0471dd95c7087e7", - "version" : "0.5.0" + "revision" : "819d9d370cd721c9d87671e29d947279292e4541", + "version" : "0.6.0" } }, { @@ -90,13 +99,22 @@ "version" : "0.4.1" } }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "6fe203dc33195667ce1759bf0182975e4653ba1c", + "version" : "1.4.4" + } + }, { "identity" : "xctest-dynamic-overlay", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay.git", "state" : { - "revision" : "38bc9242e4388b80bd23ddfdf3071428859e3260", - "version" : "0.4.0" + "revision" : "30314f1ece684dd60679d598a9b89107557b67d9", + "version" : "0.4.1" } } ], diff --git a/Examples/xx-messenger/bump-build-number.sh b/Examples/xx-messenger/bump-build-number.sh new file mode 100755 index 0000000000000000000000000000000000000000..f95dceaca1c95063eed05e6aa32388216376a0d1 --- /dev/null +++ b/Examples/xx-messenger/bump-build-number.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +script_dir=$(dirname $(realpath $0)) +project_dir="$script_dir/Project" +repo_dir="$script_dir/../../" + +if [ -n "$(git -C $repo_dir status --porcelain)" ]; then + echo "Repository has uncommitted changes!" + exit 1 +fi + +cd $project_dir +xcrun agvtool next-version +build_number=$(xcrun agvtool what-version -terse) + +cd $repo_dir +git commit -a -m "Bump xx-messenger example app build number to $build_number" diff --git a/Frameworks/Bindings.txt b/Frameworks/Bindings.txt index 9fba83df0bda30fec264e321e1a66f76665e3d7c..a5431f62c1ebe21d09d1dc4f609978ce97f7ee3d 100644 --- a/Frameworks/Bindings.txt +++ b/Frameworks/Bindings.txt @@ -1,4 +1,4 @@ -https://git.xx.network/elixxir/client/-/commit/7aac8e09168fc8fb33ace2263e8ee576724cf70f +https://git.xx.network/elixxir/client/-/commit/730d7cd8e9333143cc9fd94de8611861450c9f82 go version go1.17.13 darwin/arm64 -Xcode 14.0 Build version 14A309 +Xcode 14.0.1 Build version 14A400 gomobile bind target: ios,iossimulator,macos diff --git a/Frameworks/Bindings.xcframework/Info.plist b/Frameworks/Bindings.xcframework/Info.plist index 3d81013ba0ba606da4b42b6f3119f688c6d9b97d..e66824e5243985b4ac053ef62aeaaf571fe03970 100644 --- a/Frameworks/Bindings.xcframework/Info.plist +++ b/Frameworks/Bindings.xcframework/Info.plist @@ -21,28 +21,28 @@ </dict> <dict> <key>LibraryIdentifier</key> - <string>macos-arm64_x86_64</string> + <string>ios-arm64</string> <key>LibraryPath</key> <string>Bindings.framework</string> <key>SupportedArchitectures</key> <array> <string>arm64</string> - <string>x86_64</string> </array> <key>SupportedPlatform</key> - <string>macos</string> + <string>ios</string> </dict> <dict> <key>LibraryIdentifier</key> - <string>ios-arm64</string> + <string>macos-arm64_x86_64</string> <key>LibraryPath</key> <string>Bindings.framework</string> <key>SupportedArchitectures</key> <array> <string>arm64</string> + <string>x86_64</string> </array> <key>SupportedPlatform</key> - <string>ios</string> + <string>macos</string> </dict> </array> <key>CFBundlePackageType</key> diff --git a/Frameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Bindings b/Frameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Bindings index fa210455a0a975408dbd63e78456ac6f9a8a4b70..11bc65b9458a47c5168c044c5c2e9aeb5079521a 100644 Binary files a/Frameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Bindings and b/Frameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Bindings differ diff --git a/Frameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/Bindings.objc.h b/Frameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/Bindings.objc.h index 96523f8825cf04317f17fab6d169b33c1e82c90c..73defbbf6e76c0043035efc5c5ec165747ff20cc 100644 --- a/Frameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/Bindings.objc.h +++ b/Frameworks/Bindings.xcframework/ios-arm64/Bindings.framework/Versions/A/Headers/Bindings.objc.h @@ -132,7 +132,7 @@ Parameters: @end @protocol BindingsGroupChatProcessor <NSObject> -- (void)process:(NSData* _Nullable)decryptedMessage msg:(NSData* _Nullable)msg receptionId:(NSData* _Nullable)receptionId ephemeralId:(int64_t)ephemeralId roundId:(int64_t)roundId err:(NSError* _Nullable)err; +- (void)process:(NSData* _Nullable)decryptedMessage msg:(NSData* _Nullable)msg receptionId:(NSData* _Nullable)receptionId ephemeralId:(int64_t)ephemeralId roundId:(int64_t)roundId roundUrl:(NSString* _Nullable)roundUrl err:(NSError* _Nullable)err; - (NSString* _Nonnull)string; @end @@ -2272,29 +2272,24 @@ FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewOrLoadUd(long e2eI /** * NewUdManagerFromBackup builds a new user discover manager from a backup. It -will construct a manager that is already registered and restore already -registered facts into store. +will construct a manager that is already registered. Confirmed facts have +already been restored via the call NewCmixFromBackup. Parameters: - e2eID - e2e object ID in the tracker - follower - network follower func wrapped in UdNetworkStatus - - username - The username this user registered with initially. This should - not be nullable, and be JSON marshalled as retrieved from - UserDiscovery.GetFacts(). - - emailFactJson - nullable JSON marshalled email [fact.Fact] - - phoneFactJson - nullable JSON marshalled phone [fact.Fact] - cert - the TLS certificate for the UD server this call will connect with. You may use the UD server run by the xx network team by using - E2e.GetUdCertFromNdf. - - contactFile - the data within a marshalled contact.Contact. This + [E2e.GetUdCertFromNdf]. + - contactFile - the data within a marshalled [contact.Contact]. This represents the contact file of the server this call will connect with. You may use the UD server run by the xx network team by using - E2e.GetUdContactFromNdf. + [E2e.GetUdContactFromNdf]. - address - the IP address of the UD server this call will connect with. You may use the UD server run by the xx network team by using - E2e.GetUdAddressFromNdf. + [E2e.GetUdAddressFromNdf]. */ -FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUdManagerFromBackup(long e2eID, id<BindingsUdNetworkStatus> _Nullable follower, NSData* _Nullable usernameJson, NSData* _Nullable emailFactJson, NSData* _Nullable phoneFactJson, NSData* _Nullable cert, NSData* _Nullable contactFile, NSString* _Nullable address, NSError* _Nullable* _Nullable error); +FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUdManagerFromBackup(long e2eID, id<BindingsUdNetworkStatus> _Nullable follower, NSData* _Nullable cert, NSData* _Nullable contactFile, NSString* _Nullable address, NSError* _Nullable* _Nullable error); /** * RegisterForNotifications allows a client to register for push notifications. @@ -2598,7 +2593,7 @@ The decryptedMessage field will be a JSON marshalled GroupChatMessage. @property(strong, readonly) _Nonnull id _ref; - (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)process:(NSData* _Nullable)decryptedMessage msg:(NSData* _Nullable)msg receptionId:(NSData* _Nullable)receptionId ephemeralId:(int64_t)ephemeralId roundId:(int64_t)roundId err:(NSError* _Nullable)err; +- (void)process:(NSData* _Nullable)decryptedMessage msg:(NSData* _Nullable)msg receptionId:(NSData* _Nullable)receptionId ephemeralId:(int64_t)ephemeralId roundId:(int64_t)roundId roundUrl:(NSString* _Nullable)roundUrl err:(NSError* _Nullable)err; - (NSString* _Nonnull)string; @end diff --git a/Frameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Bindings b/Frameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Bindings index 5dd1a70d80a7f20c4ec801fa2c17eee9606724e1..2514d4ed68f5c18e8285457d59f2f1c7c47bd313 100644 Binary files a/Frameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Bindings and b/Frameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Bindings differ diff --git a/Frameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/Bindings.objc.h b/Frameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/Bindings.objc.h index 96523f8825cf04317f17fab6d169b33c1e82c90c..73defbbf6e76c0043035efc5c5ec165747ff20cc 100644 --- a/Frameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/Bindings.objc.h +++ b/Frameworks/Bindings.xcframework/ios-arm64_x86_64-simulator/Bindings.framework/Versions/A/Headers/Bindings.objc.h @@ -132,7 +132,7 @@ Parameters: @end @protocol BindingsGroupChatProcessor <NSObject> -- (void)process:(NSData* _Nullable)decryptedMessage msg:(NSData* _Nullable)msg receptionId:(NSData* _Nullable)receptionId ephemeralId:(int64_t)ephemeralId roundId:(int64_t)roundId err:(NSError* _Nullable)err; +- (void)process:(NSData* _Nullable)decryptedMessage msg:(NSData* _Nullable)msg receptionId:(NSData* _Nullable)receptionId ephemeralId:(int64_t)ephemeralId roundId:(int64_t)roundId roundUrl:(NSString* _Nullable)roundUrl err:(NSError* _Nullable)err; - (NSString* _Nonnull)string; @end @@ -2272,29 +2272,24 @@ FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewOrLoadUd(long e2eI /** * NewUdManagerFromBackup builds a new user discover manager from a backup. It -will construct a manager that is already registered and restore already -registered facts into store. +will construct a manager that is already registered. Confirmed facts have +already been restored via the call NewCmixFromBackup. Parameters: - e2eID - e2e object ID in the tracker - follower - network follower func wrapped in UdNetworkStatus - - username - The username this user registered with initially. This should - not be nullable, and be JSON marshalled as retrieved from - UserDiscovery.GetFacts(). - - emailFactJson - nullable JSON marshalled email [fact.Fact] - - phoneFactJson - nullable JSON marshalled phone [fact.Fact] - cert - the TLS certificate for the UD server this call will connect with. You may use the UD server run by the xx network team by using - E2e.GetUdCertFromNdf. - - contactFile - the data within a marshalled contact.Contact. This + [E2e.GetUdCertFromNdf]. + - contactFile - the data within a marshalled [contact.Contact]. This represents the contact file of the server this call will connect with. You may use the UD server run by the xx network team by using - E2e.GetUdContactFromNdf. + [E2e.GetUdContactFromNdf]. - address - the IP address of the UD server this call will connect with. You may use the UD server run by the xx network team by using - E2e.GetUdAddressFromNdf. + [E2e.GetUdAddressFromNdf]. */ -FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUdManagerFromBackup(long e2eID, id<BindingsUdNetworkStatus> _Nullable follower, NSData* _Nullable usernameJson, NSData* _Nullable emailFactJson, NSData* _Nullable phoneFactJson, NSData* _Nullable cert, NSData* _Nullable contactFile, NSString* _Nullable address, NSError* _Nullable* _Nullable error); +FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUdManagerFromBackup(long e2eID, id<BindingsUdNetworkStatus> _Nullable follower, NSData* _Nullable cert, NSData* _Nullable contactFile, NSString* _Nullable address, NSError* _Nullable* _Nullable error); /** * RegisterForNotifications allows a client to register for push notifications. @@ -2598,7 +2593,7 @@ The decryptedMessage field will be a JSON marshalled GroupChatMessage. @property(strong, readonly) _Nonnull id _ref; - (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)process:(NSData* _Nullable)decryptedMessage msg:(NSData* _Nullable)msg receptionId:(NSData* _Nullable)receptionId ephemeralId:(int64_t)ephemeralId roundId:(int64_t)roundId err:(NSError* _Nullable)err; +- (void)process:(NSData* _Nullable)decryptedMessage msg:(NSData* _Nullable)msg receptionId:(NSData* _Nullable)receptionId ephemeralId:(int64_t)ephemeralId roundId:(int64_t)roundId roundUrl:(NSString* _Nullable)roundUrl err:(NSError* _Nullable)err; - (NSString* _Nonnull)string; @end diff --git a/Frameworks/Bindings.xcframework/macos-arm64_x86_64/Bindings.framework/Versions/A/Bindings b/Frameworks/Bindings.xcframework/macos-arm64_x86_64/Bindings.framework/Versions/A/Bindings index 0f812dc51a542998d225b841fc3e6c7af42710e0..821b8c1854604a15ed47e44709e5cffae8108b75 100644 Binary files a/Frameworks/Bindings.xcframework/macos-arm64_x86_64/Bindings.framework/Versions/A/Bindings and b/Frameworks/Bindings.xcframework/macos-arm64_x86_64/Bindings.framework/Versions/A/Bindings differ diff --git a/Frameworks/Bindings.xcframework/macos-arm64_x86_64/Bindings.framework/Versions/A/Headers/Bindings.objc.h b/Frameworks/Bindings.xcframework/macos-arm64_x86_64/Bindings.framework/Versions/A/Headers/Bindings.objc.h index 96523f8825cf04317f17fab6d169b33c1e82c90c..73defbbf6e76c0043035efc5c5ec165747ff20cc 100644 --- a/Frameworks/Bindings.xcframework/macos-arm64_x86_64/Bindings.framework/Versions/A/Headers/Bindings.objc.h +++ b/Frameworks/Bindings.xcframework/macos-arm64_x86_64/Bindings.framework/Versions/A/Headers/Bindings.objc.h @@ -132,7 +132,7 @@ Parameters: @end @protocol BindingsGroupChatProcessor <NSObject> -- (void)process:(NSData* _Nullable)decryptedMessage msg:(NSData* _Nullable)msg receptionId:(NSData* _Nullable)receptionId ephemeralId:(int64_t)ephemeralId roundId:(int64_t)roundId err:(NSError* _Nullable)err; +- (void)process:(NSData* _Nullable)decryptedMessage msg:(NSData* _Nullable)msg receptionId:(NSData* _Nullable)receptionId ephemeralId:(int64_t)ephemeralId roundId:(int64_t)roundId roundUrl:(NSString* _Nullable)roundUrl err:(NSError* _Nullable)err; - (NSString* _Nonnull)string; @end @@ -2272,29 +2272,24 @@ FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewOrLoadUd(long e2eI /** * NewUdManagerFromBackup builds a new user discover manager from a backup. It -will construct a manager that is already registered and restore already -registered facts into store. +will construct a manager that is already registered. Confirmed facts have +already been restored via the call NewCmixFromBackup. Parameters: - e2eID - e2e object ID in the tracker - follower - network follower func wrapped in UdNetworkStatus - - username - The username this user registered with initially. This should - not be nullable, and be JSON marshalled as retrieved from - UserDiscovery.GetFacts(). - - emailFactJson - nullable JSON marshalled email [fact.Fact] - - phoneFactJson - nullable JSON marshalled phone [fact.Fact] - cert - the TLS certificate for the UD server this call will connect with. You may use the UD server run by the xx network team by using - E2e.GetUdCertFromNdf. - - contactFile - the data within a marshalled contact.Contact. This + [E2e.GetUdCertFromNdf]. + - contactFile - the data within a marshalled [contact.Contact]. This represents the contact file of the server this call will connect with. You may use the UD server run by the xx network team by using - E2e.GetUdContactFromNdf. + [E2e.GetUdContactFromNdf]. - address - the IP address of the UD server this call will connect with. You may use the UD server run by the xx network team by using - E2e.GetUdAddressFromNdf. + [E2e.GetUdAddressFromNdf]. */ -FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUdManagerFromBackup(long e2eID, id<BindingsUdNetworkStatus> _Nullable follower, NSData* _Nullable usernameJson, NSData* _Nullable emailFactJson, NSData* _Nullable phoneFactJson, NSData* _Nullable cert, NSData* _Nullable contactFile, NSString* _Nullable address, NSError* _Nullable* _Nullable error); +FOUNDATION_EXPORT BindingsUserDiscovery* _Nullable BindingsNewUdManagerFromBackup(long e2eID, id<BindingsUdNetworkStatus> _Nullable follower, NSData* _Nullable cert, NSData* _Nullable contactFile, NSString* _Nullable address, NSError* _Nullable* _Nullable error); /** * RegisterForNotifications allows a client to register for push notifications. @@ -2598,7 +2593,7 @@ The decryptedMessage field will be a JSON marshalled GroupChatMessage. @property(strong, readonly) _Nonnull id _ref; - (nonnull instancetype)initWithRef:(_Nonnull id)ref; -- (void)process:(NSData* _Nullable)decryptedMessage msg:(NSData* _Nullable)msg receptionId:(NSData* _Nullable)receptionId ephemeralId:(int64_t)ephemeralId roundId:(int64_t)roundId err:(NSError* _Nullable)err; +- (void)process:(NSData* _Nullable)decryptedMessage msg:(NSData* _Nullable)msg receptionId:(NSData* _Nullable)receptionId ephemeralId:(int64_t)ephemeralId roundId:(int64_t)roundId roundUrl:(NSString* _Nullable)roundUrl err:(NSError* _Nullable)err; - (NSString* _Nonnull)string; @end diff --git a/Package.resolved b/Package.resolved index 6741fe9d93c193203c994ec7bd191103eecea29b..eb44c7c2804b6d4b64f6ad27cdde3a8f42b6710b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -14,8 +14,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-custom-dump.git", "state" : { - "revision" : "21ec1d717c07cea5a026979cb0471dd95c7087e7", - "version" : "0.5.0" + "revision" : "819d9d370cd721c9d87671e29d947279292e4541", + "version" : "0.6.0" + } + }, + { + "identity" : "swift-log", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-log.git", + "state" : { + "revision" : "6fe203dc33195667ce1759bf0182975e4653ba1c", + "version" : "1.4.4" } }, { @@ -23,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay.git", "state" : { - "revision" : "38bc9242e4388b80bd23ddfdf3071428859e3260", - "version" : "0.4.0" + "revision" : "30314f1ece684dd60679d598a9b89107557b67d9", + "version" : "0.4.1" } } ], diff --git a/Package.swift b/Package.swift index 30e5c604ce0c444f011056ca371ffb100c704351..cd25ca17a71b46a3f2dfe1d01374d552fb429475 100644 --- a/Package.swift +++ b/Package.swift @@ -1,15 +1,10 @@ -// swift-tools-version: 5.6 - +// swift-tools-version: 5.7 import PackageDescription let swiftSettings: [SwiftSetting] = [ - .unsafeFlags( - [ - "-Xfrontend", "-debug-time-function-bodies", - "-Xfrontend", "-debug-time-expression-type-checking", - ], - .when(configuration: .debug) - ), + //.unsafeFlags(["-Xfrontend", "-warn-concurrency"], .when(configuration: .debug)), + //.unsafeFlags(["-Xfrontend", "-debug-time-function-bodies"], .when(configuration: .debug)), + //.unsafeFlags(["-Xfrontend", "-debug-time-expression-type-checking"], .when(configuration: .debug)), ] let package = Package( @@ -26,16 +21,20 @@ let package = Package( dependencies: [ .package( url: "https://github.com/pointfreeco/swift-custom-dump.git", - .upToNextMajor(from: "0.5.0") + .upToNextMajor(from: "0.5.2") ), .package( url: "https://github.com/pointfreeco/xctest-dynamic-overlay.git", - .upToNextMajor(from: "0.4.0") + .upToNextMajor(from: "0.4.1") ), .package( url: "https://github.com/kishikawakatsumi/KeychainAccess.git", .upToNextMajor(from: "4.2.2") ), + .package( + url: "https://github.com/apple/swift-log.git", + .upToNextMajor(from: "1.4.4") + ), ], targets: [ .target( @@ -60,6 +59,7 @@ let package = Package( dependencies: [ .target(name: "XXClient"), .product(name: "KeychainAccess", package: "KeychainAccess"), + .product(name: "Logging", package: "swift-log"), .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), ], swiftSettings: swiftSettings diff --git a/README.md b/README.md index 83f708d886b695c5fbd58e9f8176699991fa4d97..7b468762ffd317e3c4fe0c12a72e268402932109 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Elixxir dApps Swift SDK - +  ## 📖 Documentation @@ -14,7 +14,7 @@ Check out included [examples](Examples). ## 🛠Development -Open `Package.swift` in Xcode (≥13.4.1). +Open `Package.swift` in Xcode (≥14). ### Project structure diff --git a/Sources/XXClient/Callbacks/GroupChatProcessor.swift b/Sources/XXClient/Callbacks/GroupChatProcessor.swift index e6ab9ee4d8535a1fb8048e6713dc99d4730e512b..f303aae49186ab75df7a796f735e129027367ec6 100644 --- a/Sources/XXClient/Callbacks/GroupChatProcessor.swift +++ b/Sources/XXClient/Callbacks/GroupChatProcessor.swift @@ -8,13 +8,15 @@ public struct GroupChatProcessor { msg: Data, receptionId: Data, ephemeralId: Int64, - roundId: Int64 + roundId: Int64, + roundUrl: String ) { self.decryptedMessage = decryptedMessage self.msg = msg self.receptionId = receptionId self.ephemeralId = ephemeralId self.roundId = roundId + self.roundUrl = roundUrl } public var decryptedMessage: GroupChatMessage @@ -22,6 +24,7 @@ public struct GroupChatProcessor { public var receptionId: Data public var ephemeralId: Int64 public var roundId: Int64 + public var roundUrl: String } public init( @@ -58,28 +61,33 @@ extension GroupChatProcessor { receptionId: Data?, ephemeralId: Int64, roundId: Int64, + roundUrl: String?, err: Error? ) { if let err = err { callback.handle(.failure(err as NSError)) return } - guard let decryptedMessage = decryptedMessage else { + guard let decryptedMessage else { fatalError("BindingsGroupChatProcessor received `nil` decryptedMessage") } - guard let msg = msg else { + guard let msg else { fatalError("BindingsGroupChatProcessor received `nil` msg") } - guard let receptionId = receptionId else { + guard let receptionId else { fatalError("BindingsGroupChatProcessor received `nil` receptionId") } + guard let roundUrl else { + fatalError("BindingsGroupChatProcessor received `nil` roundUrl") + } do { callback.handle(.success(.init( decryptedMessage: try GroupChatMessage.decode(decryptedMessage), msg: msg, receptionId: receptionId, ephemeralId: ephemeralId, - roundId: roundId + roundId: roundId, + roundUrl: roundUrl ))) } catch { callback.handle(.failure(error as NSError)) diff --git a/Sources/XXClient/Callbacks/UdMultiLookupCallback.swift b/Sources/XXClient/Callbacks/UdMultiLookupCallback.swift index 1782c01ef83d2a74feef73f490e422f201c67c9a..cc40b60695912e2b5f76663d214a9592f854f248 100644 --- a/Sources/XXClient/Callbacks/UdMultiLookupCallback.swift +++ b/Sources/XXClient/Callbacks/UdMultiLookupCallback.swift @@ -49,21 +49,21 @@ extension UdMultiLookupCallback { if let err = err { result.errors.append(err as NSError) } - if let contactListJSON = contactListJSON { - do { - result.contacts = try JSONDecoder() - .decode([Data].self, from: contactListJSON) - .map { Contact.live($0) } - } catch { - result.errors.append(error as NSError) + do { + if let data = contactListJSON, + let contactListJSON = try JSONDecoder().decode([Data]?.self, from: data) { + result.contacts = contactListJSON.map { Contact.live($0) } } + } catch { + result.errors.append(error as NSError) } - if let failedIDs = failedIDs { - do { - result.failedIds = try JSONDecoder().decode([Data].self, from: failedIDs) - } catch { - result.errors.append(error as NSError) + do { + if let data = failedIDs, + let failedIDs = try JSONDecoder().decode([Data]?.self, from: data) { + result.failedIds = failedIDs } + } catch { + result.errors.append(error as NSError) } callback.handle(result) } diff --git a/Sources/XXClient/E2E/E2E.swift b/Sources/XXClient/E2E/E2E.swift index 608ccc6e3703e8818fff89705d5e822fae758916..7dea50476dee3b44b1d3172c243327991b19eff6 100644 --- a/Sources/XXClient/E2E/E2E.swift +++ b/Sources/XXClient/E2E/E2E.swift @@ -10,6 +10,7 @@ public struct E2E { public var getUdAddressFromNdf: E2EGetUdAddressFromNdf public var getUdCertFromNdf: E2EGetUdCertFromNdf public var getUdContactFromNdf: E2EGetUdContactFromNdf + public var getUdEnvironmentFromNdf: E2EGetUdEnvironmentFromNdf public var payloadSize: E2EPayloadSize public var partitionSize: E2EPartitionSize public var addPartnerCallback: E2EAddPartnerCallback @@ -40,6 +41,7 @@ extension E2E { getUdAddressFromNdf: .live(bindingsE2E), getUdCertFromNdf: .live(bindingsE2E), getUdContactFromNdf: .live(bindingsE2E), + getUdEnvironmentFromNdf: .live(bindingsE2E), payloadSize: .live(bindingsE2E), partitionSize: .live(bindingsE2E), addPartnerCallback: .live(bindingsE2E), @@ -71,6 +73,7 @@ extension E2E { getUdAddressFromNdf: .unimplemented, getUdCertFromNdf: .unimplemented, getUdContactFromNdf: .unimplemented, + getUdEnvironmentFromNdf: .unimplemented, payloadSize: .unimplemented, partitionSize: .unimplemented, addPartnerCallback: .unimplemented, diff --git a/Sources/XXClient/E2E/Functions/E2EGetUdEnvironmentFromNdf.swift b/Sources/XXClient/E2E/Functions/E2EGetUdEnvironmentFromNdf.swift new file mode 100644 index 0000000000000000000000000000000000000000..547a1b632187fbb94ddf7ea0196092d6fd8f1c53 --- /dev/null +++ b/Sources/XXClient/E2E/Functions/E2EGetUdEnvironmentFromNdf.swift @@ -0,0 +1,28 @@ +import Bindings +import XCTestDynamicOverlay + +public struct E2EGetUdEnvironmentFromNdf { + public var run: () throws -> UDEnvironment + + public func callAsFunction() throws -> UDEnvironment { + try run() + } +} + +extension E2EGetUdEnvironmentFromNdf { + public static func live(_ bindingsE2E: BindingsE2e) -> E2EGetUdEnvironmentFromNdf { + E2EGetUdEnvironmentFromNdf { + UDEnvironment( + address: E2EGetUdAddressFromNdf.live(bindingsE2E)(), + cert: E2EGetUdCertFromNdf.live(bindingsE2E)(), + contact: try E2EGetUdContactFromNdf.live(bindingsE2E)() + ) + } + } +} + +extension E2EGetUdEnvironmentFromNdf { + public static let unimplemented = E2EGetUdEnvironmentFromNdf( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/XXClient/Functions/GetFileTransferParams.swift b/Sources/XXClient/Functions/GetFileTransferParams.swift index 515a3ca17934d316e90c334a211dbb15c68be1f6..f42924d10f0955ad0972b07754db8debc6412051 100644 --- a/Sources/XXClient/Functions/GetFileTransferParams.swift +++ b/Sources/XXClient/Functions/GetFileTransferParams.swift @@ -23,4 +23,3 @@ extension GetFileTransferParams { run: XCTUnimplemented("\(Self.self)", placeholder: "unimplemented".data(using: .utf8)!) ) } - diff --git a/Sources/XXClient/Functions/NewOrLoadUd.swift b/Sources/XXClient/Functions/NewOrLoadUd.swift index f0a69b0271c1253044db9cddbe8480b35ca6e3dd..35d26b68306b0f1519bc8c57dbab6414b3d5e2f8 100644 --- a/Sources/XXClient/Functions/NewOrLoadUd.swift +++ b/Sources/XXClient/Functions/NewOrLoadUd.swift @@ -3,6 +3,20 @@ import XCTestDynamicOverlay public struct NewOrLoadUd { public struct Params: Equatable { + public init( + e2eId: Int, + username: String?, + registrationValidationSignature: Data?, + environment: UDEnvironment + ) { + self.e2eId = e2eId + self.username = username + self.registrationValidationSignature = registrationValidationSignature + self.cert = environment.cert + self.contact = environment.contact + self.address = environment.address + } + public init( e2eId: Int, username: String?, diff --git a/Sources/XXClient/Functions/NewUdManagerFromBackup.swift b/Sources/XXClient/Functions/NewUdManagerFromBackup.swift index 14f2b0e494c64ee5a7adaeffe9d3fb1c8d7ee699..3f94037abbb4c143943e76aff57f5e67f1f3c4ce 100644 --- a/Sources/XXClient/Functions/NewUdManagerFromBackup.swift +++ b/Sources/XXClient/Functions/NewUdManagerFromBackup.swift @@ -5,26 +5,27 @@ public struct NewUdManagerFromBackup { public struct Params: Equatable { public init( e2eId: Int, - username: Fact, - email: Fact?, - phone: Fact?, + environment: UDEnvironment + ) { + self.e2eId = e2eId + self.cert = environment.cert + self.contact = environment.contact + self.address = environment.address + } + + public init( + e2eId: Int, cert: Data, contact: Data, address: String ) { self.e2eId = e2eId - self.username = username - self.email = email - self.phone = phone self.cert = cert self.contact = contact self.address = address } public var e2eId: Int - public var username: Fact - public var email: Fact? - public var phone: Fact? public var cert: Data public var contact: Data public var address: String @@ -46,9 +47,6 @@ extension NewUdManagerFromBackup { let bindingsUD = BindingsNewUdManagerFromBackup( params.e2eId, follower.makeBindingsUdNetworkStatus(), - try params.username.encode(), - try params.email?.encode(), - try params.phone?.encode(), params.cert, params.contact, params.address, diff --git a/Sources/XXClient/Models/FactType.swift b/Sources/XXClient/Models/FactType.swift index cbae1a7ebdeaf510440dfcffb631848f3fbe4353..2d55ff855cf7fd25046d489bf7ac14fb10ff6f8a 100644 --- a/Sources/XXClient/Models/FactType.swift +++ b/Sources/XXClient/Models/FactType.swift @@ -1,6 +1,6 @@ import Foundation -public enum FactType: Equatable { +public enum FactType: Equatable, Hashable { public static let knownTypes: [FactType] = [.username, .email, .phone] case username diff --git a/Sources/XXClient/Models/UDEnvironment.swift b/Sources/XXClient/Models/UDEnvironment.swift new file mode 100644 index 0000000000000000000000000000000000000000..3f8a02d3ea4950caa8724a0f434e365ce61486d5 --- /dev/null +++ b/Sources/XXClient/Models/UDEnvironment.swift @@ -0,0 +1,17 @@ +import Foundation + +public struct UDEnvironment: Equatable, Codable { + public init( + address: String, + cert: Data, + contact: Data + ) { + self.address = address + self.cert = cert + self.contact = contact + } + + public var address: String + public var cert: Data + public var contact: Data +} diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerBackupParams.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerBackupParams.swift new file mode 100644 index 0000000000000000000000000000000000000000..f5ac71f04d82b27fd3585acfbfe89b4372034ac2 --- /dev/null +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerBackupParams.swift @@ -0,0 +1,33 @@ +import Bindings +import XCTestDynamicOverlay + +public struct MessengerBackupParams { + public enum Error: Swift.Error, Equatable { + case notRunning + } + + public var run: (BackupParams) throws -> Void + + public func callAsFunction(_ params: BackupParams) throws { + try run(params) + } +} + +extension MessengerBackupParams { + public static func live(_ env: MessengerEnvironment) -> MessengerBackupParams { + MessengerBackupParams { params in + guard let backup = env.backup(), backup.isRunning() else { + throw Error.notRunning + } + let paramsData = try params.encode() + let paramsString = String(data: paramsData, encoding: .utf8)! + backup.addJSON(paramsString) + } + } +} + +extension MessengerBackupParams { + public static let unimplemented = MessengerBackupParams( + run: XCTUnimplemented("\(Self.self)") + ) +} 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/MessengerCreate.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerCreate.swift index ed9d193d3592a10dd19a5a5d68d81576cb422f71..bee7f47245c6ebaabd1f615b63881abd6437fddc 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 65a718dffa7c33c246de7a8b371c749433f2898b..89c472ce7c10a142cd49b559ff149af11997d09b 100644 --- a/Sources/XXMessengerClient/Messenger/Functions/MessengerDestroy.swift +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerDestroy.swift @@ -22,7 +22,8 @@ extension MessengerDestroy { env.ud.set(nil) env.e2e.set(nil) env.cMix.set(nil) - try env.fileManager.removeDirectory(env.storageDir) + env.isListeningForMessages.set(false) + try env.fileManager.removeItem(env.storageDir) try env.passwordStorage.remove() } } diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerIsBackupRunning.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerIsBackupRunning.swift new file mode 100644 index 0000000000000000000000000000000000000000..4300ec19524ecb06596cdc836fa44f5257460140 --- /dev/null +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerIsBackupRunning.swift @@ -0,0 +1,24 @@ +import XCTestDynamicOverlay +import XXClient + +public struct MessengerIsBackupRunning { + public var run: () -> Bool + + public func callAsFunction() -> Bool { + run() + } +} + +extension MessengerIsBackupRunning { + public static func live(_ env: MessengerEnvironment) -> MessengerIsBackupRunning { + MessengerIsBackupRunning { + env.backup()?.isRunning() == true + } + } +} + +extension MessengerIsBackupRunning { + public static let unimplemented = MessengerIsBackupRunning( + run: XCTUnimplemented("\(Self.self)", placeholder: false) + ) +} 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/MessengerLogIn.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerLogIn.swift index b82e808a3fc8e9bb3daf97bd07df9fbc5093efe8..70cfa3ce3483a3879b4a02bd3ee6d88983716fe5 100644 --- a/Sources/XXMessengerClient/Messenger/Functions/MessengerLogIn.swift +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerLogIn.swift @@ -28,9 +28,7 @@ extension MessengerLogIn { e2eId: e2e.getId(), username: nil, registrationValidationSignature: nil, - cert: env.udCert ?? e2e.getUdCertFromNdf(), - contact: env.udContact ?? (try e2e.getUdContactFromNdf()), - address: env.udAddress ?? e2e.getUdAddressFromNdf() + environment: env.udEnvironment ?? (try e2e.getUdEnvironmentFromNdf()) ), follower: .init { cMix.networkFollowerStatus() diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerMyContact.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerMyContact.swift new file mode 100644 index 0000000000000000000000000000000000000000..bcedd164fb985724f90f743ce5d417180f39b067 --- /dev/null +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerMyContact.swift @@ -0,0 +1,51 @@ +import XCTestDynamicOverlay +import XXClient + +public struct MessengerMyContact { + public enum IncludeFacts: Equatable { + case all + case types(Set<FactType>) + } + + public enum Error: Swift.Error { + case notConnected + case notLoggedIn + } + + public var run: (IncludeFacts?) throws -> XXClient.Contact + + public func callAsFunction(includeFacts: IncludeFacts? = .all) throws -> XXClient.Contact { + try run(includeFacts) + } +} + +extension MessengerMyContact { + public static func live(_ env: MessengerEnvironment) -> MessengerMyContact { + MessengerMyContact { includeFacts in + guard let e2e = env.e2e() else { + throw Error.notConnected + } + var contact = e2e.getContact() + if let includeFacts { + guard let ud = env.ud() else { + throw Error.notLoggedIn + } + let udFacts = try ud.getFacts() + switch includeFacts { + case .all: + try contact.setFacts(udFacts) + + case .types(let types): + try contact.setFacts(udFacts.filter { types.contains($0.type) }) + } + } + return contact + } + } +} + +extension MessengerMyContact { + public static let unimplemented = MessengerMyContact( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerRegister.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerRegister.swift index 6baad0eb3875b59035e007d92394c9db9e6e589e..4ac83890bdcdef424f1f4292ad6796ff271cce12 100644 --- a/Sources/XXMessengerClient/Messenger/Functions/MessengerRegister.swift +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerRegister.swift @@ -30,9 +30,7 @@ extension MessengerRegister { e2eId: e2e.getId(), username: username, registrationValidationSignature: cMix.getReceptionRegistrationValidationSignature(), - cert: env.udCert ?? e2e.getUdCertFromNdf(), - contact: env.udContact ?? (try e2e.getUdContactFromNdf()), - address: env.udAddress ?? e2e.getUdAddressFromNdf() + environment: env.udEnvironment ?? (try e2e.getUdEnvironmentFromNdf()) ), follower: .init { cMix.networkFollowerStatus() diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerRegisterBackupCallback.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerRegisterBackupCallback.swift new file mode 100644 index 0000000000000000000000000000000000000000..f7c9a251072b04f28f7c12f527e42be7174dce15 --- /dev/null +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerRegisterBackupCallback.swift @@ -0,0 +1,24 @@ +import XCTestDynamicOverlay +import XXClient + +public struct MessengerRegisterBackupCallback { + public var run: (UpdateBackupFunc) -> Cancellable + + public func callAsFunction(_ callback: UpdateBackupFunc) -> Cancellable { + run(callback) + } +} + +extension MessengerRegisterBackupCallback { + public static func live(_ env: MessengerEnvironment) -> MessengerRegisterBackupCallback { + MessengerRegisterBackupCallback { callback in + env.backupCallbacks.register(callback) + } + } +} + +extension MessengerRegisterBackupCallback { + public static let unimplemented = MessengerRegisterBackupCallback( + run: XCTUnimplemented("\(Self.self)", placeholder: Cancellable {}) + ) +} diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerRestoreBackup.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerRestoreBackup.swift new file mode 100644 index 0000000000000000000000000000000000000000..0e5387a3add343ae79d27aed5eaecf752f9fb6fc --- /dev/null +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerRestoreBackup.swift @@ -0,0 +1,82 @@ +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 + let ndfData = try env.downloadNDF(env.ndfEnvironment) + let password = env.generateSecret() + try env.passwordStorage.save(password) + try env.fileManager.removeItem(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 paramsData = report.params.data(using: .utf8)! + let params = try BackupParams.decode(paramsData) + let cMix = try env.loadCMix( + storageDir: storageDir, + password: password, + cMixParamsJSON: env.getCMixParams() + ) + env.cMix.set(cMix) + 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() + ) + env.e2e.set(e2e) + env.isListeningForMessages.set(false) + let ud = try env.newUdManagerFromBackup( + params: NewUdManagerFromBackup.Params( + e2eId: e2e.getId(), + environment: env.udEnvironment ?? (try e2e.getUdEnvironmentFromNdf()) + ), + follower: UdNetworkStatus { cMix.networkFollowerStatus() } + ) + env.ud.set(ud) + return Result( + restoredParams: params, + restoredContacts: report.restoredContacts + ) + } + } +} + +extension MessengerRestoreBackup { + public static let unimplemented = MessengerRestoreBackup( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerResumeBackup.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerResumeBackup.swift new file mode 100644 index 0000000000000000000000000000000000000000..9c4d62a09fe35d22c4afc7c1d6c7fed50d5b1951 --- /dev/null +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerResumeBackup.swift @@ -0,0 +1,44 @@ +import Bindings +import XCTestDynamicOverlay + +public struct MessengerResumeBackup { + public enum Error: Swift.Error, Equatable { + case isRunning + case notConnected + case notLoggedIn + } + + public var run: () throws -> Void + + public func callAsFunction() throws { + try run() + } +} + +extension MessengerResumeBackup { + public static func live(_ env: MessengerEnvironment) -> MessengerResumeBackup { + MessengerResumeBackup { + guard env.backup()?.isRunning() != true else { + throw Error.isRunning + } + guard let e2e = env.e2e() else { + throw Error.notConnected + } + guard let ud = env.ud() else { + throw Error.notLoggedIn + } + let backup = try env.resumeBackup( + e2eId: e2e.getId(), + udId: ud.getId(), + callback: env.backupCallbacks.registered() + ) + env.backup.set(backup) + } + } +} + +extension MessengerResumeBackup { + public static let unimplemented = MessengerResumeBackup( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerSetLogLevel.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerSetLogLevel.swift new file mode 100644 index 0000000000000000000000000000000000000000..21e39bd78035438955587c816602956313389fa4 --- /dev/null +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerSetLogLevel.swift @@ -0,0 +1,22 @@ +import XCTestDynamicOverlay +import XXClient + +public struct MessengerSetLogLevel { + public var run: (LogLevel) throws -> Bool + + public func callAsFunction(_ logLevel: LogLevel) throws -> Bool { + try run(logLevel) + } +} + +extension MessengerSetLogLevel { + public static func live(_ env: MessengerEnvironment) -> MessengerSetLogLevel { + MessengerSetLogLevel(run: env.setLogLevel.run) + } +} + +extension MessengerSetLogLevel { + public static let unimplemented = MessengerSetLogLevel( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerStartBackup.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerStartBackup.swift new file mode 100644 index 0000000000000000000000000000000000000000..1512d36100b25a17b2b75175520edf94940ef5a2 --- /dev/null +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerStartBackup.swift @@ -0,0 +1,65 @@ +import Foundation +import XCTestDynamicOverlay +import XXClient + +public struct MessengerStartBackup { + public enum Error: Swift.Error, Equatable { + case isRunning + case notConnected + case notLoggedIn + } + + public var run: (String, BackupParams) throws -> Void + + public func callAsFunction( + password: String, + params: BackupParams + ) throws { + try run(password, params) + } +} + +extension MessengerStartBackup { + public static func live(_ env: MessengerEnvironment) -> MessengerStartBackup { + MessengerStartBackup { password, params in + guard env.backup()?.isRunning() != true else { + throw Error.isRunning + } + guard let e2e = env.e2e() else { + throw Error.notConnected + } + guard let ud = env.ud() else { + throw Error.notLoggedIn + } + let paramsData = try params.encode() + let paramsString = String(data: paramsData, encoding: .utf8)! + var didAddParams = false + 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 { + if let backup = env.backup() { + backup.addJSON(paramsString) + didAddParams = true + } + } else { + env.backupCallbacks.registered().handle(data) + } + } + ) + env.backup.set(backup) + semaphore?.signal() + semaphore = nil + } + } +} + +extension MessengerStartBackup { + public static let unimplemented = MessengerStartBackup( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerStartLogging.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerStartLogging.swift new file mode 100644 index 0000000000000000000000000000000000000000..dd4d12b095f3a1df1ed1fe3b52f10b15e9c1d352 --- /dev/null +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerStartLogging.swift @@ -0,0 +1,28 @@ +import Foundation +import Logging +import XCTestDynamicOverlay +import XXClient + +public struct MessengerStartLogging { + public var run: () -> Void + + public func callAsFunction() -> Void { + run() + } +} + +extension MessengerStartLogging { + public static func live(_ env: MessengerEnvironment) -> MessengerStartLogging { + return MessengerStartLogging { + env.registerLogWriter(.init { messageString in + env.logger(.parse(messageString)) + }) + } + } +} + +extension MessengerStartLogging { + public static let unimplemented = MessengerStartLogging( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerStop.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerStop.swift new file mode 100644 index 0000000000000000000000000000000000000000..6e49054fbf45c7feaced41449712da617680d9e9 --- /dev/null +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerStop.swift @@ -0,0 +1,60 @@ +import Foundation +import XCTestDynamicOverlay +import XXClient + +public struct MessengerStop { + public struct Wait: Equatable { + public init( + sleepInterval: TimeInterval = 1, + retries: Int = 10 + ) { + self.sleepInterval = sleepInterval + self.retries = retries + } + + public var sleepInterval: TimeInterval + public var retries: Int + } + + public enum Error: Swift.Error { + case notLoaded + case timedOut + } + + public var run: (Wait?) throws -> Void + + public func callAsFunction(wait: Wait? = nil) throws -> Void { + try run(wait) + } +} + +extension MessengerStop { + public static func live(_ env: MessengerEnvironment) -> MessengerStop { + MessengerStop { wait in + guard let cMix = env.cMix() else { + throw Error.notLoaded + } + guard cMix.networkFollowerStatus() == .running else { + return + } + try cMix.stopNetworkFollower() + guard let wait else { return } + var retries = wait.retries + var hasRunningProcesses = cMix.hasRunningProcesses() + while retries > 0 && hasRunningProcesses { + env.sleep(wait.sleepInterval) + hasRunningProcesses = cMix.hasRunningProcesses() + retries -= 1 + } + if hasRunningProcesses { + throw Error.timedOut + } + } + } +} + +extension MessengerStop { + public static let unimplemented = MessengerStop( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/XXMessengerClient/Messenger/Functions/MessengerStopBackup.swift b/Sources/XXMessengerClient/Messenger/Functions/MessengerStopBackup.swift new file mode 100644 index 0000000000000000000000000000000000000000..f5471166652f0d03488f70c74bf3a21aed8df6f8 --- /dev/null +++ b/Sources/XXMessengerClient/Messenger/Functions/MessengerStopBackup.swift @@ -0,0 +1,26 @@ +import Bindings +import XCTestDynamicOverlay + +public struct MessengerStopBackup { + public var run: () throws -> Void + + public func callAsFunction() throws { + try run() + } +} + +extension MessengerStopBackup { + public static func live(_ env: MessengerEnvironment) -> MessengerStopBackup { + MessengerStopBackup { + guard let backup = env.backup() else { return } + try backup.stop() + env.backup.set(nil) + } + } +} + +extension MessengerStopBackup { + public static let unimplemented = MessengerStopBackup( + run: XCTUnimplemented("\(Self.self)") + ) +} diff --git a/Sources/XXMessengerClient/Messenger/Messenger.swift b/Sources/XXMessengerClient/Messenger/Messenger.swift index 77f7f1e52f85af6d083f3b26916d8432643ab6a4..22f110303674337006b77b2f1de436a09d0392b1 100644 --- a/Sources/XXMessengerClient/Messenger/Messenger.swift +++ b/Sources/XXMessengerClient/Messenger/Messenger.swift @@ -4,20 +4,25 @@ public struct Messenger { public var cMix: Stored<CMix?> public var e2e: Stored<E2E?> public var ud: Stored<UserDiscovery?> + public var backup: Stored<Backup?> public var isCreated: MessengerIsCreated public var create: MessengerCreate + public var restoreBackup: MessengerRestoreBackup public var isLoaded: MessengerIsLoaded public var load: MessengerLoad public var registerAuthCallbacks: MessengerRegisterAuthCallbacks public var registerMessageListener: MessengerRegisterMessageListener public var start: MessengerStart + public var stop: MessengerStop public var isConnected: MessengerIsConnected public var connect: MessengerConnect + public var isListeningForMessages: MessengerIsListeningForMessages public var listenForMessages: MessengerListenForMessages public var isRegistered: MessengerIsRegistered public var register: MessengerRegister public var isLoggedIn: MessengerIsLoggedIn public var logIn: MessengerLogIn + public var myContact: MessengerMyContact public var waitForNetwork: MessengerWaitForNetwork public var waitForNodes: MessengerWaitForNodes public var destroy: MessengerDestroy @@ -27,6 +32,14 @@ public struct Messenger { public var registerForNotifications: MessengerRegisterForNotifications public var verifyContact: MessengerVerifyContact public var sendMessage: MessengerSendMessage + public var registerBackupCallback: MessengerRegisterBackupCallback + public var isBackupRunning: MessengerIsBackupRunning + public var startBackup: MessengerStartBackup + public var resumeBackup: MessengerResumeBackup + public var backupParams: MessengerBackupParams + public var stopBackup: MessengerStopBackup + public var setLogLevel: MessengerSetLogLevel + public var startLogging: MessengerStartLogging } extension Messenger { @@ -35,20 +48,25 @@ extension Messenger { cMix: env.cMix, e2e: env.e2e, ud: env.ud, + backup: env.backup, isCreated: .live(env), create: .live(env), + restoreBackup: .live(env), isLoaded: .live(env), load: .live(env), registerAuthCallbacks: .live(env), registerMessageListener: .live(env), start: .live(env), + stop: .live(env), isConnected: .live(env), connect: .live(env), + isListeningForMessages: .live(env), listenForMessages: .live(env), isRegistered: .live(env), register: .live(env), isLoggedIn: .live(env), logIn: .live(env), + myContact: .live(env), waitForNetwork: .live(env), waitForNodes: .live(env), destroy: .live(env), @@ -57,7 +75,15 @@ extension Messenger { lookupContacts: .live(env), registerForNotifications: .live(env), verifyContact: .live(env), - sendMessage: .live(env) + sendMessage: .live(env), + registerBackupCallback: .live(env), + isBackupRunning: .live(env), + startBackup: .live(env), + resumeBackup: .live(env), + backupParams: .live(env), + stopBackup: .live(env), + setLogLevel: .live(env), + startLogging: .live(env) ) } } @@ -67,20 +93,25 @@ extension Messenger { cMix: .unimplemented(), e2e: .unimplemented(), ud: .unimplemented(), + backup: .unimplemented(), isCreated: .unimplemented, create: .unimplemented, + restoreBackup: .unimplemented, isLoaded: .unimplemented, load: .unimplemented, registerAuthCallbacks: .unimplemented, registerMessageListener: .unimplemented, start: .unimplemented, + stop: .unimplemented, isConnected: .unimplemented, connect: .unimplemented, + isListeningForMessages: .unimplemented, listenForMessages: .unimplemented, isRegistered: .unimplemented, register: .unimplemented, isLoggedIn: .unimplemented, logIn: .unimplemented, + myContact: .unimplemented, waitForNetwork: .unimplemented, waitForNodes: .unimplemented, destroy: .unimplemented, @@ -89,6 +120,14 @@ extension Messenger { lookupContacts: .unimplemented, registerForNotifications: .unimplemented, verifyContact: .unimplemented, - sendMessage: .unimplemented + sendMessage: .unimplemented, + registerBackupCallback: .unimplemented, + isBackupRunning: .unimplemented, + startBackup: .unimplemented, + resumeBackup: .unimplemented, + backupParams: .unimplemented, + stopBackup: .unimplemented, + setLogLevel: .unimplemented, + startLogging: .unimplemented ) } diff --git a/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift b/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift index 4c1368061cde90c8e9acb5192302dcc18c3d7bd2..0bd1212cebf2bf1d373d1733d2506e1c52b3d098 100644 --- a/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift +++ b/Sources/XXMessengerClient/Messenger/MessengerEnvironment.swift @@ -4,6 +4,8 @@ import XCTestDynamicOverlay public struct MessengerEnvironment { public var authCallbacks: AuthCallbacksRegistry + public var backup: Stored<Backup?> + public var backupCallbacks: BackupCallbacksRegistry public var cMix: Stored<CMix?> public var downloadNDF: DownloadAndVerifySignedNdf public var e2e: Stored<E2E?> @@ -15,24 +17,30 @@ public struct MessengerEnvironment { public var getFileTransferParams: GetFileTransferParams public var getSingleUseParams: GetSingleUseParams public var initFileTransfer: InitFileTransfer + public var initializeBackup: InitializeBackup + public var isListeningForMessages: Stored<Bool> public var isRegisteredWithUD: IsRegisteredWithUD public var loadCMix: LoadCMix + public var logger: MessengerLogger public var login: Login public var lookupUD: LookupUD public var messageListeners: ListenersRegistry 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 registerLogWriter: RegisterLogWriter + public var resumeBackup: ResumeBackup public var searchUD: SearchUD + public var setLogLevel: SetLogLevel public var sleep: (TimeInterval) -> Void public var storageDir: String public var ud: Stored<UserDiscovery?> - public var udAddress: String? - public var udCert: Data? - public var udContact: Data? + public var udEnvironment: UDEnvironment? } extension MessengerEnvironment { @@ -45,6 +53,8 @@ extension MessengerEnvironment { public static func live() -> MessengerEnvironment { MessengerEnvironment( authCallbacks: .live(), + backup: .inMemory(), + backupCallbacks: .live(), cMix: .inMemory(), downloadNDF: .live, e2e: .inMemory(), @@ -56,24 +66,30 @@ extension MessengerEnvironment { getFileTransferParams: .liveDefault, getSingleUseParams: .liveDefault, initFileTransfer: .live, + initializeBackup: .live, + isListeningForMessages: .inMemory(false), isRegisteredWithUD: .live, loadCMix: .live, + logger: .live(), login: .live, lookupUD: .live, messageListeners: .live(), multiLookupUD: .live(), ndfEnvironment: .mainnet, newCMix: .live, + newCMixFromBackup: .live, newOrLoadUd: .live, + newUdManagerFromBackup: .live, passwordStorage: .keychain, registerForNotifications: .live, + registerLogWriter: .live, + resumeBackup: .live, searchUD: .live, + setLogLevel: .live, sleep: { Thread.sleep(forTimeInterval: $0) }, storageDir: MessengerEnvironment.defaultStorageDir, ud: .inMemory(), - udAddress: nil, - udCert: nil, - udContact: nil + udEnvironment: nil ) } } @@ -81,6 +97,8 @@ extension MessengerEnvironment { extension MessengerEnvironment { public static let unimplemented = MessengerEnvironment( authCallbacks: .unimplemented, + backup: .unimplemented(), + backupCallbacks: .unimplemented, cMix: .unimplemented(), downloadNDF: .unimplemented, e2e: .unimplemented(), @@ -92,23 +110,29 @@ extension MessengerEnvironment { getFileTransferParams: .unimplemented, getSingleUseParams: .unimplemented, initFileTransfer: .unimplemented, + initializeBackup: .unimplemented, + isListeningForMessages: .unimplemented(placeholder: false), isRegisteredWithUD: .unimplemented, loadCMix: .unimplemented, + logger: .unimplemented, login: .unimplemented, lookupUD: .unimplemented, messageListeners: .unimplemented, multiLookupUD: .unimplemented, ndfEnvironment: .unimplemented, newCMix: .unimplemented, + newCMixFromBackup: .unimplemented, newOrLoadUd: .unimplemented, + newUdManagerFromBackup: .unimplemented, passwordStorage: .unimplemented, registerForNotifications: .unimplemented, + registerLogWriter: .unimplemented, + resumeBackup: .unimplemented, searchUD: .unimplemented, + setLogLevel: .unimplemented, sleep: XCTUnimplemented("\(Self.self).sleep"), storageDir: "unimplemented", ud: .unimplemented(), - udAddress: nil, - udCert: nil, - udContact: nil + udEnvironment: nil ) } diff --git a/Sources/XXMessengerClient/Utils/BackupCallbackRegistry.swift b/Sources/XXMessengerClient/Utils/BackupCallbackRegistry.swift new file mode 100644 index 0000000000000000000000000000000000000000..7c7fc9e8ae08a983f72a4240728610bab152f965 --- /dev/null +++ b/Sources/XXMessengerClient/Utils/BackupCallbackRegistry.swift @@ -0,0 +1,36 @@ +import Foundation +import XCTestDynamicOverlay +import XXClient + +public struct BackupCallbacksRegistry { + public var register: (UpdateBackupFunc) -> Cancellable + public var registered: () -> UpdateBackupFunc +} + +extension BackupCallbacksRegistry { + public static func live() -> BackupCallbacksRegistry { + class Registry { + var callbacks: [UUID: UpdateBackupFunc] = [:] + } + let registry = Registry() + return BackupCallbacksRegistry( + register: { callback in + let id = UUID() + registry.callbacks[id] = callback + return Cancellable { registry.callbacks[id] = nil } + }, + registered: { + UpdateBackupFunc { data in + registry.callbacks.values.forEach { $0.handle(data) } + } + } + ) + } +} + +extension BackupCallbacksRegistry { + public static let unimplemented = BackupCallbacksRegistry( + register: XCTUnimplemented("\(Self.self).register", placeholder: Cancellable {}), + registered: XCTUnimplemented("\(Self.self).registered", placeholder: UpdateBackupFunc { _ in }) + ) +} diff --git a/Sources/XXMessengerClient/Utils/BackupParams.swift b/Sources/XXMessengerClient/Utils/BackupParams.swift new file mode 100644 index 0000000000000000000000000000000000000000..8bf39a5189049f234ec9ab991c63ecdf59b0f1ce --- /dev/null +++ b/Sources/XXMessengerClient/Utils/BackupParams.swift @@ -0,0 +1,21 @@ +import Foundation + +public struct BackupParams: Equatable { + public init( + username: String + ) { + self.username = username + } + + public var username: String +} + +extension BackupParams: Codable { + public static func decode(_ data: Data) throws -> Self { + try JSONDecoder().decode(Self.self, from: data) + } + + public func encode() throws -> Data { + try JSONEncoder().encode(self) + } +} diff --git a/Sources/XXMessengerClient/Utils/BackupStorage.swift b/Sources/XXMessengerClient/Utils/BackupStorage.swift new file mode 100644 index 0000000000000000000000000000000000000000..2f9112be7718f31fe4528e8a9bbe4c75c5d7db7a --- /dev/null +++ b/Sources/XXMessengerClient/Utils/BackupStorage.swift @@ -0,0 +1,82 @@ +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 stored: () -> Backup? + public var store: (Data) throws -> Void + public var observe: (@escaping Observer) -> Cancellable + public var remove: () throws -> Void +} + +extension BackupStorage { + 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( + stored: { + backup + }, + store: { data in + let newBackup = Backup( + date: now(), + data: data + ) + backup = newBackup + notifyObservers() + try fileManager.saveFile(path, newBackup.data) + }, + observe: { observer in + let id = UUID() + observers[id] = observer + return Cancellable { + observers[id] = nil + } + }, + remove: { + backup = nil + notifyObservers() + try fileManager.removeItem(path) + } + ) + } +} + +extension BackupStorage { + public static let unimplemented = BackupStorage( + stored: XCTUnimplemented("\(Self.self).stored"), + store: XCTUnimplemented("\(Self.self).store"), + observe: XCTUnimplemented("\(Self.self).observe", placeholder: Cancellable {}), + remove: XCTUnimplemented("\(Self.self).remove") + ) +} diff --git a/Sources/XXMessengerClient/Utils/LogMessage.swift b/Sources/XXMessengerClient/Utils/LogMessage.swift new file mode 100644 index 0000000000000000000000000000000000000000..7169d953a60109e654c6d7063cbbe3b1857394cd --- /dev/null +++ b/Sources/XXMessengerClient/Utils/LogMessage.swift @@ -0,0 +1,56 @@ +import Foundation +import Logging + +public struct LogMessage: Equatable { + public init(level: Logger.Level, text: String) { + self.level = level + self.text = text + } + + public var level: Logger.Level + public var text: String +} + +extension LogMessage { + public static func parse(_ string: String) -> LogMessage { + let level: Logger.Level + let text: String + let pattern = #"^([A-Z]+)( \d{4}/\d{2}/\d{2})?( \d{1,2}:\d{2}:\d{2}\.\d+)? (.*)"# + let regex = try! NSRegularExpression( + pattern: pattern, + options: .dotMatchesLineSeparators + ) + let stringRange = NSRange(location: 0, length: string.utf16.count) + if let match = regex.firstMatch(in: string, range: stringRange) { + var groups: [Int: String] = [:] + for rangeIndex in 1..<match.numberOfRanges { + let nsRange = match.range(at: rangeIndex) + if !NSEqualRanges(nsRange, NSMakeRange(NSNotFound, 0)) { + let group = (string as NSString).substring(with: nsRange) + groups[rangeIndex] = group + } + } + level = .fromString(groups[1]) + text = groups[4] ?? string + } else { + level = .notice + text = string + } + return LogMessage(level: level, text: text) + } +} + +private extension Logger.Level { + static func fromString(_ string: String?) -> Logger.Level { + switch string { + case "TRACE": return .trace + case "DEBUG": return .debug + case "INFO": return .info + case "WARN": return .warning + case "ERROR": return .error + case "CRITICAL": return .critical + case "FATAL": return .critical + default: return .notice + } + } +} diff --git a/Sources/XXMessengerClient/Utils/MessengerFileManager.swift b/Sources/XXMessengerClient/Utils/MessengerFileManager.swift index 700990ea64b97c92175995ded858de7fa7486381..ad116d82a6f4a15df0021aa5f8b6ebdd845b7298 100644 --- a/Sources/XXMessengerClient/Utils/MessengerFileManager.swift +++ b/Sources/XXMessengerClient/Utils/MessengerFileManager.swift @@ -3,8 +3,11 @@ 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 + public var saveFile: (String, Data) throws -> Void + public var loadFile: (String) throws -> Data? + public var modifiedTime: (String) throws -> Date? } extension MessengerFileManager { @@ -16,7 +19,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) } @@ -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 } ) } @@ -34,7 +47,10 @@ extension MessengerFileManager { extension MessengerFileManager { public static let unimplemented = MessengerFileManager( isDirectoryEmpty: XCTUnimplemented("\(Self.self).isDirectoryEmpty", placeholder: false), - removeDirectory: XCTUnimplemented("\(Self.self).removeDirectory"), - createDirectory: XCTUnimplemented("\(Self.self).createDirectory") + removeItem: XCTUnimplemented("\(Self.self).removeItem"), + createDirectory: XCTUnimplemented("\(Self.self).createDirectory"), + saveFile: XCTUnimplemented("\(Self.self).saveFile"), + loadFile: XCTUnimplemented("\(Self.self).loadFile"), + modifiedTime: XCTUnimplemented("\(Self.self).modifiedTime") ) } diff --git a/Sources/XXMessengerClient/Utils/MessengerLogger.swift b/Sources/XXMessengerClient/Utils/MessengerLogger.swift new file mode 100644 index 0000000000000000000000000000000000000000..0dcf0bc52ca3bdb1c20e935caeefcef9a81b8d8b --- /dev/null +++ b/Sources/XXMessengerClient/Utils/MessengerLogger.swift @@ -0,0 +1,90 @@ +import Foundation +import Logging +import XCTestDynamicOverlay + +public struct MessengerLogger { + public struct Log: Equatable { + public init(level: Logger.Level, message: String) { + self.level = level + self.message = message + } + + public var level: Logger.Level + public var message: String + } + + public var run: (Log, String, String, UInt) -> Void + + public func callAsFunction( + _ item: Log, + file: String = #fileID, + function: String = #function, + line: UInt = #line + ) { + run(item, file, function, line) + } +} + +extension MessengerLogger { + public static func live( + logger: Logger = Logger(label: "xx.network.MessengerClient") + ) -> MessengerLogger { + MessengerLogger { item, file, function, line in + logger.log( + level: item.level, + .init(stringLiteral: item.message), + file: file, + function: function, + line: line + ) + } + } +} + +extension MessengerLogger { + public static let unimplemented = MessengerLogger( + run: XCTUnimplemented("\(Self.self)") + ) +} + +extension MessengerLogger.Log { + static func parse(_ string: String) -> MessengerLogger.Log { + let level: Logger.Level + let message: String + let pattern = #"^([A-Z]+)( \d{4}/\d{2}/\d{2})?( \d{1,2}:\d{2}:\d{2}\.\d+)? (.*)"# + let regex = try! NSRegularExpression( + pattern: pattern, + options: .dotMatchesLineSeparators + ) + let stringRange = NSRange(location: 0, length: string.utf16.count) + if let match = regex.firstMatch(in: string, range: stringRange) { + var groups: [Int: String] = [:] + for rangeIndex in 1..<match.numberOfRanges { + let nsRange = match.range(at: rangeIndex) + if !NSEqualRanges(nsRange, NSMakeRange(NSNotFound, 0)) { + let group = (string as NSString).substring(with: nsRange) + groups[rangeIndex] = group + } + } + level = MessengerLogger.Log.level(form: groups[1]) + message = groups[4] ?? string + } else { + level = .notice + message = string + } + return MessengerLogger.Log(level: level, message: message) + } + + static func level(form string: String?) -> Logger.Level { + switch string { + case "TRACE": return .trace + case "DEBUG": return .debug + case "INFO": return .info + case "WARN": return .warning + case "ERROR": return .error + case "CRITICAL": return .critical + case "FATAL": return .critical + default: return .notice + } + } +} diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerBackupParamsTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerBackupParamsTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..b65a34b9fbac97ab2a3fc629a7bce09552ae39b1 --- /dev/null +++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerBackupParamsTests.swift @@ -0,0 +1,55 @@ +import CustomDump +import XCTest +import XXClient +@testable import XXMessengerClient + +final class MessengerBackupParamsTests: XCTestCase { + func testBackupParams() throws { + var didAddJSON: [String] = [] + + var env: MessengerEnvironment = .unimplemented + env.backup.get = { + var backup: Backup = .unimplemented + backup.isRunning.run = { true } + backup.addJSON.run = { didAddJSON.append($0) } + return backup + } + let backup: MessengerBackupParams = .live(env) + + try backup(.stub) + + XCTAssertNoDifference(didAddJSON, [ + String(data: try BackupParams.stub.encode(), encoding: .utf8)! + ]) + } + + func testBackupParamsWhenNoBackup() { + var env: MessengerEnvironment = .unimplemented + env.backup.get = { nil } + let backup: MessengerBackupParams = .live(env) + + XCTAssertThrowsError(try backup(.stub)) { error in + XCTAssertNoDifference( + error as NSError, + MessengerBackupParams.Error.notRunning as NSError + ) + } + } + + func testBackupParamsWhenBackupNotRunning() { + var env: MessengerEnvironment = .unimplemented + env.backup.get = { + var backup: Backup = .unimplemented + backup.isRunning.run = { false } + return backup + } + let backup: MessengerBackupParams = .live(env) + + XCTAssertThrowsError(try backup(.stub)) { error in + XCTAssertNoDifference( + error as NSError, + MessengerBackupParams.Error.notRunning as NSError + ) + } + } +} 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/MessengerCreateTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerCreateTests.swift index 2180dda3db578b74a892b2166e66a9beb20357dd..0c8ce3bab33f2923976b113098da9c124c9cc2bf 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 36745d028934142b59912d341649e1519b435e1e..99a52e8402ac9c93e71988adaf99610f21b1cb95 100644 --- a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerDestroyTests.swift +++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerDestroyTests.swift @@ -9,11 +9,12 @@ 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?] = [] var didRemovePassword = 0 + var didSetIsListeningForMessages: [Bool] = [] var env: MessengerEnvironment = .unimplemented env.cMix.get = { @@ -28,7 +29,8 @@ final class MessengerDestroyTests: XCTestCase { env.ud.set = { didSetUD.append($0) } env.e2e.set = { didSetE2E.append($0) } env.cMix.set = { didSetCMix.append($0) } - env.fileManager.removeDirectory = { didRemoveDirectory.append($0) } + env.isListeningForMessages.set = { didSetIsListeningForMessages.append($0) } + env.fileManager.removeItem = { didRemoveItem.append($0) } env.passwordStorage.remove = { didRemovePassword += 1 } let destroy: MessengerDestroy = .live(env) @@ -39,7 +41,8 @@ final class MessengerDestroyTests: XCTestCase { XCTAssertNoDifference(didSetUD.map { $0 == nil }, [true]) XCTAssertNoDifference(didSetE2E.map { $0 == nil }, [true]) XCTAssertNoDifference(didSetCMix.map { $0 == nil }, [true]) - XCTAssertNoDifference(didRemoveDirectory, [storageDir]) + XCTAssertNoDifference(didSetIsListeningForMessages, [false]) + XCTAssertNoDifference(didRemoveItem, [storageDir]) XCTAssertNoDifference(didRemovePassword, 1) } @@ -67,13 +70,15 @@ 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.fileManager.removeDirectory = { _ in throw error } + env.isListeningForMessages.set = { didSetIsListeningForMessages.append($0) } + env.fileManager.removeItem = { _ in throw error } let destroy: MessengerDestroy = .live(env) XCTAssertThrowsError(try destroy()) { err in @@ -82,24 +87,27 @@ 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() { 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?] = [] + 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.fileManager.removeItem = { didRemoveItem.append($0) } env.passwordStorage.remove = { throw error } let destroy: MessengerDestroy = .live(env) @@ -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(didRemoveDirectory, [storageDir]) + XCTAssertNoDifference(didSetIsListeningForMessages, [false]) + XCTAssertNoDifference(didRemoveItem, [storageDir]) } } diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerIsBackupRunningTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerIsBackupRunningTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..7076aa648b4d287ece157cf56ae868000fdfe6ed --- /dev/null +++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerIsBackupRunningTests.swift @@ -0,0 +1,37 @@ +import XCTest +import XXClient +@testable import XXMessengerClient + +final class MessengerIsBackupRunningTests: XCTestCase { + func testWithoutBackup() { + var env: MessengerEnvironment = .unimplemented + env.backup.get = { nil } + let isRunning: MessengerIsBackupRunning = .live(env) + + XCTAssertFalse(isRunning()) + } + + func testWithBackupRunning() { + var env: MessengerEnvironment = .unimplemented + env.backup.get = { + var backup: Backup = .unimplemented + backup.isRunning.run = { true } + return backup + } + let isRunning: MessengerIsBackupRunning = .live(env) + + XCTAssertTrue(isRunning()) + } + + func testWithBackupNotRunning() { + var env: MessengerEnvironment = .unimplemented + env.backup.get = { + var backup: Backup = .unimplemented + backup.isRunning.run = { false } + return backup + } + let isRunning: MessengerIsBackupRunning = .live(env) + + XCTAssertFalse(isRunning()) + } +} 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/MessengerLogInTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerLogInTests.swift index db4e9742a0c6d5160c144243443d68c2babce8bc..26dd2da85798c9e0a50ecac9e05665a7c55ba780 100644 --- a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerLogInTests.swift +++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerLogInTests.swift @@ -11,10 +11,12 @@ final class MessengerLogInTests: XCTestCase { let e2eId = 1234 let networkFollowerStatus: NetworkFollowerStatus = .stopped - let udCertFromNDF = "ndf-ud-cert".data(using: .utf8)! - let udContactFromNDF = "ndf-ud-contact".data(using: .utf8)! - let udAddressFromNDF = "ndf-ud-address" - + let udEnvironmentFromNDF = UDEnvironment( + address: "ndf-ud-address", + cert: "ndf-ud-cert".data(using: .utf8)!, + contact: "ndf-ud-contact".data(using: .utf8)! + ) + var env: MessengerEnvironment = .unimplemented env.cMix.get = { var cMix: CMix = .unimplemented @@ -24,15 +26,11 @@ final class MessengerLogInTests: XCTestCase { env.e2e.get = { var e2e: E2E = .unimplemented e2e.getId.run = { e2eId } - e2e.getUdCertFromNdf.run = { udCertFromNDF } - e2e.getUdContactFromNdf.run = { udContactFromNDF } - e2e.getUdAddressFromNdf.run = { udAddressFromNDF } + e2e.getUdEnvironmentFromNdf.run = { udEnvironmentFromNDF } return e2e } env.ud.set = { didSetUD.append($0) } - env.udCert = nil - env.udContact = nil - env.udAddress = nil + env.udEnvironment = nil env.newOrLoadUd.run = { params, follower in didNewOrLoadUDWithParams.append(params) didNewOrLoadUDWithFollower.append(follower) @@ -45,9 +43,7 @@ final class MessengerLogInTests: XCTestCase { e2eId: e2eId, username: nil, registrationValidationSignature: nil, - cert: udCertFromNDF, - contact: udContactFromNDF, - address: udAddressFromNDF + environment: udEnvironmentFromNDF )]) XCTAssertEqual(didNewOrLoadUDWithFollower.count, 1) XCTAssertEqual( @@ -62,9 +58,11 @@ final class MessengerLogInTests: XCTestCase { var didSetUD: [UserDiscovery?] = [] let e2eId = 1234 - let altUdCert = "alt-ud-cert".data(using: .utf8)! - let altUdContact = "alt-ud-contact".data(using: .utf8)! - let altUdAddress = "alt-ud-address" + let udEnvironment = UDEnvironment( + address: "alt-ud-address", + cert: "alt-ud-cert".data(using: .utf8)!, + contact: "alt-ud-contact".data(using: .utf8)! + ) var env: MessengerEnvironment = .unimplemented env.cMix.get = { @@ -78,9 +76,7 @@ final class MessengerLogInTests: XCTestCase { return e2e } env.ud.set = { didSetUD.append($0) } - env.udCert = altUdCert - env.udContact = altUdContact - env.udAddress = altUdAddress + env.udEnvironment = udEnvironment env.newOrLoadUd.run = { params, _ in didNewOrLoadUDWithParams.append(params) return .unimplemented @@ -92,9 +88,7 @@ final class MessengerLogInTests: XCTestCase { e2eId: e2eId, username: nil, registrationValidationSignature: nil, - cert: altUdCert, - contact: altUdContact, - address: altUdAddress + environment: udEnvironment )]) XCTAssertEqual(didSetUD.compactMap { $0 }.count, 1) } @@ -139,12 +133,10 @@ final class MessengerLogInTests: XCTestCase { env.e2e.get = { var e2e: E2E = .unimplemented e2e.getId.run = { 1234 } - e2e.getUdCertFromNdf.run = { "ndf-ud-cert".data(using: .utf8)! } - e2e.getUdContactFromNdf.run = { throw error } + e2e.getUdEnvironmentFromNdf.run = { throw error } return e2e } - env.udCert = nil - env.udContact = nil + env.udEnvironment = nil let logIn: MessengerLogIn = .live(env) XCTAssertThrowsError(try logIn()) { err in @@ -167,9 +159,11 @@ final class MessengerLogInTests: XCTestCase { e2e.getId.run = { 1234 } return e2e } - env.udCert = "ud-cert".data(using: .utf8)! - env.udContact = "ud-contact".data(using: .utf8)! - env.udAddress = "ud-address" + env.udEnvironment = UDEnvironment( + address: "ud-address", + cert: "ud-cert".data(using: .utf8)!, + contact: "ud-contact".data(using: .utf8)! + ) env.newOrLoadUd.run = { _, _ in throw error } let logIn: MessengerLogIn = .live(env) diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerMyContactTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerMyContactTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..113677612ceb36dd8d08e8c35965c890c2586d9c --- /dev/null +++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerMyContactTests.swift @@ -0,0 +1,121 @@ +import CustomDump +import XCTest +import XXClient +@testable import XXMessengerClient + +final class MessengerMyContactTests: XCTestCase { + func testMyContactWithAllFacts() throws { + let e2eContactData = "e2e-contact-data".data(using: .utf8)! + var e2eContactSetFacts: [[Fact]] = [] + let e2eContactWithFactsData = "e2e-contact-with-facts-data".data(using: .utf8)! + let udFacts = [ + Fact(type: .username, value: "ud-fact-username"), + Fact(type: .email, value: "ud-fact-email"), + Fact(type: .phone, value: "ud-fact-phone"), + ] + var env: MessengerEnvironment = .unimplemented + env.e2e.get = { + var e2e: E2E = .unimplemented + e2e.getContact.run = { + var contact: Contact = .unimplemented(e2eContactData) + contact.setFactsOnContact.run = { _, facts in + e2eContactSetFacts.append(facts) + return e2eContactWithFactsData + } + return contact + } + return e2e + } + env.ud.get = { + var ud: UserDiscovery = .unimplemented + ud.getFacts.run = { udFacts } + return ud + } + let myContact: MessengerMyContact = .live(env) + + let contact = try myContact() + + XCTAssertNoDifference(e2eContactSetFacts, [udFacts]) + XCTAssertNoDifference(contact, .unimplemented(e2eContactWithFactsData)) + } + + func testMyContactWithoutFacts() throws { + let e2eContactData = "e2e-contact-data".data(using: .utf8)! + var env: MessengerEnvironment = .unimplemented + env.e2e.get = { + var e2e: E2E = .unimplemented + e2e.getContact.run = { .unimplemented(e2eContactData) } + return e2e + } + let myContact: MessengerMyContact = .live(env) + + let contact = try myContact(includeFacts: .none) + + XCTAssertNoDifference(contact, .unimplemented(e2eContactData)) + } + + func testMyContactWithFactTypes() throws { + let e2eContactData = "e2e-contact-data".data(using: .utf8)! + var e2eContactSetFacts: [[Fact]] = [] + let e2eContactWithFactsData = "e2e-contact-with-facts-data".data(using: .utf8)! + let udFactUsername = Fact(type: .username, value: "ud-fact-username") + let udFactEmail = Fact(type: .email, value: "ud-fact-email") + let udFactPhone = Fact(type: .phone, value: "ud-fact-phone") + let udFacts = [udFactUsername, udFactEmail, udFactPhone] + var env: MessengerEnvironment = .unimplemented + env.e2e.get = { + var e2e: E2E = .unimplemented + e2e.getContact.run = { + var contact: Contact = .unimplemented(e2eContactData) + contact.setFactsOnContact.run = { _, facts in + e2eContactSetFacts.append(facts) + return e2eContactWithFactsData + } + return contact + } + return e2e + } + env.ud.get = { + var ud: UserDiscovery = .unimplemented + ud.getFacts.run = { udFacts } + return ud + } + let myContact: MessengerMyContact = .live(env) + + let contact = try myContact(includeFacts: .types([.username, .phone])) + + XCTAssertNoDifference(e2eContactSetFacts, [[udFactUsername, udFactPhone]]) + XCTAssertNoDifference(contact, .unimplemented(e2eContactWithFactsData)) + } + + func testMyContactWhenNotConnected() { + var env: MessengerEnvironment = .unimplemented + env.e2e.get = { nil } + let myContact: MessengerMyContact = .live(env) + + XCTAssertThrowsError(try myContact()) { error in + XCTAssertNoDifference( + error as NSError, + MessengerMyContact.Error.notConnected as NSError + ) + } + } + + func testMyContactWithFactsWhenNotLoggedIn() { + var env: MessengerEnvironment = .unimplemented + env.e2e.get = { + var e2e: E2E = .unimplemented + e2e.getContact.run = { .unimplemented(Data()) } + return e2e + } + env.ud.get = { nil } + let myContact: MessengerMyContact = .live(env) + + XCTAssertThrowsError(try myContact()) { error in + XCTAssertNoDifference( + error as NSError, + MessengerMyContact.Error.notLoggedIn as NSError + ) + } + } +} diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerRegisterBackupCallbackTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerRegisterBackupCallbackTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..44e7e21c2bbac9badafd94370a595fea209da2f9 --- /dev/null +++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerRegisterBackupCallbackTests.swift @@ -0,0 +1,34 @@ +import CustomDump +import XCTest +import XXClient +@testable import XXMessengerClient + +final class MessengerRegisterBackupCallbackTests: XCTestCase { + func testRegisterBackupCallback() { + var registeredCallbacks: [UpdateBackupFunc] = [] + var didHandleData: [Data] = [] + var didCancelRegisteredCallback = 0 + + var env: MessengerEnvironment = .unimplemented + env.backupCallbacks.register = { callback in + registeredCallbacks.append(callback) + return Cancellable { didCancelRegisteredCallback += 1 } + } + let registerBackupCallback: MessengerRegisterBackupCallback = .live(env) + let cancellable = registerBackupCallback(UpdateBackupFunc { data in + didHandleData.append(data) + }) + + XCTAssertEqual(registeredCallbacks.count, 1) + + registeredCallbacks.forEach { callback in + callback.handle("test".data(using: .utf8)!) + } + + XCTAssertNoDifference(didHandleData, ["test".data(using: .utf8)!]) + + cancellable.cancel() + + XCTAssertEqual(didCancelRegisteredCallback, 1) + } +} diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerRegisterTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerRegisterTests.swift index 35d078c2a38314245dbd1ede4517d82022ef1507..2ec30071accc8ef475a3e89134221c76e26bc4da 100644 --- a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerRegisterTests.swift +++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerRegisterTests.swift @@ -12,9 +12,11 @@ final class MessengerRegisterTests: XCTestCase { let e2eId = 1234 let networkFollowerStatus: NetworkFollowerStatus = .stopped let registrationSignature = "registration-signature".data(using: .utf8)! - let udCertFromNDF = "ndf-ud-cert".data(using: .utf8)! - let udContactFromNDF = "ndf-ud-contact".data(using: .utf8)! - let udAddressFromNDF = "ndf-ud-address" + let udEnvironmentFromNDF = UDEnvironment( + address: "ndf-ud-address", + cert: "ndf-ud-cert".data(using: .utf8)!, + contact: "ndf-ud-contact".data(using: .utf8)! + ) let username = "new-user-name" var env: MessengerEnvironment = .unimplemented @@ -29,15 +31,11 @@ final class MessengerRegisterTests: XCTestCase { env.e2e.get = { var e2e: E2E = .unimplemented e2e.getId.run = { e2eId } - e2e.getUdCertFromNdf.run = { udCertFromNDF } - e2e.getUdContactFromNdf.run = { udContactFromNDF } - e2e.getUdAddressFromNdf.run = { udAddressFromNDF } + e2e.getUdEnvironmentFromNdf.run = { udEnvironmentFromNDF } return e2e } env.ud.set = { didSetUD.append($0) } - env.udCert = nil - env.udContact = nil - env.udAddress = nil + env.udEnvironment = nil env.newOrLoadUd.run = { params, follower in didNewOrLoadUDWithParams.append(params) didNewOrLoadUDWithFollower.append(follower) @@ -50,9 +48,7 @@ final class MessengerRegisterTests: XCTestCase { e2eId: e2eId, username: username, registrationValidationSignature: registrationSignature, - cert: udCertFromNDF, - contact: udContactFromNDF, - address: udAddressFromNDF + environment: udEnvironmentFromNDF )]) XCTAssertEqual(didNewOrLoadUDWithFollower.count, 1) XCTAssertEqual( @@ -68,9 +64,11 @@ final class MessengerRegisterTests: XCTestCase { let e2eId = 1234 let registrationSignature = "registration-signature".data(using: .utf8)! - let altUdCert = "alt-ud-cert".data(using: .utf8)! - let altUdContact = "alt-ud-contact".data(using: .utf8)! - let altUdAddress = "alt-ud-address" + let udEnvironment = UDEnvironment( + address: "alt-ud-address", + cert: "alt-ud-cert".data(using: .utf8)!, + contact: "alt-ud-contact".data(using: .utf8)! + ) let username = "new-user-name" var env: MessengerEnvironment = .unimplemented @@ -88,9 +86,7 @@ final class MessengerRegisterTests: XCTestCase { return e2e } env.ud.set = { didSetUD.append($0) } - env.udCert = altUdCert - env.udContact = altUdContact - env.udAddress = altUdAddress + env.udEnvironment = udEnvironment env.newOrLoadUd.run = { params, _ in didNewOrLoadUDWithParams.append(params) return .unimplemented @@ -102,9 +98,7 @@ final class MessengerRegisterTests: XCTestCase { e2eId: e2eId, username: username, registrationValidationSignature: registrationSignature, - cert: altUdCert, - contact: altUdContact, - address: altUdAddress + environment: udEnvironment )]) XCTAssertEqual(didSetUD.compactMap { $0 }.count, 1) } @@ -152,12 +146,10 @@ final class MessengerRegisterTests: XCTestCase { env.e2e.get = { var e2e: E2E = .unimplemented e2e.getId.run = { 1234 } - e2e.getUdCertFromNdf.run = { "ndf-ud-cert".data(using: .utf8)! } - e2e.getUdContactFromNdf.run = { throw error } + e2e.getUdEnvironmentFromNdf.run = { throw error } return e2e } - env.udCert = nil - env.udContact = nil + env.udEnvironment = nil let register: MessengerRegister = .live(env) XCTAssertThrowsError(try register(username: "new-user-name")) { err in @@ -183,9 +175,11 @@ final class MessengerRegisterTests: XCTestCase { e2e.getId.run = { 1234 } return e2e } - env.udCert = "ud-cert".data(using: .utf8)! - env.udContact = "ud-contact".data(using: .utf8)! - env.udAddress = "ud-address" + env.udEnvironment = UDEnvironment( + address: "ud-address", + cert: "ud-cert".data(using: .utf8)!, + contact: "ud-contact".data(using: .utf8)! + ) env.newOrLoadUd.run = { _, _ in throw error } let register: MessengerRegister = .live(env) diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerRestoreBackupTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerRestoreBackupTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..4f819b47393545d46caffb71097dc92ff2f08337 --- /dev/null +++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerRestoreBackupTests.swift @@ -0,0 +1,225 @@ +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.stub + 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 udEnvironmentFromNDF = UDEnvironment( + address: "ud-address", + cert: "ud-cert".data(using: .utf8)!, + contact: "ud-contact".data(using: .utf8)! + ) + + 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.removeItem = { caughtActions.append(.didRemoveItem(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.getUdEnvironmentFromNdf.run = { udEnvironmentFromNDF } + 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 + ), + .didRemoveItem( + 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 + ), + .didSetCMix, + .cMixDidStartNetworkFollower( + timeoutMS: 30_000 + ), + .cMixDidMakeReceptionIdentity( + legacy: true + ), + .didLogin( + ephemeral: false, + cMixId: cMixId, + identity: receptionIdentity, + e2eParamsJSON: e2eParams + ), + .didSetE2E, + .didSetIsListeningForMessages( + isListening: false + ), + .didNewUdManagerFromBackup(params: .init( + e2eId: e2eId, + environment: udEnvironmentFromNDF + )), + .didSetUD, + ]) + + XCTAssertNoDifference(result, MessengerRestoreBackup.Result( + restoredParams: backupParams, + restoredContacts: backupContacts + )) + } + + func testDownloadNdfFailure() { + struct Failure: Error, Equatable {} + let failure = Failure() + + var env: MessengerEnvironment = .unimplemented + env.downloadNDF.run = { _ in throw failure } + let restore: MessengerRestoreBackup = .live(env) + + XCTAssertThrowsError(try restore(backupData: Data(), backupPassphrase: "")) { error in + XCTAssertNoDifference(error as? Failure, failure) + } + } +} + +private enum CaughtAction: Equatable { + case didDownloadNDF( + environment: NDFEnvironment + ) + case didSavePassword( + password: Data + ) + case didRemoveItem( + 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 + ) +} diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerResumeBackupTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerResumeBackupTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..7979ffbd7da561d8b3405e701d2bb84718e39ea1 --- /dev/null +++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerResumeBackupTests.swift @@ -0,0 +1,126 @@ +import CustomDump +import XCTest +import XXClient +@testable import XXMessengerClient + +final class MessengerResumeBackupTests: XCTestCase { + func testResume() throws { + struct ResumeBackupParams: Equatable { + var e2eId: Int + var udId: Int + } + var didResumeBackup: [ResumeBackupParams] = [] + var backupCallbacks: [UpdateBackupFunc] = [] + var didHandleCallback: [Data] = [] + var didSetBackup: [Backup?] = [] + + let e2eId = 123 + let udId = 321 + let data = "test-data".data(using: .utf8)! + + var env: MessengerEnvironment = .unimplemented + env.backup.get = { nil } + env.backup.set = { didSetBackup.append($0) } + env.e2e.get = { + var e2e: E2E = .unimplemented + e2e.getId.run = { e2eId } + return e2e + } + env.ud.get = { + var ud: UserDiscovery = .unimplemented + ud.getId.run = { udId } + return ud + } + env.backupCallbacks.registered = { + UpdateBackupFunc { didHandleCallback.append($0) } + } + env.resumeBackup.run = { e2eId, udId, callback in + didResumeBackup.append(.init(e2eId: e2eId, udId: udId)) + backupCallbacks.append(callback) + return .unimplemented + } + let resume: MessengerResumeBackup = .live(env) + + try resume() + + XCTAssertNoDifference(didResumeBackup, [ + .init(e2eId: e2eId, udId: udId) + ]) + XCTAssertNoDifference(didSetBackup.map { $0 != nil }, [true]) + + backupCallbacks.forEach { $0.handle(data) } + + XCTAssertNoDifference(didHandleCallback, [data]) + } + + func testResumeWhenRunning() { + var env: MessengerEnvironment = .unimplemented + env.backup.get = { + var backup: Backup = .unimplemented + backup.isRunning.run = { true } + return backup + } + let resume: MessengerResumeBackup = .live(env) + + XCTAssertThrowsError(try resume()) { error in + XCTAssertNoDifference( + error as NSError, + MessengerResumeBackup.Error.isRunning as NSError + ) + } + } + + func testResumeWhenNotConnected() { + var env: MessengerEnvironment = .unimplemented + env.backup.get = { nil } + env.e2e.get = { nil } + let resume: MessengerResumeBackup = .live(env) + + XCTAssertThrowsError(try resume()) { error in + XCTAssertNoDifference( + error as NSError, + MessengerResumeBackup.Error.notConnected as NSError + ) + } + } + + func testResumeWhenNotLoggedIn() { + var env: MessengerEnvironment = .unimplemented + env.backup.get = { nil } + env.e2e.get = { .unimplemented } + env.ud.get = { nil } + let resume: MessengerResumeBackup = .live(env) + + XCTAssertThrowsError(try resume()) { error in + XCTAssertNoDifference( + error as NSError, + MessengerResumeBackup.Error.notLoggedIn as NSError + ) + } + } + + func testResumeFailure() { + struct Failure: Error, Equatable {} + let failure = Failure() + + var env: MessengerEnvironment = .unimplemented + env.backup.get = { nil } + env.e2e.get = { + var e2e: E2E = .unimplemented + e2e.getId.run = { 123 } + return e2e + } + env.ud.get = { + var ud: UserDiscovery = .unimplemented + ud.getId.run = { 321 } + return ud + } + env.backupCallbacks.registered = { UpdateBackupFunc { _ in } } + env.resumeBackup.run = { _, _ , _ in throw failure } + let resume: MessengerResumeBackup = .live(env) + + XCTAssertThrowsError(try resume()) { error in + XCTAssertNoDifference(error as NSError, failure as NSError) + } + } +} diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerSetLogLevelTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerSetLogLevelTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..d6d44d05465f52cc6c9cb7b0a949397d22560b42 --- /dev/null +++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerSetLogLevelTests.swift @@ -0,0 +1,44 @@ +import CustomDump +import XCTest +import XXClient +@testable import XXMessengerClient + +final class MessengerSetLogLevelTests: XCTestCase { + func testSetLogLevel() throws { + var didSetLogLevel: [LogLevel] = [] + var env: MessengerEnvironment = .unimplemented + env.setLogLevel.run = { level in + didSetLogLevel.append(level) + return true + } + let setLogLevel: MessengerSetLogLevel = .live(env) + + let result = try setLogLevel(.debug) + + XCTAssertNoDifference(didSetLogLevel, [.debug]) + XCTAssertNoDifference(result, true) + } + + func testSetLogLevelReturnsFalse() throws { + var env: MessengerEnvironment = .unimplemented + env.setLogLevel.run = { _ in return false } + let setLogLevel: MessengerSetLogLevel = .live(env) + + let result = try setLogLevel(.debug) + + XCTAssertNoDifference(result, false) + } + + func testSetLogLevelFailure() { + struct Failure: Error, Equatable {} + let failure = Failure() + + var env: MessengerEnvironment = .unimplemented + env.setLogLevel.run = { _ in throw failure } + let setLogLevel: MessengerSetLogLevel = .live(env) + + XCTAssertThrowsError(try setLogLevel(.debug)) { error in + XCTAssertNoDifference(error as NSError, failure as NSError) + } + } +} diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerStartBackupTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerStartBackupTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..b92bd1ee38948737453e973a8e4b4cd874797b84 --- /dev/null +++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerStartBackupTests.swift @@ -0,0 +1,147 @@ +import CustomDump +import XCTest +import XXClient +@testable import XXMessengerClient + +final class MessengerStartBackupTests: XCTestCase { + func testStart() throws { + struct InitBackupParams: Equatable { + var e2eId: Int + var udId: Int + var password: String + } + var didInitializeBackup: [InitBackupParams] = [] + var backupCallbacks: [UpdateBackupFunc] = [] + var didHandleCallback: [Data] = [] + var didSetBackup: [Backup?] = [] + var didAddJSON: [String] = [] + + let password = "test-password" + let e2eId = 123 + let udId = 321 + let dataWithoutParams = "backup-without-params".data(using: .utf8)! + let dataWithParams = "backup-with-params".data(using: .utf8)! + + var env: MessengerEnvironment = .unimplemented + env.backup.get = { didSetBackup.last as? Backup } + env.backup.set = { didSetBackup.append($0) } + env.e2e.get = { + var e2e: E2E = .unimplemented + e2e.getId.run = { e2eId } + return e2e + } + env.ud.get = { + var ud: UserDiscovery = .unimplemented + ud.getId.run = { udId } + return ud + } + env.backupCallbacks.registered = { + UpdateBackupFunc { didHandleCallback.append($0) } + } + env.initializeBackup.run = { e2eId, udId, password, callback in + didInitializeBackup.append(.init(e2eId: e2eId, udId: udId, password: password)) + backupCallbacks.append(callback) + var backup: Backup = .unimplemented + backup.addJSON.run = { string in + didAddJSON.append(string) + } + return backup + } + let start: MessengerStartBackup = .live(env) + + try start(password: password, params: .stub) + + XCTAssertNoDifference(didInitializeBackup, [ + .init(e2eId: e2eId, udId: udId, password: password) + ]) + XCTAssertNoDifference(didSetBackup.map { $0 != nil }, [true]) + + backupCallbacks.forEach { $0.handle(dataWithoutParams) } + + XCTAssertNoDifference( + didHandleCallback.map(StringData.init), + [].map(StringData.init) + ) + XCTAssertNoDifference(didAddJSON, [ + String(data: try BackupParams.stub.encode(), encoding: .utf8)! + ]) + + backupCallbacks.forEach { $0.handle(dataWithParams) } + + XCTAssertNoDifference( + didHandleCallback.map(StringData.init), + [dataWithParams].map(StringData.init) + ) + } + + func testStartWhenRunning() { + var env: MessengerEnvironment = .unimplemented + env.backup.get = { + var backup: Backup = .unimplemented + backup.isRunning.run = { true } + return backup + } + let start: MessengerStartBackup = .live(env) + + XCTAssertThrowsError(try start(password: "", params: .stub)) { error in + XCTAssertNoDifference( + error as NSError, + MessengerStartBackup.Error.isRunning as NSError + ) + } + } + + func testStartWhenNotConnected() { + var env: MessengerEnvironment = .unimplemented + env.backup.get = { nil } + env.e2e.get = { nil } + let start: MessengerStartBackup = .live(env) + + XCTAssertThrowsError(try start(password: "", params: .stub)) { error in + XCTAssertNoDifference( + error as NSError, + MessengerStartBackup.Error.notConnected as NSError + ) + } + } + + func testStartWhenNotLoggedIn() { + var env: MessengerEnvironment = .unimplemented + env.backup.get = { nil } + env.e2e.get = { .unimplemented } + env.ud.get = { nil } + let start: MessengerStartBackup = .live(env) + + XCTAssertThrowsError(try start(password: "", params: .stub)) { error in + XCTAssertNoDifference( + error as NSError, + MessengerStartBackup.Error.notLoggedIn as NSError + ) + } + } + + func testStartFailure() { + struct Failure: Error, Equatable {} + let failure = Failure() + + var env: MessengerEnvironment = .unimplemented + env.backup.get = { nil } + env.e2e.get = { + var e2e: E2E = .unimplemented + e2e.getId.run = { 123 } + return e2e + } + env.ud.get = { + var ud: UserDiscovery = .unimplemented + ud.getId.run = { 321 } + return ud + } + env.backupCallbacks.registered = { UpdateBackupFunc { _ in } } + env.initializeBackup.run = { _, _, _, _ in throw failure } + let start: MessengerStartBackup = .live(env) + + XCTAssertThrowsError(try start(password: "", params: .stub)) { error in + XCTAssertNoDifference(error as NSError, failure as NSError) + } + } +} diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerStartLoggingTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerStartLoggingTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..d3b4fb38c9e21fea85a58e49c9e6e93f80e5a33f --- /dev/null +++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerStartLoggingTests.swift @@ -0,0 +1,30 @@ +import CustomDump +import XCTest +import XXClient +@testable import XXMessengerClient + +final class MessengerStartLoggingTests: XCTestCase { + func testStartLogging() { + var registeredLogWriters: [LogWriter] = [] + var logs: [MessengerLogger.Log] = [] + + var env: MessengerEnvironment = .unimplemented + env.registerLogWriter.run = { writer in + registeredLogWriters.append(writer) + } + env.logger.run = { log, _, _, _ in + logs.append(log) + } + let start: MessengerStartLogging = .live(env) + + start() + + XCTAssertNoDifference(registeredLogWriters.count, 1) + + registeredLogWriters.first?.handle("DEBUG Hello, World!") + + XCTAssertNoDifference(logs, [ + .init(level: .debug, message: "Hello, World!"), + ]) + } +} diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerStopBackupTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerStopBackupTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..306b6c2d1d6b6507cd0bd83071d54ad99fd2e8f7 --- /dev/null +++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerStopBackupTests.swift @@ -0,0 +1,53 @@ +import CustomDump +import XCTest +import XXClient +@testable import XXMessengerClient + +final class MessengerStopBackupTests: XCTestCase { + func testStop() throws { + var didStopBackup = 0 + var didSetBackup: [Backup?] = [] + + var env: MessengerEnvironment = .unimplemented + env.backup.get = { + var backup: Backup = .unimplemented + backup.stop.run = { didStopBackup += 1 } + return backup + } + env.backup.set = { backup in + didSetBackup.append(backup) + } + let stop: MessengerStopBackup = .live(env) + + try stop() + + XCTAssertEqual(didStopBackup, 1) + XCTAssertEqual(didSetBackup.count, 1) + XCTAssertNil(didSetBackup.first as? Backup) + } + + func testStopFailure() { + struct Failure: Error, Equatable {} + let failure = Failure() + + var env: MessengerEnvironment = .unimplemented + env.backup.get = { + var backup: Backup = .unimplemented + backup.stop.run = { throw failure } + return backup + } + let stop: MessengerStopBackup = .live(env) + + XCTAssertThrowsError(try stop()) { error in + XCTAssertNoDifference(error as NSError, failure as NSError) + } + } + + func testStopWithoutBackup() throws { + var env: MessengerEnvironment = .unimplemented + env.backup.get = { nil } + let stop: MessengerStopBackup = .live(env) + + try stop() + } +} diff --git a/Tests/XXMessengerClientTests/Messenger/Functions/MessengerStopTests.swift b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerStopTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..e9cfd928ef2fb878121fb074fdb7dc389e2313f8 --- /dev/null +++ b/Tests/XXMessengerClientTests/Messenger/Functions/MessengerStopTests.swift @@ -0,0 +1,123 @@ +import CustomDump +import XCTest +import XXClient +@testable import XXMessengerClient + +final class MessengerStopTests: XCTestCase { + func testStop() throws { + var didStopNetworkFollower = 0 + + var env: MessengerEnvironment = .unimplemented + env.cMix.get = { + var cMix: CMix = .unimplemented + cMix.networkFollowerStatus.run = { .running } + cMix.stopNetworkFollower.run = { didStopNetworkFollower += 1 } + return cMix + } + let stop: MessengerStop = .live(env) + + try stop() + + XCTAssertNoDifference(didStopNetworkFollower, 1) + } + + func testStopWhenNotLoaded() { + var env: MessengerEnvironment = .unimplemented + env.cMix.get = { nil } + let stop: MessengerStop = .live(env) + + XCTAssertThrowsError(try stop()) { error in + XCTAssertNoDifference( + error as NSError, + MessengerStop.Error.notLoaded as NSError + ) + } + } + + func testStopWhenNotRunning() throws { + var env: MessengerEnvironment = .unimplemented + env.cMix.get = { + var cMix: CMix = .unimplemented + cMix.networkFollowerStatus.run = { .stopped } + return cMix + } + let stop: MessengerStop = .live(env) + + try stop() + } + + func testStopFailure() { + struct Failure: Error {} + let failure = Failure() + + var env: MessengerEnvironment = .unimplemented + env.cMix.get = { + var cMix: CMix = .unimplemented + cMix.networkFollowerStatus.run = { .running } + cMix.stopNetworkFollower.run = { throw failure } + return cMix + } + let stop: MessengerStop = .live(env) + + XCTAssertThrowsError(try stop()) { error in + XCTAssertNoDifference( + error as NSError, + failure as NSError + ) + } + } + + func testStopAndWait() throws { + var hasRunningProcesses: [Bool] = [true, true, false] + var didStopNetworkFollower = 0 + var didSleep: [TimeInterval] = [] + + var env: MessengerEnvironment = .unimplemented + env.cMix.get = { + var cMix: CMix = .unimplemented + cMix.networkFollowerStatus.run = { .running } + cMix.stopNetworkFollower.run = { didStopNetworkFollower += 1 } + cMix.hasRunningProcesses.run = { hasRunningProcesses.removeFirst() } + return cMix + } + env.sleep = { didSleep.append($0) } + let stop: MessengerStop = .live(env) + + try stop(wait: .init(sleepInterval: 123, retries: 3)) + + XCTAssertNoDifference(didStopNetworkFollower, 1) + XCTAssertNoDifference(didSleep, [123, 123]) + } + + func testStopAndWaitTimeout() { + var hasRunningProcesses: [Bool] = [true, true, true, true] + var didStopNetworkFollower = 0 + var didSleep: [TimeInterval] = [] + + var env: MessengerEnvironment = .unimplemented + env.cMix.get = { + var cMix: CMix = .unimplemented + cMix.networkFollowerStatus.run = { .running } + cMix.stopNetworkFollower.run = { didStopNetworkFollower += 1 } + cMix.hasRunningProcesses.run = { hasRunningProcesses.removeFirst() } + return cMix + } + env.sleep = { didSleep.append($0) } + let stop: MessengerStop = .live(env) + + XCTAssertThrowsError( + try stop(wait: .init( + sleepInterval: 123, + retries: 3 + )) + ) { error in + XCTAssertNoDifference( + error as NSError, + MessengerStop.Error.timedOut as NSError + ) + } + + XCTAssertNoDifference(didStopNetworkFollower, 1) + XCTAssertNoDifference(didSleep, [123, 123, 123]) + } +} diff --git a/Tests/XXMessengerClientTests/TestHelpers/StringData.swift b/Tests/XXMessengerClientTests/TestHelpers/StringData.swift new file mode 100644 index 0000000000000000000000000000000000000000..6987a4ea385989f6823e701b0e00002b036f0f00 --- /dev/null +++ b/Tests/XXMessengerClientTests/TestHelpers/StringData.swift @@ -0,0 +1,14 @@ +import CustomDump +import Foundation + +struct StringData: Equatable, CustomDumpStringConvertible { + var data: Data + + var customDumpDescription: String { + if let string = String(data: data, encoding: .utf8) { + return #"Data(string: "\#(string)", encoding: .utf8)"# + } else { + return data.customDumpDescription + } + } +} diff --git a/Tests/XXMessengerClientTests/TestHelpers/Message+stubs.swift b/Tests/XXMessengerClientTests/TestHelpers/TestDoubles.swift similarity index 80% rename from Tests/XXMessengerClientTests/TestHelpers/Message+stubs.swift rename to Tests/XXMessengerClientTests/TestHelpers/TestDoubles.swift index e10bea4a6298544c2e4c4d01ba267a00d2dcc167..c9a90bde352a9da4ac0c359c26fed8e1a89d9f8f 100644 --- a/Tests/XXMessengerClientTests/TestHelpers/Message+stubs.swift +++ b/Tests/XXMessengerClientTests/TestHelpers/TestDoubles.swift @@ -1,4 +1,5 @@ import XXClient +import XXMessengerClient extension Message { static func stub(_ stubId: Int) -> Message { @@ -16,3 +17,9 @@ extension Message { ) } } + +extension BackupParams { + static let stub = BackupParams( + username: "stub-username" + ) +} diff --git a/Tests/XXMessengerClientTests/Utils/BackupCallbackRegistryTests.swift b/Tests/XXMessengerClientTests/Utils/BackupCallbackRegistryTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..d3cba50cb81fe1efb2188985fed7ef9a8470290d --- /dev/null +++ b/Tests/XXMessengerClientTests/Utils/BackupCallbackRegistryTests.swift @@ -0,0 +1,43 @@ +import CustomDump +import XCTest +import XXClient +@testable import XXMessengerClient + +final class BackupCallbackRegistryTests: XCTestCase { + func testRegistry() { + var firstCallbackDidHandle: [Data] = [] + var secondCallbackDidHandle: [Data] = [] + + let firstCallback = UpdateBackupFunc { data in + firstCallbackDidHandle.append(data) + } + let secondCallback = UpdateBackupFunc { data in + secondCallbackDidHandle.append(data) + } + let callbackRegistry: BackupCallbacksRegistry = .live() + let registeredCallbacks = callbackRegistry.registered() + let firstCallbackCancellable = callbackRegistry.register(firstCallback) + let secondCallbackCancellable = callbackRegistry.register(secondCallback) + + let firstData = "1".data(using: .utf8)! + registeredCallbacks.handle(firstData) + + XCTAssertNoDifference(firstCallbackDidHandle, [firstData]) + XCTAssertNoDifference(secondCallbackDidHandle, [firstData]) + + firstCallbackCancellable.cancel() + let secondData = "2".data(using: .utf8)! + registeredCallbacks.handle(secondData) + + XCTAssertNoDifference(firstCallbackDidHandle, [firstData]) + XCTAssertNoDifference(secondCallbackDidHandle, [firstData, secondData]) + + secondCallbackCancellable.cancel() + + let thirdData = "3".data(using: .utf8)! + registeredCallbacks.handle(thirdData) + + XCTAssertNoDifference(firstCallbackDidHandle, [firstData]) + XCTAssertNoDifference(secondCallbackDidHandle, [firstData, secondData]) + } +} diff --git a/Tests/XXMessengerClientTests/Utils/BackupStorageTests.swift b/Tests/XXMessengerClientTests/Utils/BackupStorageTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..10da4a7c0eb62ae7647b3a52b506f6fcdf8fc57c --- /dev/null +++ b/Tests/XXMessengerClientTests/Utils/BackupStorageTests.swift @@ -0,0 +1,120 @@ +import CustomDump +import XCTest +@testable import XXMessengerClient + +final class BackupStorageTests: XCTestCase { + func testStorage() throws { + var actions: [Action]! + + var now: Date = .init(0) + 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 + ) + + XCTAssertNoDifference( + storage.stored(), + BackupStorage.Backup(date: fileDate, data: fileData) + ) + XCTAssertNoDifference(actions, [ + .didLoadFile(path), + .didGetModifiedTime(path), + ]) + + actions = [] + let observerA = storage.observe { backup in + actions.append(.didObserve("A", backup)) + } + + XCTAssertNoDifference(actions, []) + XCTAssertNoDifference( + storage.stored(), + BackupStorage.Backup(date: fileDate, data: fileData) + ) + + actions = [] + now = .init(1) + let data1 = "data-1".data(using: .utf8)! + try storage.store(data1) + + XCTAssertNoDifference( + storage.stored(), + BackupStorage.Backup(date: .init(1), data: data1) + ) + XCTAssertNoDifference(actions, [ + .didObserve("A", .init(date: .init(1), data: data1)), + .didSaveFile(path, data1), + ]) + + actions = [] + let observerB = storage.observe { backup in + actions.append(.didObserve("B", backup)) + } + + XCTAssertNoDifference(actions, []) + + actions = [] + now = .init(2) + observerA.cancel() + let data2 = "data-2".data(using: .utf8)! + try storage.store(data2) + + XCTAssertNoDifference(actions, [ + .didObserve("B", .init(date: .init(2), data: data2)), + .didSaveFile(path, data2), + ]) + + actions = [] + now = .init(3) + try storage.remove() + + XCTAssertNoDifference(actions, [ + .didObserve("B", nil), + .didRemoveItem(path), + ]) + + actions = [] + now = .init(4) + observerB.cancel() + let data3 = "data-3".data(using: .utf8)! + try storage.store(data3) + + XCTAssertNoDifference(actions, [ + .didSaveFile(path, data3), + ]) + } +} + +private extension Date { + init(_ timeIntervalSince1970: TimeInterval) { + 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) +} diff --git a/Tests/XXMessengerClientTests/Utils/LogMessageTests.swift b/Tests/XXMessengerClientTests/Utils/LogMessageTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..bbea8995439aec672f0ebddf778484cf7889f0ee --- /dev/null +++ b/Tests/XXMessengerClientTests/Utils/LogMessageTests.swift @@ -0,0 +1,71 @@ +import CustomDump +import XCTest +@testable import XXMessengerClient + +final class LogMessageTests: XCTestCase { + func testParsing() { + XCTAssertNoDifference( + LogMessage.parse("TRACE Tracing..."), + LogMessage(level: .trace, text: "Tracing...") + ) + XCTAssertNoDifference( + LogMessage.parse("DEBUG Debugging..."), + LogMessage(level: .debug, text: "Debugging...") + ) + XCTAssertNoDifference( + LogMessage.parse("INFO Informing..."), + LogMessage(level: .info, text: "Informing...") + ) + XCTAssertNoDifference( + LogMessage.parse("WARN Warning!"), + LogMessage(level: .warning, text: "Warning!") + ) + XCTAssertNoDifference( + LogMessage.parse("ERROR Failure!"), + LogMessage(level: .error, text: "Failure!") + ) + XCTAssertNoDifference( + LogMessage.parse("CRITICAL Critical failure!"), + LogMessage(level: .critical, text: "Critical failure!") + ) + XCTAssertNoDifference( + LogMessage.parse("FATAL Fatal failure!"), + LogMessage(level: .critical, text: "Fatal failure!") + ) + } + + func testParsingFallbacks() { + XCTAssertNoDifference( + LogMessage.parse("1234 Wrongly formatted"), + LogMessage(level: .notice, text: "1234 Wrongly formatted") + ) + } + + func testParsingStripsDateTime() { + XCTAssertNoDifference( + LogMessage.parse("INFO 2022/10/04 Informing..."), + LogMessage(level: .info, text: "Informing...") + ) + XCTAssertNoDifference( + LogMessage.parse("INFO 23:36:55.755390 Informing..."), + LogMessage(level: .info, text: "Informing...") + ) + XCTAssertNoDifference( + LogMessage.parse("INFO 2022/10/04 23:36:55.755390 Informing..."), + LogMessage(level: .info, text: "Informing...") + ) + } + + func testParsingMultilineMessage() { + XCTAssertNoDifference( + LogMessage.parse(""" + ERROR 2022/10/04 23:51:15.021658 First line + Second line + """), + LogMessage(level: .error, text: """ + First line + Second line + """) + ) + } +} diff --git a/Tests/XXMessengerClientTests/Utils/MessengerLoggerTests.swift b/Tests/XXMessengerClientTests/Utils/MessengerLoggerTests.swift new file mode 100644 index 0000000000000000000000000000000000000000..755863d99a7d26a35496256bb9be11b776952add --- /dev/null +++ b/Tests/XXMessengerClientTests/Utils/MessengerLoggerTests.swift @@ -0,0 +1,71 @@ +import CustomDump +import XCTest +@testable import XXMessengerClient + +final class MessengerLoggerTests: XCTestCase { + func testParsingLog() { + XCTAssertNoDifference( + MessengerLogger.Log.parse("TRACE Tracing..."), + MessengerLogger.Log(level: .trace, message: "Tracing...") + ) + XCTAssertNoDifference( + MessengerLogger.Log.parse("DEBUG Debugging..."), + MessengerLogger.Log(level: .debug, message: "Debugging...") + ) + XCTAssertNoDifference( + MessengerLogger.Log.parse("INFO Informing..."), + MessengerLogger.Log(level: .info, message: "Informing...") + ) + XCTAssertNoDifference( + MessengerLogger.Log.parse("WARN Warning!"), + MessengerLogger.Log(level: .warning, message: "Warning!") + ) + XCTAssertNoDifference( + MessengerLogger.Log.parse("ERROR Failure!"), + MessengerLogger.Log(level: .error, message: "Failure!") + ) + XCTAssertNoDifference( + MessengerLogger.Log.parse("CRITICAL Critical failure!"), + MessengerLogger.Log(level: .critical, message: "Critical failure!") + ) + XCTAssertNoDifference( + MessengerLogger.Log.parse("FATAL Fatal failure!"), + MessengerLogger.Log(level: .critical, message: "Fatal failure!") + ) + } + + func testParsingFallbacks() { + XCTAssertNoDifference( + MessengerLogger.Log.parse("1234 Wrongly formatted"), + MessengerLogger.Log(level: .notice, message: "1234 Wrongly formatted") + ) + } + + func testParsingStripsDateTime() { + XCTAssertNoDifference( + MessengerLogger.Log.parse("INFO 2022/10/04 Informing..."), + MessengerLogger.Log(level: .info, message: "Informing...") + ) + XCTAssertNoDifference( + MessengerLogger.Log.parse("INFO 23:36:55.755390 Informing..."), + MessengerLogger.Log(level: .info, message: "Informing...") + ) + XCTAssertNoDifference( + MessengerLogger.Log.parse("INFO 2022/10/04 23:36:55.755390 Informing..."), + MessengerLogger.Log(level: .info, message: "Informing...") + ) + } + + func testParsingMultilineMessage() { + XCTAssertNoDifference( + MessengerLogger.Log.parse(""" + ERROR 2022/10/04 23:51:15.021658 First line + Second line + """), + MessengerLogger.Log(level: .error, message: """ + First line + Second line + """) + ) + } +} diff --git a/run-tests.sh b/run-tests.sh index 1a87f661803af51200586db33dac341407cc7e78..d69dcac5dec6225c758182325019f63e42fe538d 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -4,17 +4,17 @@ set -e if [ "$1" = "macos" ]; then echo "\n\033[1;32mâ–¶ Running package tests on macOS...\033[0m" - set -o pipefail && swift test 2>&1 | ./xcbeautify + set -o pipefail && swift test | ./xcbeautify elif [ "$1" = "ios" ]; then echo "\n\033[1;32mâ–¶ Running package tests on iOS Simulator...\033[0m" - set -o pipefail && xcodebuild -scheme 'elixxir-dapps-sdk-swift-Package' -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=15.5,name=iPhone 13' test | ./xcbeautify + set -o pipefail && xcodebuild -scheme 'elixxir-dapps-sdk-swift-Package' -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=16.0,name=iPhone 14' test | ./xcbeautify elif [ "$1" = "examples-ios" ]; then echo "\n\033[1;32mâ–¶ Running XXMessenger example tests on iOS Simulator...\033[0m" - set -o pipefail && xcodebuild -workspace 'Examples/xx-messenger/XXMessenger.xcworkspace' -scheme 'XXMessenger' -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=15.5,name=iPhone 13' test | ./xcbeautify + set -o pipefail && xcodebuild -workspace 'Examples/xx-messenger/XXMessenger.xcworkspace' -scheme 'XXMessenger' -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=16.0,name=iPhone 14' test | ./xcbeautify else diff --git a/xcbeautify b/xcbeautify index c68c40e98dc4aff708435c6c159fa713ac74a58b..76b5df583a5d8f479e2088d4927ad90288c9d4cd 100755 Binary files a/xcbeautify and b/xcbeautify differ diff --git a/xcode-remove-caches.sh b/xcode-remove-caches.sh new file mode 100755 index 0000000000000000000000000000000000000000..4d24eab47daf55841f3152f75e371388188612af --- /dev/null +++ b/xcode-remove-caches.sh @@ -0,0 +1,6 @@ +pkill -int com.apple.CoreSimulator.CoreSimulatorService +killall Xcode +rm -rf "$(getconf DARWIN_USER_CACHE_DIR)/org.llvm.clang/ModuleCache" +rm -rf "$(getconf DARWIN_USER_CACHE_DIR)/org.llvm.clang.$(whoami)/ModuleCache" +rm -rf ~/Library/Developer/Xcode/DerivedData/* +rm -rf ~/Library/Caches/com.apple.dt.Xcode/*