libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit a657794c0861a0000d073193328d810e2f6b0f8e
parent 3a4c4b61ef871788252b7a258aa94aa82db9670f
Author: Antoine A <>
Date:   Tue, 26 Dec 2023 16:14:27 +0000

Improve testing and error

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt | 11++++++-----
Mbank/src/main/kotlin/tech/libeufin/bank/Error.kt | 7+++++++
Mbank/src/main/kotlin/tech/libeufin/bank/Main.kt | 2+-
Mbank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt | 2+-
Mbank/src/test/kotlin/CoreBankApiTest.kt | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
5 files changed, 130 insertions(+), 19 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt @@ -200,6 +200,10 @@ suspend fun patchAccount(db: Database, ctx: BankConfig, req: AccountReconfigurat "'admin' account cannot be public", TalerErrorCode.END ) + + if (req.tan_channel is Option.Some && req.tan_channel.value != null && ctx.tanChannels.get(req.tan_channel.value ) == null) { + throw unsupportedTanChannel(req.tan_channel.value) + } return db.account.reconfig( login = username, @@ -707,11 +711,8 @@ private fun Routing.coreBankTanApi(db: Database, ctx: BankConfig) { ) is TanSendResult.Success -> { res.tanCode?.run { - val tanScript = ctx.tanChannels.get(res.tanChannel) ?: throw libeufinError( - HttpStatusCode.NotImplemented, - "Unsupported tan channel ${res.tanChannel}", - TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED - ) + val tanScript = ctx.tanChannels.get(res.tanChannel) + ?: throw unsupportedTanChannel(res.tanChannel) val exitValue = withContext(Dispatchers.IO) { val process = ProcessBuilder(tanScript, res.tanInfo).start() try { diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Error.kt b/bank/src/main/kotlin/tech/libeufin/bank/Error.kt @@ -145,4 +145,11 @@ fun unknownAccount(id: String): LibeufinException { "Account '$id' not found", TalerErrorCode.BANK_UNKNOWN_ACCOUNT ) +} + +fun unsupportedTanChannel(channel: TanChannel): LibeufinException { + return conflict( + "Unsupported tan channel $channel", + TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED + ) } \ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -386,7 +386,7 @@ class EditAccount : CliktCommand( cashout_payto_uri = Option.Some(cashout_payto_uri), debit_threshold = debit_threshold ) - when (patchAccount(db, ctx, req, username, true)) { + when (patchAccount(db, ctx, req, username, true, false)) { AccountPatchResult.Success -> logger.info("Account '$username' edited") AccountPatchResult.UnknownAccount -> diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt @@ -274,7 +274,7 @@ class AccountDAO(private val db: Database) { it.getBoolean("phone_change") -> return@transaction AccountPatchResult.NonAdminContact it.getBoolean("email_change") -> return@transaction AccountPatchResult.NonAdminContact it.getBoolean("missing_tan_info") -> return@transaction AccountPatchResult.MissingTanInfo - it.getBoolean("tan_required") && !is2fa -> return@transaction AccountPatchResult.TanRequired + it.getBoolean("tan_required") && !is2fa && !isAdmin -> return@transaction AccountPatchResult.TanRequired else -> it.getLong("customer_id") } } diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt @@ -416,18 +416,11 @@ class CoreBankAccountsApiTest { }.assertNoContent() // Check tan info - client.patchA("/accounts/merchant") { - json { - "tan_channel" to "sms" - "email" to "mail@test.com" - } - }.assertErr(TalerErrorCode.BANK_MISSING_TAN_INFO) - client.patchA("/accounts/merchant") { - json { - "tan_channel" to "email" - "phone" to "+99" - } - }.assertErr(TalerErrorCode.BANK_MISSING_TAN_INFO) + for (channel in listOf("sms", "email")) { + client.patchA("/accounts/merchant") { + json { "tan_channel" to channel } + }.assertErr(TalerErrorCode.BANK_MISSING_TAN_INFO) + } checkAdminOnly( obj(req) { "debit_threshold" to "KUDOS:100" }, @@ -527,6 +520,17 @@ class CoreBankAccountsApiTest { } } + // Test TAN check account patch + @Test + fun patchNoTan() = bankSetup(conf = "test_no_tan.conf") { _ -> + // Check unsupported TAN channel + client.patchA("/accounts/customer") { + json { + "tan_channel" to "sms" + } + }.assertConflict(TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED) + } + // PATCH /accounts/USERNAME/auth @Test fun passwordChange() = bankSetup { _ -> @@ -1360,4 +1364,103 @@ class CoreBankCashoutApiTest { client.get("/accounts/customer/cashouts") .assertNotImplemented() } +} + +class CoreBankTanApiTest { + // POST /accounts/{USERNAME}/challenge/{challenge_id} + @Test + fun create() = bankSetup { _ -> + authRoutine(HttpMethod.Post, "/accounts/merchant/challenge/42") + + client.patch("/accounts/merchant") { + pwAuth("admin") + json { + "contact_data" to obj { + "phone" to "+99" + } + "tan_channel" to "sms" + } + }.assertNoContent() + client.patchA("/accounts/merchant") { + json { "is_public" to false } + }.assertAcceptedJson<TanChallenge> { + // Check ok + client.postA("/accounts/merchant/challenge/${it.challenge_id}") + .assertOkJson<TanTransmission> { + assertEquals(it.tan_info, "+99") + assertEquals(it.tan_channel.name, "sms") + } + // Check retry + client.postA("/accounts/merchant/challenge/${it.challenge_id}") + .assertOkJson<TanTransmission> { + assertEquals(it.tan_info, "+99") + assertEquals(it.tan_channel.name, "sms") + } + } + + // Check fail + client.patch("/accounts/merchant") { + pwAuth("admin") + json { + "contact_data" to obj { + "email" to "test@test.com" + } + "tan_channel" to "email" + } + }.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) + } + + // TODO check what happens when tan info or tan channel change + + // Unknown challenge + client.postA("/accounts/merchant/challenge/42") + .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) + } + + // POST /accounts/{USERNAME}/challenge/{challenge_id}/confirm + @Test + fun confirm() = bankSetup { _ -> + authRoutine(HttpMethod.Post, "/accounts/merchant/challenge/42/confirm") + + client.patch("/accounts/merchant") { + pwAuth("admin") + json { + "contact_data" to obj { + "phone" to "+99" + } + "tan_channel" to "sms" + } + }.assertNoContent() + val id = client.patchA("/accounts/merchant") { + json { "is_public" to false } + }.assertAcceptedJson<TanChallenge>().challenge_id + client.postA("/accounts/merchant/challenge/$id").assertOk() + val code = smsCode("+99") + + // Check bad TAN code + client.postA("/accounts/merchant/challenge/$id/confirm") { + json { "tan" to "nice-try" } + }.assertConflict(TalerErrorCode.BANK_TAN_CHALLENGE_FAILED) + + // 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() + + // TODO check what happens when tan info or tan channel change + + // Unknown challenge + client.postA("/accounts/merchant/challenge/42/confirm") { + json { "tan" to code } + }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) + } } \ No newline at end of file