Initialization refactored & covered by tests
This commit is contained in:
parent
88e31f6b14
commit
d0cca8121c
|
@ -102,6 +102,7 @@
|
|||
9E80B47227E4B34B008FF493 /* UserPreferencesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */; };
|
||||
9EAFEB84280597B700199FC9 /* WrappedSecItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB83280597B700199FC9 /* WrappedSecItem.swift */; };
|
||||
9EAFEB862805A23100199FC9 /* WrappedSecItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB852805A23100199FC9 /* WrappedSecItemTests.swift */; };
|
||||
9EAFEB822805793200199FC9 /* AppReducerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB812805793200199FC9 /* AppReducerTests.swift */; };
|
||||
9EBEF87A27CE369800B4F343 /* RecoveryPhraseTestPreambleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EBEF87927CE369800B4F343 /* RecoveryPhraseTestPreambleView.swift */; };
|
||||
9ECAE56827FC713C0089A0EF /* DatabaseFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9ECAE56727FC713C0089A0EF /* DatabaseFiles.swift */; };
|
||||
9EF8135C27ECC25E0075AF48 /* WalletStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF8135A27ECC25E0075AF48 /* WalletStorageTests.swift */; };
|
||||
|
@ -256,6 +257,7 @@
|
|||
9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreferencesStorage.swift; sourceTree = "<group>"; };
|
||||
9EAFEB83280597B700199FC9 /* WrappedSecItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedSecItem.swift; sourceTree = "<group>"; };
|
||||
9EAFEB852805A23100199FC9 /* WrappedSecItemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedSecItemTests.swift; sourceTree = "<group>"; };
|
||||
9EAFEB812805793200199FC9 /* AppReducerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReducerTests.swift; sourceTree = "<group>"; };
|
||||
9EBEF87927CE369800B4F343 /* RecoveryPhraseTestPreambleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseTestPreambleView.swift; sourceTree = "<group>"; };
|
||||
9ECAE56727FC713C0089A0EF /* DatabaseFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseFiles.swift; sourceTree = "<group>"; };
|
||||
9EF8135A27ECC25E0075AF48 /* WalletStorageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletStorageTests.swift; sourceTree = "<group>"; };
|
||||
|
@ -451,6 +453,7 @@
|
|||
0D4E7A1926B364180058B01E /* secantTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9EAFEB802805791400199FC9 /* AppReducer */,
|
||||
9EF8135927ECC25E0075AF48 /* Util */,
|
||||
0DFE93E4272CB6D0000FCCA5 /* RecoveryPhraseValidationTests */,
|
||||
0DFE93DD272C6D4B000FCCA5 /* BackupFlowTests */,
|
||||
|
@ -754,6 +757,14 @@
|
|||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9EAFEB802805791400199FC9 /* AppReducer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9EAFEB812805793200199FC9 /* AppReducerTests.swift */,
|
||||
);
|
||||
path = AppReducer;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9EBEF87827CE365D00B4F343 /* Preamble */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1242,6 +1253,7 @@
|
|||
6654C7442715A4AC00901167 /* OnboardingStoreTests.swift in Sources */,
|
||||
9EAFEB862805A23100199FC9 /* WrappedSecItemTests.swift in Sources */,
|
||||
0D1C1AA327611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift in Sources */,
|
||||
9EAFEB822805793200199FC9 /* AppReducerTests.swift in Sources */,
|
||||
0D4E7A1B26B364180058B01E /* secantTests.swift in Sources */,
|
||||
0DFE93E6272CB6F7000FCCA5 /* RecoveryPhraseValidationTests.swift in Sources */,
|
||||
9EF8135C27ECC25E0075AF48 /* WalletStorageTests.swift in Sources */,
|
||||
|
|
|
@ -30,6 +30,7 @@ enum AppAction: Equatable {
|
|||
case onboarding(OnboardingAction)
|
||||
case phraseDisplay(RecoveryPhraseDisplayAction)
|
||||
case phraseValidation(RecoveryPhraseValidationAction)
|
||||
case respondToWalletInitializationState(InitializationState)
|
||||
case updateRoute(AppState.Route)
|
||||
case welcome(WelcomeAction)
|
||||
}
|
||||
|
@ -78,59 +79,33 @@ extension AppReducer {
|
|||
|
||||
private static let appReducer = AppReducer { state, action, environment in
|
||||
switch action {
|
||||
case .createNewWallet:
|
||||
let randomPhraseWords: [String]
|
||||
do {
|
||||
let randomPhrase = try environment.mnemonicSeedPhraseProvider.randomMnemonic()
|
||||
randomPhraseWords = try environment.mnemonicSeedPhraseProvider.asWords(randomPhrase)
|
||||
// TODO: - Get birthday from the integrated SDK, issue 228 (https://github.com/zcash/secant-ios-wallet/issues/228)
|
||||
let birthday = BlockHeight(12345678)
|
||||
|
||||
try environment.walletStorage.importWallet(randomPhrase, birthday, .english, false)
|
||||
} catch {
|
||||
// TODO: - merge with issue 201 (https://github.com/zcash/secant-ios-wallet/issues/201) and its Error States
|
||||
return .none
|
||||
}
|
||||
case .appDelegate(.didFinishLaunching):
|
||||
/// We need to fetch data from keychain, in order to be 100% sure the kecyhain can be read we delay the check a bit
|
||||
return Effect(value: .checkWalletInitialization)
|
||||
.delay(for: 0.02, scheduler: environment.scheduler)
|
||||
.eraseToEffect()
|
||||
|
||||
let recoveryPhrase = RecoveryPhrase(words: randomPhraseWords)
|
||||
state.phraseDisplayState.phrase = recoveryPhrase
|
||||
state.phraseValidationState = RecoveryPhraseValidationState.random(phrase: recoveryPhrase)
|
||||
|
||||
return Effect(value: .phraseValidation(.displayBackedUpPhrase))
|
||||
|
||||
/// Checking presense of stored wallet in the keychain and presense of database files in documents directory.
|
||||
/// Evaluate the wallet's state based on keychain keys and database files presence
|
||||
case .checkWalletInitialization:
|
||||
var keysPresent = false
|
||||
do {
|
||||
// TODO: replace the hardcoded network with the environmental value, issue 239 (https://github.com/zcash/secant-ios-wallet/issues/239)
|
||||
keysPresent = try environment.walletStorage.areKeysPresent()
|
||||
let databaseFilesPresent = try environment.databaseFiles.areDbFilesPresentFor("mainnet")
|
||||
|
||||
switch (keysPresent, databaseFilesPresent) {
|
||||
case (false, false):
|
||||
state.appInitializationState = .uninitialized
|
||||
case (false, true):
|
||||
state.appInitializationState = .keysMissing
|
||||
// TODO: error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221)
|
||||
case (true, false), (true, true):
|
||||
return Effect(value: .initializeApp)
|
||||
}
|
||||
} catch DatabaseFiles.DatabaseFilesError.filesPresentCheck {
|
||||
if keysPresent {
|
||||
/// This state is not an error as long as wallet keys are present. The process to initialize the missing files is handled by `.initializeApp` action.
|
||||
state.appInitializationState = .filesMissing
|
||||
return Effect(value: .initializeApp)
|
||||
} else {
|
||||
state.appInitializationState = .uninitialized
|
||||
}
|
||||
} catch WalletStorage.WalletStorageError.uninitializedWallet {
|
||||
state.appInitializationState = .uninitialized
|
||||
} catch {
|
||||
state.appInitializationState = .failed
|
||||
// TODO: error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221)
|
||||
}
|
||||
let walletState = walletInitializationState(environment)
|
||||
return Effect(value: .respondToWalletInitializationState(walletState))
|
||||
|
||||
if state.appInitializationState == .uninitialized {
|
||||
/// Respond to all possible states of the wallet and initiate appropriate side effects including errors handling
|
||||
case .respondToWalletInitializationState(let walletState):
|
||||
switch walletState {
|
||||
case .failed:
|
||||
// TODO: error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221)
|
||||
state.appInitializationState = .failed
|
||||
case .initialized:
|
||||
return Effect(value: .initializeApp)
|
||||
case .keysMissing:
|
||||
// TODO: error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221)
|
||||
state.appInitializationState = .keysMissing
|
||||
case .filesMissing:
|
||||
state.appInitializationState = .filesMissing
|
||||
return Effect(value: .initializeApp)
|
||||
case .uninitialized:
|
||||
state.appInitializationState = .uninitialized
|
||||
return Effect(value: .updateRoute(.onboarding))
|
||||
.delay(for: 3, scheduler: environment.scheduler)
|
||||
.eraseToEffect()
|
||||
|
@ -179,6 +154,30 @@ extension AppReducer {
|
|||
.eraseToEffect()
|
||||
.cancellable(id: ListenerId(), cancelInFlight: true)
|
||||
|
||||
case .createNewWallet:
|
||||
do {
|
||||
// get the random english mnemonic
|
||||
let randomPhrase = try environment.mnemonicSeedPhraseProvider.randomMnemonic()
|
||||
let randomPhraseWords = try environment.mnemonicSeedPhraseProvider.asWords(randomPhrase)
|
||||
// TODO: - Get birthday from the integrated SDK, issue 228 (https://github.com/zcash/secant-ios-wallet/issues/228)
|
||||
// get the latest block height
|
||||
let birthday = BlockHeight(12345678)
|
||||
|
||||
// store the wallet to the keychain
|
||||
try environment.walletStorage.importWallet(randomPhrase, birthday, .english, false)
|
||||
|
||||
// start the backup phrase validation test
|
||||
let recoveryPhrase = RecoveryPhrase(words: randomPhraseWords)
|
||||
state.phraseDisplayState.phrase = recoveryPhrase
|
||||
state.phraseValidationState = RecoveryPhraseValidationState.random(phrase: recoveryPhrase)
|
||||
|
||||
return Effect(value: .phraseValidation(.displayBackedUpPhrase))
|
||||
} catch {
|
||||
// TODO: - merge with issue 201 (https://github.com/zcash/secant-ios-wallet/issues/201) and its Error States
|
||||
}
|
||||
|
||||
return .none
|
||||
|
||||
case .phraseValidation(.succeed):
|
||||
do {
|
||||
try environment.walletStorage.markUserPassedPhraseBackupTest()
|
||||
|
@ -278,6 +277,47 @@ extension AppReducer {
|
|||
)
|
||||
}
|
||||
|
||||
// MARK: - AppReducer Helper Functions
|
||||
|
||||
extension AppReducer {
|
||||
static func walletInitializationState(_ environment: AppEnvironment) -> InitializationState {
|
||||
var keysPresent = false
|
||||
do {
|
||||
keysPresent = try environment.walletStorage.areKeysPresent()
|
||||
// TODO: replace the hardcoded network with the environmental value, issue 239 (https://github.com/zcash/secant-ios-wallet/issues/239)
|
||||
let databaseFilesPresent = try environment.databaseFiles.areDbFilesPresentFor("mainnet")
|
||||
|
||||
switch (keysPresent, databaseFilesPresent) {
|
||||
case (false, false):
|
||||
return .uninitialized
|
||||
case (false, true):
|
||||
return .keysMissing
|
||||
case (true, false):
|
||||
return .filesMissing
|
||||
case (true, true):
|
||||
return .initialized
|
||||
}
|
||||
} catch DatabaseFiles.DatabaseFilesError.filesPresentCheck {
|
||||
if keysPresent {
|
||||
return .filesMissing
|
||||
}
|
||||
} catch WalletStorage.WalletStorageError.uninitializedWallet {
|
||||
do {
|
||||
// TODO: replace the hardcoded network with the environmental value, issue 239 (https://github.com/zcash/secant-ios-wallet/issues/239)
|
||||
_ = try environment.databaseFiles.areDbFilesPresentFor("mainnet")
|
||||
|
||||
return .keysMissing
|
||||
} catch {
|
||||
return .uninitialized
|
||||
}
|
||||
} catch {
|
||||
return .failed
|
||||
}
|
||||
|
||||
return .uninitialized
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AppStore
|
||||
|
||||
typealias AppStore = Store<AppState, AppAction>
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
//
|
||||
// AppReducerTests.swift
|
||||
// secantTests
|
||||
//
|
||||
// Created by Lukáš Korba on 12.04.2022.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import secant_testnet
|
||||
import ComposableArchitecture
|
||||
|
||||
class AppReducerTests: XCTestCase {
|
||||
static let testScheduler = DispatchQueue.test
|
||||
|
||||
let testEnvironment = AppEnvironment(
|
||||
databaseFiles: .throwing,
|
||||
scheduler: testScheduler.eraseToAnyScheduler(),
|
||||
mnemonicSeedPhraseProvider: .mock,
|
||||
walletStorage: .throwing
|
||||
)
|
||||
|
||||
func testWalletInitializationState_Uninitialized() throws {
|
||||
let uninitializedEnvironment = AppEnvironment(
|
||||
databaseFiles: .throwing,
|
||||
scheduler: DispatchQueue.test.eraseToAnyScheduler(),
|
||||
mnemonicSeedPhraseProvider: .mock,
|
||||
walletStorage: .throwing
|
||||
)
|
||||
|
||||
let walletState = AppReducer.walletInitializationState(uninitializedEnvironment)
|
||||
|
||||
XCTAssertEqual(walletState, .uninitialized)
|
||||
}
|
||||
|
||||
func testWalletInitializationState_KeysMissing() throws {
|
||||
let wfmMock = WrappedFileManager(
|
||||
url: { _, _, _, _ in URL(fileURLWithPath: "") },
|
||||
fileExists: { _ in return true },
|
||||
removeItem: { _ in }
|
||||
)
|
||||
|
||||
let keysMissingEnvironment = AppEnvironment(
|
||||
databaseFiles: .live(databaseFiles: DatabaseFiles(fileManager: wfmMock)),
|
||||
scheduler: Self.testScheduler.eraseToAnyScheduler(),
|
||||
mnemonicSeedPhraseProvider: .mock,
|
||||
walletStorage: .throwing
|
||||
)
|
||||
|
||||
let walletState = AppReducer.walletInitializationState(keysMissingEnvironment)
|
||||
|
||||
XCTAssertEqual(walletState, .keysMissing)
|
||||
}
|
||||
|
||||
// TODO: - Implement testWalletInitializationState_FilesMissing when WalletStorage mock is available, issue 231 (https://github.com/zcash/secant-ios-wallet/issues/231)
|
||||
|
||||
// TODO: - Implement testWalletInitializationState_Initialized when WalletStorage mock is available, issue 231 (https://github.com/zcash/secant-ios-wallet/issues/231)
|
||||
|
||||
func testRespondToWalletInitializationState_Uninitialized() throws {
|
||||
let store = TestStore(
|
||||
initialState: .placeholder,
|
||||
reducer: AppReducer.default,
|
||||
environment: testEnvironment
|
||||
)
|
||||
|
||||
store.send(.respondToWalletInitializationState(.uninitialized))
|
||||
|
||||
Self.testScheduler.advance(by: 3)
|
||||
|
||||
store.receive(.updateRoute(.onboarding)) {
|
||||
$0.route = .onboarding
|
||||
$0.appInitializationState = .uninitialized
|
||||
}
|
||||
}
|
||||
|
||||
func testRespondToWalletInitializationState_KeysMissing() throws {
|
||||
let store = TestStore(
|
||||
initialState: .placeholder,
|
||||
reducer: AppReducer.default,
|
||||
environment: testEnvironment
|
||||
)
|
||||
|
||||
store.send(.respondToWalletInitializationState(.keysMissing)) { state in
|
||||
state.appInitializationState = .keysMissing
|
||||
}
|
||||
}
|
||||
|
||||
func testRespondToWalletInitializationState_FilesMissing() throws {
|
||||
let store = TestStore(
|
||||
initialState: .placeholder,
|
||||
reducer: AppReducer.default,
|
||||
environment: testEnvironment
|
||||
)
|
||||
|
||||
store.send(.respondToWalletInitializationState(.filesMissing)) { state in
|
||||
state.appInitializationState = .filesMissing
|
||||
}
|
||||
|
||||
store.receive(.initializeApp) { state in
|
||||
// failed is expected because environment is throwing errors
|
||||
state.appInitializationState = .failed
|
||||
}
|
||||
}
|
||||
|
||||
func testRespondToWalletInitializationState_Initialized() throws {
|
||||
let store = TestStore(
|
||||
initialState: .placeholder,
|
||||
reducer: AppReducer.default,
|
||||
environment: testEnvironment
|
||||
)
|
||||
|
||||
store.send(.respondToWalletInitializationState(.initialized))
|
||||
|
||||
store.receive(.initializeApp) { state in
|
||||
// failed is expected because environment is throwing errors
|
||||
state.appInitializationState = .failed
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue