Initialization refactored & covered by tests

This commit is contained in:
Lukas Korba 2022-04-12 13:12:07 +02:00
parent 88e31f6b14
commit d0cca8121c
3 changed files with 220 additions and 50 deletions

View File

@ -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 */,

View File

@ -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)
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()
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
}
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")
let walletState = walletInitializationState(environment)
return Effect(value: .respondToWalletInitializationState(walletState))
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
/// 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)
}
if state.appInitializationState == .uninitialized {
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>

View File

@ -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
}
}
}