secant-ios-wallet/secantTests/SendTests/SendTests.swift

548 lines
23 KiB
Swift

//
// SendTests.swift
// secantTests
//
// Created by Lukáš Korba on 02.05.2022.
//
import XCTest
@testable import secant_testnet
import ComposableArchitecture
import ZcashLightClientKit
// swiftlint:disable type_body_length
class SendTests: XCTestCase {
var storage = WalletStorage(secItem: .live)
let usNumberFormatter = NumberFormatter()
override func setUp() {
super.setUp()
storage.zcashStoredWalletPrefix = "test_send_"
storage.deleteData(forKey: WalletStorage.Constants.zcashStoredWallet)
usNumberFormatter.maximumFractionDigits = 8
usNumberFormatter.maximumIntegerDigits = 8
usNumberFormatter.numberStyle = .decimal
usNumberFormatter.usesGroupingSeparator = true
usNumberFormatter.locale = Locale(identifier: "en_US")
}
func testSendSucceeded() throws {
// the test needs to pass the exportWallet() so we simulate some in the keychain
try storage.importWallet(bip39: "one two three", birthday: nil)
// setup the store and environment to be fully mocked
let testScheduler = DispatchQueue.test
let testEnvironment = SendFlowEnvironment(
derivationTool: .live(),
mnemonic: .mock,
numberFormatter: .live(),
SDKSynchronizer: MockWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: storage)
)
let store = TestStore(
initialState: .placeholder,
reducer: SendFlowReducer.default,
environment: testEnvironment
)
// simulate the sending confirmation button to be pressed
store.send(.sendConfirmationPressed) { state in
// once sending is confirmed, the attemts to try to send again by pressing the button
// needs to be eliminated, indicated by the flag `isSendingTransaction`, need to be true
state.isSendingTransaction = true
}
testScheduler.advance(by: 0.01)
let transactionState = TransactionState(
expirationHeight: 40,
memo: "test",
minedHeight: 50,
shielded: true,
zAddress: "tteafadlamnelkqe",
date: Date.init(timeIntervalSince1970: 1234567),
id: "id",
status: .paid(success: true),
subtitle: "sub",
zecAmount: Zatoshi(amount: 10)
)
// check the success transaction to be received back
store.receive(.sendTransactionResult(Result.success(transactionState))) { state in
// from this moment on the sending next transaction is allowed again
// the 'isSendingTransaction' needs to be false again
state.isSendingTransaction = false
}
// all went well, the success screen is triggered
store.receive(.updateRoute(.success)) { state in
state.route = .success
}
}
func testSendFailed() throws {
// the test needs to pass the exportWallet() so we simulate some in the keychain
try storage.importWallet(bip39: "one two three", birthday: nil)
// setup the store and environment to be fully mocked
let testScheduler = DispatchQueue.test
let testEnvironment = SendFlowEnvironment(
derivationTool: .live(),
mnemonic: .mock,
numberFormatter: .live(),
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: storage)
)
let store = TestStore(
initialState: .placeholder,
reducer: SendFlowReducer.default,
environment: testEnvironment
)
// simulate the sending confirmation button to be pressed
store.send(.sendConfirmationPressed) { state in
// once sending is confirmed, the attemts to try to send again by pressing the button
// needs to be eliminated, indicated by the flag `isSendingTransaction`, need to be true
state.isSendingTransaction = true
}
testScheduler.advance(by: 0.01)
// check the failure transaction to be received back
store.receive(.sendTransactionResult(Result.failure(SynchronizerError.criticalError as NSError))) { state in
// from this moment on the sending next transaction is allowed again
// the 'isSendingTransaction' needs to be false again
state.isSendingTransaction = false
}
// the failure screen is triggered as expected
store.receive(.updateRoute(.failure)) { state in
state.route = .failure
}
}
func testAddressValidation() throws {
let testScheduler = DispatchQueue.test
let testEnvironment = SendFlowEnvironment(
derivationTool: .live(),
mnemonic: .mock,
numberFormatter: .live(),
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
)
let store = TestStore(
initialState: .placeholder,
reducer: SendFlowReducer.default,
environment: testEnvironment
)
store.send(.transactionAddressInput(.textField(.set("3HRG769ii3HDSJV5vNknQPzXqtL2mTSGnr")))) { state in
state.transactionAddressInputState.textFieldState.text = "3HRG769ii3HDSJV5vNknQPzXqtL2mTSGnr"
// true is expected here because textField doesn't have any `validationType: String.ValidationType?`
// isValid function returns true, `guard let validationType = validationType else { return true }`
state.transactionAddressInputState.textFieldState.valid = true
state.transactionAddressInputState.isValidAddress = false
XCTAssertTrue(
state.isInvalidAddressFormat,
"Send Tests: `testAddressValidation` is expected to be true but it's \(state.isInvalidAddressFormat)"
)
}
store.send(.transactionAddressInput(.textField(.set("t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po")))) { state in
state.transactionAddressInputState.textFieldState.text = "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po"
// true is expected here because textField doesn't have any `validationType: String.ValidationType?`
// isValid function returns true, `guard let validationType = validationType else { return true }`
state.transactionAddressInputState.textFieldState.valid = true
state.transactionAddressInputState.isValidAddress = true
XCTAssertFalse(
state.isInvalidAddressFormat,
"Send Tests: `testAddressValidation` is expected to be false but it's \(state.isInvalidAddressFormat)"
)
}
}
func testInvalidAmountFormatEmptyInput() throws {
let testScheduler = DispatchQueue.test
let testEnvironment = SendFlowEnvironment(
derivationTool: .live(),
mnemonic: .mock,
numberFormatter: .live(),
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
)
let store = TestStore(
initialState: .placeholder,
reducer: SendFlowReducer.default,
environment: testEnvironment
)
// Checks the computed property `isInvalidAmountFormat` which controls the error message to be shown on the screen
// With empty input it must be false
store.send(.transactionAmountInput(.textField(.set(""))))
store.receive(.transactionAmountInput(.updateAmount))
}
func testInvalidAddressFormatEmptyInput() throws {
let testScheduler = DispatchQueue.test
let testEnvironment = SendFlowEnvironment(
derivationTool: .live(),
mnemonic: .mock,
numberFormatter: .live(),
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
)
let store = TestStore(
initialState: .placeholder,
reducer: SendFlowReducer.default,
environment: testEnvironment
)
// Checks the computed property `isInvalidAddressFormat` which controls the error message to be shown on the screen
// With empty input it must be false
store.send(.transactionAddressInput(.textField(.set("")))) { state in
state.transactionAddressInputState.textFieldState.text = ""
// true is expected here because textField doesn't have any `validationType: String.ValidationType?`
// isValid function returns true, `guard let validationType = validationType else { return true }`
state.transactionAddressInputState.textFieldState.valid = true
XCTAssertFalse(
state.isInvalidAddressFormat,
"Send Tests: `testInvalidAddressFormatEmptyInput` is expected to be false but it's \(state.isInvalidAddressFormat)"
)
}
}
func testFundsSufficiency() throws {
let sendState = SendFlowState(
transaction: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState:
TransactionAmountTextFieldState(
currencySelectionState: CurrencySelectionState(),
maxValue: 501_300,
textFieldState:
TCATextFieldState(
validationType: .customFloatingPoint(usNumberFormatter),
text: ""
)
)
)
let testScheduler = DispatchQueue.test
let testEnvironment = SendFlowEnvironment(
derivationTool: .live(),
mnemonic: .mock,
numberFormatter: .live(numberFormatter: usNumberFormatter),
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
)
let store = TestStore(
initialState: sendState,
reducer: SendFlowReducer.default,
environment: testEnvironment
)
store.send(.transactionAmountInput(.textField(.set("0.00501299")))) { state in
state.transactionAmountInputState.textFieldState.text = "0.00501299"
state.transactionAmountInputState.textFieldState.valid = true
XCTAssertFalse(
state.isInsufficientFunds,
"Send Tests: `testFundsSufficiency` is expected to be false but it's \(state.isInsufficientFunds)"
)
}
store.receive(.transactionAmountInput(.updateAmount)) { state in
state.transactionAmountInputState.amount = 501_299
}
store.send(.transactionAmountInput(.textField(.set("0.00501301")))) { state in
state.transactionAmountInputState.textFieldState.text = "0.00501301"
state.transactionAmountInputState.textFieldState.valid = true
XCTAssertFalse(
state.isInsufficientFunds,
"Send Tests: `testFundsSufficiency` is expected to be false but it's \(state.isInsufficientFunds)"
)
}
store.receive(.transactionAmountInput(.updateAmount)) { state in
state.transactionAmountInputState.amount = 501_301
XCTAssertTrue(
state.isInsufficientFunds,
"Send Tests: `testFundsSufficiency` is expected to be true but it's \(state.isInsufficientFunds)"
)
}
}
func testDifferentAmountFormats() throws {
let testScheduler = DispatchQueue.test
let testEnvironment = SendFlowEnvironment(
derivationTool: .live(),
mnemonic: .mock,
numberFormatter: .live(numberFormatter: usNumberFormatter),
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
)
let store = TestStore(
initialState: .init(
route: nil,
transaction: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState:
TransactionAmountTextFieldState(
currencySelectionState: CurrencySelectionState(),
textFieldState:
TCATextFieldState(
validationType: .customFloatingPoint(usNumberFormatter),
text: ""
)
)
),
reducer: SendFlowReducer.default,
environment: testEnvironment
)
try amountFormatTest("1.234", true, 123_400_000, false, store)
try amountFormatTest("1,234", true, 123_400_000_000, false, store)
try amountFormatTest("1 234", true, 123_400_000_000, true, store)
try amountFormatTest("1,234.567", true, 123_456_700_000, false, store)
try amountFormatTest("1.", true, 100_000_000, false, store)
try amountFormatTest("1..", false, 0, false, store)
try amountFormatTest("1,.", false, 0, true, store)
try amountFormatTest("1.,", false, 0, true, store)
try amountFormatTest("1,,", false, 0, true, store)
try amountFormatTest("1,23", false, 0, true, store)
try amountFormatTest("1 23", false, 0, true, store)
try amountFormatTest("1.2.3", false, 0, true, store)
}
func testValidForm() throws {
let sendState = SendFlowState(
transaction: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState:
TransactionAmountTextFieldState(
amount: 501_301,
currencySelectionState: CurrencySelectionState(),
maxValue: 501_302,
textFieldState:
TCATextFieldState(
validationType: .customFloatingPoint(usNumberFormatter),
text: "0.00501301"
)
)
)
let testScheduler = DispatchQueue.test
let testEnvironment = SendFlowEnvironment(
derivationTool: .live(),
mnemonic: .mock,
numberFormatter: .live(),
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
)
let store = TestStore(
initialState: sendState,
reducer: SendFlowReducer.default,
environment: testEnvironment
)
store.send(.transactionAddressInput(.textField(.set("t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po")))) { state in
state.transactionAddressInputState.textFieldState.text = "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po"
// true is expected here because textField doesn't have any `validationType: String.ValidationType?`
// isValid function returns true, `guard let validationType = validationType else { return true }`
state.transactionAddressInputState.textFieldState.valid = true
state.transactionAddressInputState.isValidAddress = true
XCTAssertTrue(
state.isValidForm,
"Send Tests: `testValidForm` is expected to be true but it's \(state.isValidForm)"
)
}
}
func testInvalidForm_InsufficientFunds() throws {
let sendState = SendFlowState(
transaction: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState:
TransactionAmountTextFieldState(
currencySelectionState: CurrencySelectionState(),
maxValue: 501_300,
textFieldState:
TCATextFieldState(
validationType: .floatingPoint,
text: "0.00501301"
)
)
)
let testScheduler = DispatchQueue.test
let testEnvironment = SendFlowEnvironment(
derivationTool: .live(),
mnemonic: .mock,
numberFormatter: .live(),
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
)
let store = TestStore(
initialState: sendState,
reducer: SendFlowReducer.default,
environment: testEnvironment
)
store.send(.transactionAddressInput(.textField(.set("t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po")))) { state in
state.transactionAddressInputState.textFieldState.text = "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po"
// true is expected here because textField doesn't have any `validationType: String.ValidationType?`
// isValid function returns true, `guard let validationType = validationType else { return true }`
state.transactionAddressInputState.textFieldState.valid = true
state.transactionAddressInputState.isValidAddress = true
XCTAssertFalse(
state.isValidForm,
"Send Tests: `testValidForm` is expected to be false but it's \(state.isValidForm)"
)
}
}
func testInvalidForm_AddressFormat() throws {
let sendState = SendFlowState(
transaction: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState:
TransactionAmountTextFieldState(
currencySelectionState: CurrencySelectionState(),
maxValue: 501_302,
textFieldState:
TCATextFieldState(
validationType: .floatingPoint,
text: "0.00501301"
)
)
)
let testScheduler = DispatchQueue.test
let testEnvironment = SendFlowEnvironment(
derivationTool: .live(),
mnemonic: .mock,
numberFormatter: .live(),
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
)
let store = TestStore(
initialState: sendState,
reducer: SendFlowReducer.default,
environment: testEnvironment
)
store.send(.transactionAddressInput(.textField(.set("3HRG769ii3HDSJV5vNknQPzXqtL2mTSGnr")))) { state in
state.transactionAddressInputState.textFieldState.text = "3HRG769ii3HDSJV5vNknQPzXqtL2mTSGnr"
// true is expected here because textField doesn't have any `validationType: String.ValidationType?`
// isValid function returns true, `guard let validationType = validationType else { return true }`
state.transactionAddressInputState.textFieldState.valid = true
state.transactionAddressInputState.isValidAddress = false
XCTAssertFalse(
state.isValidForm,
"Send Tests: `testValidForm` is expected to be false but it's \(state.isValidForm)"
)
}
}
func testInvalidForm_AmountFormat() throws {
let sendState = SendFlowState(
transaction: .placeholder,
transactionAddressInputState: .placeholder,
transactionAmountInputState:
TransactionAmountTextFieldState(
currencySelectionState: CurrencySelectionState(),
maxValue: 501_302,
textFieldState:
TCATextFieldState(
validationType: .floatingPoint,
text: "0.0.0501301"
)
)
)
let testScheduler = DispatchQueue.test
let testEnvironment = SendFlowEnvironment(
derivationTool: .live(),
mnemonic: .mock,
numberFormatter: .live(),
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .live(walletStorage: WalletStorage(secItem: .live))
)
let store = TestStore(
initialState: sendState,
reducer: SendFlowReducer.default,
environment: testEnvironment
)
store.send(.transactionAddressInput(.textField(.set("t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po")))) { state in
state.transactionAddressInputState.textFieldState.text = "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po"
// true is expected here because textField doesn't have any `validationType: String.ValidationType?`
// isValid function returns true, `guard let validationType = validationType else { return true }`
state.transactionAddressInputState.textFieldState.valid = true
state.transactionAddressInputState.isValidAddress = true
XCTAssertFalse(
state.isValidForm,
"Send Tests: `testValidForm` is expected to be false but it's \(state.isValidForm)"
)
}
}
}
private extension SendTests {
func amountFormatTest(
_ amount: String,
_ expectedValidationResult: Bool,
_ expectedAmount: Int64,
_ expectedToReceive: Bool,
_ store: TestStore<SendFlowState, SendFlowState, SendFlowAction, SendFlowAction, SendFlowEnvironment>
) throws {
store.send(.transactionAmountInput(.textField(.set(amount)))) { state in
state.transactionAmountInputState.textFieldState.text = amount
state.transactionAmountInputState.textFieldState.valid = expectedValidationResult
}
if expectedToReceive {
store.receive(.transactionAmountInput(.updateAmount))
} else {
store.receive(.transactionAmountInput(.updateAmount)) { state in
state.transactionAmountInputState.amount = expectedAmount
}
}
}
}