libeufin

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

commit e6ee5db0b3de1ffe2a3aebbdb46023610005aaa8
parent ffcab69a15266300b1dbe8fdd49f102c335b56f0
Author: MS <ms@taler.net>
Date:   Thu, 22 Dec 2022 15:16:03 +0100

Fix naming inconsistency.

"admin" is both the aministrator username
and the bank's main account label.  The latter
was "bank".

Diffstat:
Mnexus/src/test/kotlin/DownloadAndSubmit.kt | 4++--
Mnexus/src/test/kotlin/MakeEnv.kt | 16++++++++++++----
Mnexus/src/test/kotlin/SandboxAccessApiTest.kt | 63++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mnexus/src/test/kotlin/SandboxBankAccountTest.kt | 14+++++++-------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt | 10++--------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt | 51+++++++++++++++++++++++++++++++--------------------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt | 15++++++++++-----
Msandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt | 8+++++++-
8 files changed, 133 insertions(+), 48 deletions(-)

diff --git a/nexus/src/test/kotlin/DownloadAndSubmit.kt b/nexus/src/test/kotlin/DownloadAndSubmit.kt @@ -99,14 +99,14 @@ class DownloadAndSubmit { fun download() { withNexusAndSandboxUser { wireTransfer( - "bank", + "admin", "foo", "default", "Show up in logging!", "TESTKUDOS:1" ) wireTransfer( - "bank", + "admin", "foo", "default", "Exist in logging!", diff --git a/nexus/src/test/kotlin/MakeEnv.kt b/nexus/src/test/kotlin/MakeEnv.kt @@ -117,8 +117,8 @@ fun prepSandboxDb() { } BankAccountEntity.new { iban = BANK_IBAN - label = "bank" // used by the wire helper - owner = "bank" // used by the person name finder + label = "admin" // used by the wire helper + owner = "admin" // used by the person name finder // For now, the model assumes always one demobank this.demoBank = demoBank } @@ -190,12 +190,12 @@ fun withNexusAndSandboxUser(f: () -> Unit) { } } -// Creates tables and the default demobank. +// Creates tables, the default demobank, and admin's bank account. fun withSandboxTestDatabase(f: () -> Unit) { withTestDatabase { tech.libeufin.sandbox.dbCreateTables(TEST_DB_CONN) transaction { - DemobankConfigEntity.new { + val d = DemobankConfigEntity.new { currency = "TESTKUDOS" bankDebtLimit = 10000 usersDebtLimit = 1000 @@ -204,6 +204,14 @@ fun withSandboxTestDatabase(f: () -> Unit) { this.withSignupBonus = false captchaUrl = "http://example.com/" // unused } + // admin's bank account. + BankAccountEntity.new { + iban = BANK_IBAN + label = "admin" // used by the wire helper + owner = "admin" // used by the person name finder + // For now, the model assumes always one demobank + this.demoBank = d + } } f() } diff --git a/nexus/src/test/kotlin/SandboxAccessApiTest.kt b/nexus/src/test/kotlin/SandboxAccessApiTest.kt @@ -7,8 +7,9 @@ import io.ktor.http.* import io.ktor.server.testing.* import io.netty.handler.codec.http.HttpResponseStatus import kotlinx.coroutines.runBlocking +import org.jetbrains.exposed.sql.transactions.transaction import org.junit.Test -import tech.libeufin.sandbox.sandboxApp +import tech.libeufin.sandbox.* import tech.libeufin.util.buildBasicAuthLine class SandboxAccessApiTest { @@ -56,6 +57,66 @@ class SandboxAccessApiTest { } } + /** + * Tests that 'admin' and 'bank' are not possible to register + * and that after 'admin' logs in it gets access to the bank's + * main account. + */ + @Test + fun adminRegisterAndLoginTest() { + withTestDatabase { + prepSandboxDb() + withTestApplication(sandboxApp) { + runBlocking { + val registerAdmin = mapper.writeValueAsString(object { + val username = "admin" + val password = "y" + }) + val registerBank = mapper.writeValueAsString(object { + val username = "bank" + val password = "y" + }) + for (b in mutableListOf<String>(registerAdmin, registerBank)) { + val r = client.post<HttpResponse>( + urlString = "/demobanks/default/access-api/testing/register", + ) { + this.body = b + expectSuccess = false + headers { + append( + HttpHeaders.ContentType, + ContentType.Application.Json + ) + } + } + assert(r.status.value == HttpStatusCode.Forbidden.value) + } + // Set arbitrary balance to the bank. + wireTransfer( + "foo", + "admin", + "default", + "setting the balance", + "TESTKUDOS:99" + ) + // Get admin's balance. + val r = client.get<String>( + urlString = "/demobanks/default/access-api/accounts/admin", + ) { + expectSuccess = true + headers { + append( + HttpHeaders.Authorization, + buildBasicAuthLine("admin", "foo") + ) + } + } + println(r) + } + } + } + } + @Test fun registerTest() { // Test IBAN conflict detection. diff --git a/nexus/src/test/kotlin/SandboxBankAccountTest.kt b/nexus/src/test/kotlin/SandboxBankAccountTest.kt @@ -19,7 +19,7 @@ class SandboxBankAccountTest { withTestDatabase { prepSandboxDb() wireTransfer( - "bank", + "admin", "foo", "default", "Show up in logging!", @@ -30,22 +30,22 @@ class SandboxBankAccountTest { * the payment is still pending (= not booked), the pending * transactions must be included in the calculation. */ - var bankBalance = getBalance("bank", true) + var bankBalance = getBalance("admin", true) assert(bankBalance == parseDecimal("-1")) wireTransfer( "foo", - "bank", + "admin", "default", "Show up in logging!", "TESTKUDOS:5" ) - bankBalance = getBalance("bank", true) + bankBalance = getBalance("admin", true) assert(bankBalance == parseDecimal("4")) // Trigger Insufficient funds case for users. try { wireTransfer( "foo", - "bank", + "admin", "default", "Show up in logging!", "TESTKUDOS:5000" @@ -57,7 +57,7 @@ class SandboxBankAccountTest { // Trigger Insufficient funds case for the bank. try { wireTransfer( - "bank", + "admin", "foo", "default", "Show up in logging!", @@ -68,7 +68,7 @@ class SandboxBankAccountTest { assert(e.statusCode == HttpStatusCode.PreconditionFailed) } // Check balance didn't change for both parties. - bankBalance = getBalance("bank", true) + bankBalance = getBalance("admin", true) assert(bankBalance == parseDecimal("4")) val fooBalance = getBalance("foo", true) assert(fooBalance == parseDecimal("-4")) diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt @@ -378,14 +378,8 @@ object BankAccountsTable : IntIdTable() { val label = text("label").uniqueIndex("accountLabelIndex") /** * This field is the username of the customer that owns the - * bank account. Some actors do not have a customer registered, - * but they can still specify their "username" - merely a label - * that identifies their operations - to this field. - * - * Two examples of such actors are: "admin" and "bank". Note: - * "admin" cannot act as the bank, because it participates in - * tests and therefore should not have its balance affected by - * awarding sign-up bonuses. + * bank account. Admin is the only exception: that can specify + * this field as "admin" although no customer backs it. */ val owner = text("owner") val isPublic = bool("isPublic").default(false) diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt @@ -26,8 +26,6 @@ import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.transactions.transaction import tech.libeufin.util.* -import java.awt.Label -import java.math.BigDecimal import java.security.interfaces.RSAPublicKey import java.util.* import java.util.zip.DeflaterInputStream @@ -166,19 +164,23 @@ fun getCustomer(username: String): DemobankCustomerEntity { } /** - * Get person name from a customer's username. + * Get person name from a customer's username, or throw + * exception if not found. */ -fun getPersonNameFromCustomer(ownerUsername: String): String { - return when (ownerUsername) { - "admin" -> "admin" // Could be changed to Admin, or some different value. - "bank" -> "The Bank" +fun getPersonNameFromCustomer(customerUsername: String): String { + return when (customerUsername) { + "admin" -> "Admin" else -> transaction { val ownerCustomer = DemobankCustomerEntity.find( - DemobankCustomersTable.username eq ownerUsername - ).firstOrNull() ?: throw SandboxError( - HttpStatusCode.InternalServerError, - "'$ownerUsername' not a customer." - ) + DemobankCustomersTable.username eq customerUsername + ).firstOrNull() ?: run { + logger.error("Customer '${customerUsername}' not found, couldn't get their name.") + throw SandboxError( + HttpStatusCode.InternalServerError, + "'$customerUsername' not a customer." + ) + + } ownerCustomer.name ?: "Never given." } } @@ -227,17 +229,26 @@ fun getBankAccountFromIban(iban: String): BankAccountEntity { ) } +fun getBankAccountFromLabel(label: String, demobank: String = "default"): BankAccountEntity { + val maybeDemobank = getDemobank(demobank) + if (maybeDemobank == null) { + logger.error("Demobank '$demobank' not found") + throw SandboxError( + HttpStatusCode.NotFound, + "Demobank '$demobank' not found" + ) + } + return getBankAccountFromLabel(label, maybeDemobank) +} fun getBankAccountFromLabel(label: String, demobank: DemobankConfigEntity ): BankAccountEntity { - var labelCheck = label; - /** - * Admin is the only exception to the "username == bank account label" rule. - * Consider calling the default demobank's bank account directly "admin"? - */ - if (label == "admin") labelCheck = "bank" return transaction { - BankAccountEntity.find(BankAccountsTable.label eq labelCheck and (BankAccountsTable.demoBank eq demobank.id)).firstOrNull() ?: throw SandboxError( + BankAccountEntity.find( + BankAccountsTable.label eq label and ( + BankAccountsTable.demoBank eq demobank.id + ) + ).firstOrNull() ?: throw SandboxError( HttpStatusCode.NotFound, "Did not find a bank account for label $label" ) @@ -255,7 +266,7 @@ fun getBankAccountFromSubscriber(subscriber: EbicsSubscriberEntity): BankAccount fun BankAccountEntity.bonus(amount: String) { wireTransfer( - "bank", + "admin", this.label, this.demoBank.name, "Sign-up bonus", diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt @@ -151,7 +151,9 @@ class Config : CliktCommand( execThrowableOrTerminate { dbCreateTables(dbConnString) transaction { - val maybeDemobank = BankAccountEntity.find(BankAccountsTable.label eq "bank").firstOrNull() + val maybeDemobank = BankAccountEntity.find( + BankAccountsTable.label eq "admin" + ).firstOrNull() if (showOption) { if (maybeDemobank != null) { val ret = ObjectMapper() @@ -184,8 +186,8 @@ class Config : CliktCommand( } BankAccountEntity.new { iban = getIban() - label = "bank" - owner = "bank" // Not backed by an actual customer object. + label = "admin" + owner = "admin" // Not backed by an actual customer object. // For now, the model assumes always one demobank this.demoBank = demoBank } @@ -682,7 +684,7 @@ val sandboxApp: Application.() -> Unit = { } // Book one incoming payment for the requesting account. - // The debtor is not required to have an account at this Sandbox. + // The debtor is not required to have a customer account at this Sandbox. post("/admin/bank-accounts/{label}/simulate-incoming-transaction") { call.request.basicAuth(onlyAdmin = true) val body = call.receiveJson<IncomingPaymentInfo>() @@ -1396,7 +1398,10 @@ val sandboxApp: Application.() -> Unit = { val paytoUri = buildIbanPaytoUri( iban = bankAccount.iban, bic = bankAccount.bic, - receiverName = getPersonNameFromCustomer(username ?: "Not given.") + // username 'null' should only happen when auth is disabled. + receiverName = getPersonNameFromCustomer( + username ?: "Not given." + ) ) val iban = bankAccount.iban }) diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt @@ -60,6 +60,12 @@ fun getBalance(accountLabel: String, withPending: Boolean = false): BigDecimal { return getBalance(account, withPending) } +/** + * 'debitAccount' and 'creditAccount' are customer usernames + * and ALSO labels of the bank accounts owned by them. They are + * used to both resort a bank account and the legal name owning + * the bank accounts. + */ fun wireTransfer( debitAccount: String, creditAccount: String, @@ -108,7 +114,7 @@ fun wireTransfer( throw badRequest("Won't wire transfer with currency: ${checkAmount.currency}") // Check funds are sufficient. val pendingBalance = getBalance(debitAccount, withPending = true) - val maxDebt = if (debitAccount.label == "bank") { + val maxDebt = if (debitAccount.label == "admin") { demobank.bankDebtLimit } else demobank.usersDebtLimit if ((pendingBalance - checkAmount.amount).abs() > BigDecimal.valueOf(maxDebt.toLong())) {