diff options
Diffstat (limited to 'bank/src/test/kotlin/CoreBankApiTest.kt')
-rw-r--r-- | bank/src/test/kotlin/CoreBankApiTest.kt | 740 |
1 files changed, 429 insertions, 311 deletions
diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt index ca538da4..86115e78 100644 --- a/bank/src/test/kotlin/CoreBankApiTest.kt +++ b/bank/src/test/kotlin/CoreBankApiTest.kt @@ -183,13 +183,13 @@ class CoreBankAccountsApiTest { // Check idempotency with payto client.post("/accounts") { json(req) { - "internal_payto_uri" to payto + "payto_uri" to payto } }.assertOk() // Check payto conflict client.post("/accounts") { json(req) { - "internal_payto_uri" to genIbanPaytoUri() + "payto_uri" to genIbanPaytoUri() } }.assertConflict(TalerErrorCode.BANK_REGISTER_USERNAME_REUSE) } @@ -201,7 +201,7 @@ class CoreBankAccountsApiTest { "password" to "password" "name" to "Jane" "is_public" to true - "internal_payto_uri" to ibanPayto + "payto_uri" to ibanPayto "is_taler_exchange" to true } // Check Ok @@ -215,7 +215,7 @@ class CoreBankAccountsApiTest { json(req) }.assertOk() - // Check debit_threshold + // Check admin only debit_threshold obj { "username" to "bat" "password" to "password" @@ -231,6 +231,38 @@ class CoreBankAccountsApiTest { }.assertOk() } + // Check admin only tan_channel + obj { + "username" to "bat2" + "password" to "password" + "name" to "Bat" + "contact_data" to obj { + "phone" to "+456" + } + "tan_channel" to "sms" + }.let { req -> + client.post("/accounts") { + json(req) + }.assertErr(TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL) + client.post("/accounts") { + json(req) + pwAuth("admin") + }.assertOk() + } + + // Check tan info + for (channel in listOf("sms", "email")) { + client.post("/accounts") { + pwAuth("admin") + json { + "username" to "bat2" + "password" to "password" + "name" to "Bat" + "tan_channel" to channel + } + }.assertErr(TalerErrorCode.BANK_MISSING_TAN_INFO) + } + // Reserved account RESERVED_ACCOUNTS.forEach { client.post("/accounts") { @@ -317,13 +349,24 @@ class CoreBankAccountsApiTest { }.assertOk() } - // DELETE /accounts/USERNAME + // Test admin-only account creation @Test - fun delete() = bankSetup { _ -> - // Unknown account - client.delete("/accounts/unknown") { + fun createTanErr() = bankSetup(conf = "test_tan_err.conf") { _ -> + client.post("/accounts") { pwAuth("admin") - }.assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) + json { + "username" to "baz" + "password" to "xyz" + "name" to "Mallory" + "tan_channel" to "email" + } + }.assertConflict(TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED) + } + + // DELETE /accounts/USERNAME + @Test + fun delete() = bankSetup { db -> + authRoutine(HttpMethod.Delete, "/accounts/merchant", allowAdmin = true) // Reserved account RESERVED_ACCOUNTS.forEach { @@ -333,31 +376,42 @@ class CoreBankAccountsApiTest { } client.deleteA("/accounts/exchange") .assertConflict(TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT) - - // successful deletion + client.post("/accounts") { json { "username" to "john" - "password" to "password" - "name" to "John Smith" + "password" to "john-password" + "name" to "John" + "payto_uri" to genTmpPayTo() } }.assertOk() - client.delete("/accounts/john") { - pwAuth("admin") - }.assertNoContent() - // Trying again must yield 404 + fillTanInfo("john") + // Fail to delete, due to a non-zero balance. + tx("customer", "KUDOS:1", "john") + client.deleteA("/accounts/john") + .assertConflict(TalerErrorCode.BANK_ACCOUNT_BALANCE_NOT_ZERO) + // Sucessful deletion + tx("john", "KUDOS:1", "customer") + // TODO remove with gc + db.conn { conn -> + val id = conn.prepareStatement("SELECT bank_account_id FROM bank_accounts JOIN customers ON customer_id=owning_customer_id WHERE login = ?").run { + setString(1, "john") + oneOrNull { + it.getLong(1) + }!! + } + conn.prepareStatement("DELETE FROM bank_account_transactions WHERE bank_account_id=?").run { + setLong(1, id) + execute() + } + } + client.deleteA("/accounts/john") + .assertChallenge() + .assertNoContent() + // Account no longer exists client.delete("/accounts/john") { pwAuth("admin") }.assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) - - - // fail to delete, due to a non-zero balance. - tx("customer", "KUDOS:1", "merchant") - client.deleteA("/accounts/merchant") - .assertConflict(TalerErrorCode.BANK_ACCOUNT_BALANCE_NOT_ZERO) - tx("merchant", "KUDOS:1", "customer") - client.deleteA("/accounts/merchant") - .assertNoContent() } // Test admin-only account deletion @@ -399,35 +453,39 @@ class CoreBankAccountsApiTest { // PATCH /accounts/USERNAME @Test fun reconfig() = bankSetup { _ -> - authRoutine(HttpMethod.Patch, "/accounts/merchant", withAdmin = true) + authRoutine(HttpMethod.Patch, "/accounts/merchant", allowAdmin = true) + + // Check tan info + for (channel in listOf("sms", "email")) { + client.patchA("/accounts/merchant") { + json { "tan_channel" to channel } + }.assertErr(TalerErrorCode.BANK_MISSING_TAN_INFO) + } - // Successful attempt now. + // Successful attempt now val cashout = IbanPayTo(genIbanPaytoUri()) val req = obj { "cashout_payto_uri" to cashout.canonical "name" to "Roger" - "is_public" to true + "is_public" to true + "contact_data" to obj { + "phone" to "+99" + "email" to "foo@example.com" + } } client.patchA("/accounts/merchant") { json(req) }.assertNoContent() - // Checking idempotence. + // Checking idempotence client.patchA("/accounts/merchant") { json(req) }.assertNoContent() + checkAdminOnly( obj(req) { "debit_threshold" to "KUDOS:100" }, TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT ) - checkAdminOnly( - obj(req) { "contact_data" to obj { "phone" to "+99" } }, - TalerErrorCode.BANK_NON_ADMIN_PATCH_CONTACT - ) - checkAdminOnly( - obj(req) { "contact_data" to obj { "email" to "foo@example.com" } }, - TalerErrorCode.BANK_NON_ADMIN_PATCH_CONTACT - ) // Check currency client.patch("/accounts/merchant") { @@ -466,6 +524,19 @@ class CoreBankAccountsApiTest { "is_public" to true } }.assertConflict(TalerErrorCode.END) + + // Check 2FA + fillTanInfo("merchant") + client.patchA("/accounts/merchant") { + json { "is_public" to false } + }.assertChallenge { _, _ -> + client.getA("/accounts/merchant").assertOkJson<AccountData> { obj -> + assert(obj.is_public) + } + }.assertNoContent(); + client.getA("/accounts/merchant").assertOkJson<AccountData> { obj -> + assert(!obj.is_public) + } } // Test admin-only account patch @@ -492,10 +563,21 @@ class CoreBankAccountsApiTest { } } + // Test TAN check account patch + @Test + fun patchTanErr() = bankSetup(conf = "test_tan_err.conf") { _ -> + // Check unsupported TAN channel + client.patchA("/accounts/customer") { + json { + "tan_channel" to "email" + } + }.assertConflict(TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED) + } + // PATCH /accounts/USERNAME/auth @Test fun passwordChange() = bankSetup { _ -> - authRoutine(HttpMethod.Patch, "/accounts/merchant/auth", withAdmin = true) + authRoutine(HttpMethod.Patch, "/accounts/merchant/auth", allowAdmin = true) // Changing the password. client.patch("/accounts/customer/auth") { @@ -540,6 +622,21 @@ class CoreBankAccountsApiTest { client.patch("/accounts/customer/auth") { pwAuth("admin") json { + "new_password" to "customer-password" + } + }.assertNoContent() + + // Check 2FA + fillTanInfo("customer") + client.patchA("/accounts/customer/auth") { + json { + "old_password" to "customer-password" + "new_password" to "it-password" + } + }.assertChallenge().assertNoContent() + client.patch("/accounts/customer/auth") { + pwAuth("admin") + json { "new_password" to "new-password" } }.assertNoContent() @@ -578,7 +675,7 @@ class CoreBankAccountsApiTest { val obj = json<PublicAccountsResponse>() assertEquals(3, obj.public_accounts.size) obj.public_accounts.forEach { - assertEquals(0, it.account_name.toInt() % 2) + assertEquals(0, it.username.toInt() % 2) } } // All accounts @@ -610,7 +707,7 @@ class CoreBankAccountsApiTest { // GET /accounts/USERNAME @Test fun get() = bankSetup { _ -> - authRoutine(HttpMethod.Get, "/accounts/merchant", withAdmin = true) + authRoutine(HttpMethod.Get, "/accounts/merchant", allowAdmin = true) // Check ok client.getA("/accounts/merchant").assertOkJson<AccountData> { assertEquals("Merchant", it.name) @@ -770,8 +867,7 @@ class CoreBankTransactionsApiTest { repeat(2) { tx("merchant", "KUDOS:3", "customer") } - client.post("/accounts/merchant/transactions") { - pwAuth("merchant") + client.postA("/accounts/merchant/transactions") { json { "payto_uri" to "$customerPayto?message=payout2&amount=KUDOS:5" } @@ -789,9 +885,9 @@ class CoreBankTransactionsApiTest { assertBalance("exchange", "+KUDOS:0") tx("merchant", "KUDOS:1", "exchange", "") // Bounce common to transaction tx("merchant", "KUDOS:1", "exchange", "Malformed") // Bounce malformed transaction - val reserve_pub = randEddsaPublicKey(); - tx("merchant", "KUDOS:1", "exchange", randIncomingSubject(reserve_pub)) // Accept incoming - tx("merchant", "KUDOS:1", "exchange", randIncomingSubject(reserve_pub)) // Bounce reserve_pub reuse + val reservePub = randEddsaPublicKey(); + tx("merchant", "KUDOS:1", "exchange", randIncomingSubject(reservePub)) // Accept incoming + tx("merchant", "KUDOS:1", "exchange", randIncomingSubject(reservePub)) // Bounce reserve_pub reuse assertBalance("merchant", "-KUDOS:1") assertBalance("exchange", "+KUDOS:1") @@ -806,6 +902,22 @@ class CoreBankTransactionsApiTest { tx("exchange", "KUDOS:1", "merchant", randOutgoingSubject(wtid, exchange)) // Warn wtid reuse assertBalance("merchant", "+KUDOS:3") assertBalance("exchange", "-KUDOS:3") + + // Check 2fa + fillTanInfo("merchant") + assertBalance("merchant", "+KUDOS:3") + assertBalance("customer", "+KUDOS:0") + client.postA("/accounts/merchant/transactions") { + json { + "payto_uri" to "$customerPayto?message=tan+check&amount=KUDOS:1" + } + }.assertChallenge { _,_-> + assertBalance("merchant", "+KUDOS:3") + assertBalance("customer", "+KUDOS:0") + }.assertOkJson <TransactionCreateResponse> { + assertBalance("merchant", "+KUDOS:2") + assertBalance("customer", "+KUDOS:1") + } } } @@ -842,11 +954,7 @@ class CoreBankWithdrawalApiTest { client.get("/withdrawals/${it.withdrawal_id}") { pwAuth("merchant") }.assertOkJson<WithdrawalPublicInfo> { - assert(!it.selection_done) - assert(!it.aborted) - assert(!it.confirmation_done) assertEquals(amount, it.amount) - // TODO check all status } } @@ -864,7 +972,7 @@ class CoreBankWithdrawalApiTest { // POST /accounts/USERNAME/withdrawals/withdrawal_id/confirm @Test fun confirm() = bankSetup { _ -> - // TODO auth routine + authRoutine(HttpMethod.Post, "/accounts/merchant/withdrawals/42/confirm") // Check confirm created client.postA("/accounts/merchant/withdrawals") { json { "amount" to "KUDOS:1" } @@ -895,7 +1003,7 @@ class CoreBankWithdrawalApiTest { }.assertOkJson<BankAccountCreateWithdrawalResponse> { val uuid = it.taler_withdraw_uri.split("/").last() withdrawalSelect(uuid) - client.postA("/accounts/merchant/withdrawals/$uuid/abort").assertNoContent() + client.postA("/taler-integration/withdrawal-operation/$uuid/abort").assertNoContent() // Check error client.postA("/accounts/merchant/withdrawals/$uuid/confirm") @@ -915,7 +1023,19 @@ class CoreBankWithdrawalApiTest { .assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) // Check can abort because not confirmed - client.postA("/accounts/merchant/withdrawals/$uuid/abort").assertNoContent() + client.postA("/taler-integration/withdrawal-operation/$uuid/abort").assertNoContent() + } + + // Check confirm another user's operation + client.postA("/accounts/customer/withdrawals") { + json { "amount" to "KUDOS:1" } + }.assertOkJson<BankAccountCreateWithdrawalResponse> { + val uuid = it.taler_withdraw_uri.split("/").last() + withdrawalSelect(uuid) + + // Check error + client.postA("/accounts/merchant/withdrawals/$uuid/confirm") + .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) } // Check bad UUID @@ -924,6 +1044,21 @@ class CoreBankWithdrawalApiTest { // Check unknown client.postA("/accounts/merchant/withdrawals/${UUID.randomUUID()}/confirm") .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) + + // Check 2fa + fillTanInfo("merchant") + assertBalance("merchant", "-KUDOS:6") + client.postA("/accounts/merchant/withdrawals") { + json { "amount" to "KUDOS:1" } + }.assertOkJson<BankAccountCreateWithdrawalResponse> { + val uuid = it.taler_withdraw_uri.split("/").last() + withdrawalSelect(uuid) + + client.postA("/accounts/merchant/withdrawals/$uuid/confirm") + .assertChallenge { _,_-> + assertBalance("merchant", "-KUDOS:6") + }.assertNoContent() + } } } @@ -940,40 +1075,17 @@ class CoreBankCashoutApiTest { "amount_credit" to convert("KUDOS:1") } - // Check missing TAN info + // Missing info client.postA("/accounts/customer/cashouts") { json(req) - }.assertConflict(TalerErrorCode.BANK_MISSING_TAN_INFO) - client.patch("/accounts/customer") { - pwAuth("admin") - json { - "contact_data" to obj { - "phone" to "+99" - "email" to "foo@example.com" - } - } - }.assertNoContent() + }.assertConflict(TalerErrorCode.BANK_CONFIRM_INCOMPLETE) - // Check email TAN error - client.postA("/accounts/customer/cashouts") { - json(req) { - "tan_channel" to "email" - } - }.assertStatus(HttpStatusCode.BadGateway, TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED) + fillCashoutInfo("customer") // Check OK client.postA("/accounts/customer/cashouts") { json(req) - }.assertOkJson<CashoutPending> { first -> - smsCode("+99") - // Check idempotency - client.postA("/accounts/customer/cashouts") { - json(req) - }.assertOkJson<CashoutPending> { second -> - assertEquals(first.cashout_id, second.cashout_id) - assertNull(smsCode("+99")) - } - } + }.assertOkJson<CashoutResponse>() // Trigger conflict due to reused request_uid client.postA("/accounts/customer/cashouts") { @@ -991,6 +1103,7 @@ class CoreBankCashoutApiTest { // Check insufficient fund client.postA("/accounts/customer/cashouts") { json(req) { + "request_uid" to randShortHashCode() "amount_debit" to "KUDOS:75" "amount_credit" to convert("KUDOS:75") } @@ -1014,211 +1127,19 @@ class CoreBankCashoutApiTest { "amount_credit" to "KUDOS:1" } }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) - } - - // POST /accounts/{USERNAME}/cashouts - @Test - fun createNoTan() = bankSetup("test_no_tan.conf") { _ -> - val req = obj { - "request_uid" to randShortHashCode() - "amount_debit" to "KUDOS:1" - "amount_credit" to convert("KUDOS:1") - } - - fillCashoutInfo("customer") - - // Check unsupported TAN channel - client.postA("/accounts/customer/cashouts") { - json(req) - }.assertStatus(HttpStatusCode.NotImplemented, TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED) - } - - // POST /accounts/{USERNAME}/cashouts/{CASHOUT_ID}/abort - @Test - fun abort() = bankSetup { _ -> - authRoutine(HttpMethod.Post, "/accounts/merchant/cashouts/42/abort") - - fillCashoutInfo("customer") - - val req = obj { - "request_uid" to randShortHashCode() - "amount_debit" to "KUDOS:1" - "amount_credit" to convert("KUDOS:1") - } - - // Check abort created - client.postA("/accounts/customer/cashouts") { - json(req) - }.assertOkJson<CashoutPending> { - val id = it.cashout_id - - // Check OK - client.postA("/accounts/customer/cashouts/$id/abort") - .assertNoContent() - // Check idempotence - client.postA("/accounts/customer/cashouts/$id/abort") - .assertNoContent() - } - // Check abort confirmed + // Check 2fa + fillTanInfo("customer") + assertBalance("customer", "-KUDOS:1") client.postA("/accounts/customer/cashouts") { - json(req) { "request_uid" to randShortHashCode() } - }.assertOkJson<CashoutPending> { - val id = it.cashout_id - - client.postA("/accounts/customer/cashouts/$id/confirm") { - json { "tan" to smsCode("+99") } - }.assertNoContent() - - // Check error - client.postA("/accounts/customer/cashouts/$id/abort") - .assertConflict(TalerErrorCode.BANK_ABORT_CONFIRM_CONFLICT) - } - - // Check bad id - client.postA("/accounts/customer/cashouts/chocolate/abort") { - json { "tan" to "code" } - }.assertBadRequest() - - // Check unknown - client.postA("/accounts/customer/cashouts/42/abort") { - json { "tan" to "code" } - }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - - // Check abort another user's operation - client.postA("/accounts/customer/cashouts") { - json(req) { "request_uid" to randShortHashCode() } - }.assertOkJson<CashoutPending> { - val id = it.cashout_id - - // Check error - client.postA("/accounts/merchant/cashouts/$id/abort") - .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - } - } - - // POST /accounts/{USERNAME}/cashouts/{CASHOUT_ID}/confirm - @Test - fun confirm() = bankSetup { _ -> - authRoutine(HttpMethod.Post, "/accounts/merchant/cashouts/42/confirm") - - client.patch("/accounts/customer") { - pwAuth("admin") - json { - "contact_data" to obj { - "phone" to "+99" - } - } - }.assertNoContent() - - val req = obj { - "request_uid" to randShortHashCode() - "amount_debit" to "KUDOS:1" - "amount_credit" to convert("KUDOS:1") - } - - // Check confirm - client.postA("/accounts/customer/cashouts") { - json(req) { "request_uid" to randShortHashCode() } - }.assertOkJson<CashoutPending> { - val id = it.cashout_id - - // Check missing cashout address - client.postA("/accounts/customer/cashouts/$id/confirm") { - json { "tan" to "code" } - }.assertConflict(TalerErrorCode.BANK_CONFIRM_INCOMPLETE) - fillCashoutInfo("customer") - - // Check bad TAN code - client.postA("/accounts/customer/cashouts/$id/confirm") { - json { "tan" to "nice-try" } - }.assertConflict(TalerErrorCode.BANK_TAN_CHALLENGE_FAILED) - - val code = smsCode("+99") - - // Check OK - client.postA("/accounts/customer/cashouts/$id/confirm") { - json { "tan" to code } - }.assertNoContent() - // Check idempotence - client.postA("/accounts/customer/cashouts/$id/confirm") { - json { "tan" to code } - }.assertNoContent() - } - - // Check confirm another user's operation - client.postA("/accounts/customer/cashouts") { - json(req) { - "request_uid" to randShortHashCode() - "amount_credit" to convert("KUDOS:1") - } - }.assertOkJson<CashoutPending> { - val id = it.cashout_id - - // Check error - client.postA("/accounts/merchant/cashouts/$id/confirm") { - json { "tan" to "unused" } - }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - } - - // Check bad conversion - client.postA("/accounts/customer/cashouts") { - json(req) { "request_uid" to randShortHashCode() } - }.assertOkJson<CashoutPending> { - val id = it.cashout_id - client.post("/conversion-info/conversion-rate") { - pwAuth("admin") - json { - "cashin_ratio" to "1" - "cashin_fee" to "KUDOS:0.1" - "cashin_tiny_amount" to "KUDOS:0.0001" - "cashin_rounding_mode" to "nearest" - "cashin_min_amount" to "EUR:0.0001" - "cashout_ratio" to "1" - "cashout_fee" to "EUR:0.1" - "cashout_tiny_amount" to "EUR:0.0001" - "cashout_rounding_mode" to "nearest" - "cashout_min_amount" to "KUDOS:0.0001" - } - }.assertNoContent() - - client.postA("/accounts/customer/cashouts/$id/confirm"){ - json { "tan" to smsCode("+99") } - }.assertConflict(TalerErrorCode.BANK_BAD_CONVERSION) - - // Check can abort because not confirmed - client.postA("/accounts/customer/cashouts/$id/abort") - .assertNoContent() - } - - // Check balance insufficient - client.postA("/accounts/customer/cashouts") { - json(req) { + json(req) { "request_uid" to randShortHashCode() - "amount_credit" to convert("KUDOS:1") } - }.assertOkJson<CashoutPending> { - val id = it.cashout_id - // Send too much money - tx("customer", "KUDOS:9", "merchant") - client.postA("/accounts/customer/cashouts/$id/confirm"){ - json { "tan" to smsCode("+99") } - }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) - - // Check can abort because not confirmed - client.postA("/accounts/customer/cashouts/$id/abort") - .assertNoContent() + }.assertChallenge { _,_-> + assertBalance("customer", "-KUDOS:1") + }.assertOkJson<CashoutResponse> { + assertBalance("customer", "-KUDOS:2") } - - // Check bad UUID - client.postA("/accounts/customer/cashouts/chocolate/confirm") { - json { "tan" to "code" } - }.assertBadRequest() - - // Check unknown - client.postA("/accounts/customer/cashouts/42/confirm") { - json { "tan" to "code" } - }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) } // GET /accounts/{USERNAME}/cashouts/{CASHOUT_ID} @@ -1237,41 +1158,15 @@ class CoreBankCashoutApiTest { // Check confirm client.postA("/accounts/customer/cashouts") { json(req) { "request_uid" to randShortHashCode() } - }.assertOkJson<CashoutPending> { + }.assertOkJson<CashoutResponse> { val id = it.cashout_id client.getA("/accounts/customer/cashouts/$id") .assertOkJson<CashoutStatusResponse> { - assertEquals(CashoutStatus.pending, it.status) + assertEquals(CashoutStatus.confirmed, it.status) assertEquals(amountDebit, it.amount_debit) assertEquals(amountCredit, it.amount_credit) - assertEquals(TanChannel.sms, it.tan_channel) - assertEquals("+99", it.tan_info) - } - - client.postA("/accounts/customer/cashouts/$id/confirm") { - json { "tan" to smsCode("+99") } - }.assertNoContent() - client.getA("/accounts/customer/cashouts/$id") - .assertOkJson<CashoutStatusResponse> { - assertEquals(CashoutStatus.confirmed, it.status) - } - } - - // Check abort - client.postA("/accounts/customer/cashouts") { - json(req) { "request_uid" to randShortHashCode() } - }.assertOkJson<CashoutPending> { - val id = it.cashout_id - client.getA("/accounts/customer/cashouts/$id") - .assertOkJson<CashoutStatusResponse> { - assertEquals(CashoutStatus.pending, it.status) - } - - client.postA("/accounts/customer/cashouts/$id/abort") - .assertNoContent() - client.getA("/accounts/customer/cashouts/$id") - .assertOkJson<CashoutStatusResponse> { - assertEquals(CashoutStatus.aborted, it.status) + assertNull(it.tan_channel) + assertNull(it.tan_info) } } @@ -1286,7 +1181,7 @@ class CoreBankCashoutApiTest { // Check get another user's operation client.postA("/accounts/customer/cashouts") { json(req) { "request_uid" to randShortHashCode() } - }.assertOkJson<CashoutPending> { + }.assertOkJson<CashoutResponse> { val id = it.cashout_id // Check error @@ -1325,4 +1220,227 @@ class CoreBankCashoutApiTest { client.get("/accounts/customer/cashouts") .assertNotImplemented() } +} + +class CoreBankTanApiTest { + // POST /accounts/{USERNAME}/challenge/{challenge_id} + @Test + fun send() = bankSetup { _ -> + authRoutine(HttpMethod.Post, "/accounts/merchant/challenge/42") + + suspend fun HttpResponse.expectChallenge(channel: TanChannel, info: String): HttpResponse { + return assertChallenge { tanChannel, tanInfo -> + assertEquals(channel, tanChannel) + assertEquals(info, tanInfo) + } + } + + suspend fun HttpResponse.expectTransmission(channel: TanChannel, info: String) { + this.assertOkJson<TanTransmission> { + assertEquals(it.tan_channel, channel) + assertEquals(it.tan_info, info) + } + } + + // Set up 2fa + client.patchA("/accounts/merchant") { + json { + "contact_data" to obj { + "phone" to "+99" + "email" to "email@example.com" + } + "tan_channel" to "sms" + } + }.expectChallenge(TanChannel.sms, "+99") + .assertNoContent() + + // Update 2fa settings - first 2FA challenge then new tan channel check + client.patchA("/accounts/merchant") { + json { // Info change + "contact_data" to obj { "phone" to "+98" } + } + }.expectChallenge(TanChannel.sms, "+99") + .expectChallenge(TanChannel.sms, "+98") + .assertNoContent() + client.patchA("/accounts/merchant") { + json { // Channel change + "tan_channel" to "email" + } + }.expectChallenge(TanChannel.sms, "+98") + .expectChallenge(TanChannel.email, "email@example.com") + .assertNoContent() + client.patchA("/accounts/merchant") { + json { // Both change + "contact_data" to obj { "phone" to "+97" } + "tan_channel" to "sms" + } + }.expectChallenge(TanChannel.email, "email@example.com") + .expectChallenge(TanChannel.sms, "+97") + .assertNoContent() + + // Disable 2fa + client.patchA("/accounts/merchant") { + json { "tan_channel" to null as String? } + }.expectChallenge(TanChannel.sms, "+97") + .assertNoContent() + + // Admin has no 2FA + client.patch("/accounts/merchant") { + pwAuth("admin") + json { + "contact_data" to obj { "phone" to "+99" } + "tan_channel" to "sms" + } + }.assertNoContent() + client.patch("/accounts/merchant") { + pwAuth("admin") + json { "tan_channel" to "email" } + }.assertNoContent() + client.patch("/accounts/merchant") { + pwAuth("admin") + json { "tan_channel" to null as String? } + }.assertNoContent() + + // Check retry and invalidate + client.patchA("/accounts/merchant") { + json { + "contact_data" to obj { "phone" to "+88" } + "tan_channel" to "sms" + } + }.assertChallenge().assertNoContent() + client.patchA("/accounts/merchant") { + json { "is_public" to false } + }.assertAcceptedJson<TanChallenge> { + // Check ok + client.postA("/accounts/merchant/challenge/${it.challenge_id}") + .expectTransmission(TanChannel.sms, "+88") + assertNotNull(tanCode("+88")) + // Check retry + client.postA("/accounts/merchant/challenge/${it.challenge_id}") + .expectTransmission(TanChannel.sms, "+88") + assertNull(tanCode("+88")) + // Idempotent patch does nothing + client.patchA("/accounts/merchant") { + json { + "contact_data" to obj { "phone" to "+88" } + "tan_channel" to "sms" + } + } + client.postA("/accounts/merchant/challenge/${it.challenge_id}") + .expectTransmission(TanChannel.sms, "+88") + assertNull(tanCode("+88")) + // Change 2fa settings + client.patchA("/accounts/merchant") { + json { + "tan_channel" to "email" + } + }.expectChallenge(TanChannel.sms, "+88") + .expectChallenge(TanChannel.email, "email@example.com") + .assertNoContent() + // Check invalidated + client.postA("/accounts/merchant/challenge/${it.challenge_id}") + .expectTransmission(TanChannel.email, "email@example.com") + assertNotNull(tanCode("email@example.com")) + } + + // Unknown challenge + client.postA("/accounts/merchant/challenge/42") + .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) + } + + // POST /accounts/{USERNAME}/challenge/{challenge_id} + @Test + fun sendTanErr() = bankSetup("test_tan_err.conf") { _ -> + // Check fail + client.patch("/accounts/merchant") { + pwAuth("admin") + json { + "contact_data" to obj { "phone" to "+1234" } + "tan_channel" to "sms" + } + }.assertNoContent() + client.patchA("/accounts/merchant") { + json { "is_public" to false } + }.assertAcceptedJson<TanChallenge> { + client.postA("/accounts/merchant/challenge/${it.challenge_id}") + .assertStatus(HttpStatusCode.BadGateway, TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED) + } + } + + // POST /accounts/{USERNAME}/challenge/{challenge_id}/confirm + @Test + fun confirm() = bankSetup { _ -> + authRoutine(HttpMethod.Post, "/accounts/merchant/challenge/42/confirm") + + fillTanInfo("merchant") + + // Check simple case + client.patchA("/accounts/merchant") { + json { "is_public" to false } + }.assertAcceptedJson<TanChallenge> { + val id = it.challenge_id + val info = client.postA("/accounts/merchant/challenge/$id") + .assertOkJson<TanTransmission>().tan_info + val code = tanCode(info) + + // Check bad TAN code + client.postA("/accounts/merchant/challenge/$id/confirm") { + json { "tan" to "nice-try" } + }.assertConflict(TalerErrorCode.BANK_TAN_CHALLENGE_FAILED) + + // Check wrong account + client.postA("/accounts/customer/challenge/$id/confirm") { + json { "tan" to "nice-try" } + }.assertNotFound(TalerErrorCode.BANK_CHALLENGE_NOT_FOUND) + + // Check OK + client.postA("/accounts/merchant/challenge/$id/confirm") { + json { "tan" to code } + }.assertNoContent() + // Check idempotence + client.postA("/accounts/merchant/challenge/$id/confirm") { + json { "tan" to code } + }.assertNoContent() + + // Unknown challenge + client.postA("/accounts/merchant/challenge/42/confirm") { + json { "tan" to code } + }.assertNotFound(TalerErrorCode.BANK_CHALLENGE_NOT_FOUND) + } + + // Check invalidation + client.patchA("/accounts/merchant") { + json { "is_public" to true } + }.assertAcceptedJson<TanChallenge> { + val id = it.challenge_id + val info = client.postA("/accounts/merchant/challenge/$id") + .assertOkJson<TanTransmission>().tan_info + + // Check invalidated + fillTanInfo("merchant") + client.postA("/accounts/merchant/challenge/$id/confirm") { + json { "tan" to tanCode(info) } + }.assertConflict(TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED) + + val new = client.postA("/accounts/merchant/challenge/$id") + .assertOkJson<TanTransmission>().tan_info + val code = tanCode(new) + // Idempotent patch does nothing + client.patchA("/accounts/merchant") { + json { + "contact_data" to obj { "phone" to "+88" } + "tan_channel" to "sms" + } + } + client.postA("/accounts/merchant/challenge/$id/confirm") { + json { "tan" to code } + }.assertNoContent() + + // Solved challenge remain solved + fillTanInfo("merchant") + client.postA("/accounts/merchant/challenge/$id/confirm") { + json { "tan" to code } + }.assertNoContent() + } + } }
\ No newline at end of file |