libeufin

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

commit f487bce708c2bd688642767ecbcbaf96439acda0
parent 149a0fabab3346f850f4f16b49bc0d3e46ca51d6
Author: ms <ms@taler.net>
Date:   Fri, 22 Oct 2021 20:47:50 +0200

Default demobank policy.

All the endpoints outside of the /demobank trunk got
their bank accounts and transactions associated to the
default demobank.  To be tested

Diffstat:
Msandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt | 17+++++------------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt | 140+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt | 85++-----------------------------------------------------------------------------
4 files changed, 145 insertions(+), 161 deletions(-)

diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt @@ -19,6 +19,7 @@ package tech.libeufin.sandbox +import io.ktor.http.* import org.jetbrains.exposed.dao.Entity import org.jetbrains.exposed.dao.EntityClass import org.jetbrains.exposed.dao.IntEntity @@ -326,6 +327,7 @@ object BankAccountTransactionsTable : LongIdTable() { * only both parties to be registered at the running Sandbox. */ val account = reference("account", BankAccountsTable) + val demobank = reference("demobank", DemobankConfigsTable) } class BankAccountTransactionEntity(id: EntityID<Long>) : LongEntity(id) { @@ -352,6 +354,7 @@ class BankAccountTransactionEntity(id: EntityID<Long>) : LongEntity(id) { var pmtInfId by BankAccountTransactionsTable.pmtInfId var direction by BankAccountTransactionsTable.direction var account by BankAccountEntity referencedOn BankAccountTransactionsTable.account + var demobank by DemobankConfigEntity referencedOn BankAccountsTable.demoBank } /** @@ -362,7 +365,6 @@ object BankAccountsTable : IntIdTable() { val iban = text("iban") val bic = text("bic").default("EUSANDBOX") val label = text("label").uniqueIndex("accountLabelIndex") - val currency = text("currency") val isDebit = bool("isDebit").default(false) /** * Allow to assign "admin" - who doesn't have a customer DB entry - @@ -370,15 +372,7 @@ object BankAccountsTable : IntIdTable() { */ val owner = text("owner") val isPublic = bool("isPublic").default(false) - - /** - * Used only by the operations triggered under one /demobanks/$demobankId endpoint. - * - * For example, current tests do never configure one demobank or one customer account; - * for those, every bank account will have null demobank reference and "admin" owner that - * do not point to any customer row. - */ - val demoBank = reference("demoBank", DemobankConfigsTable).nullable() + val demoBank = reference("demoBank", DemobankConfigsTable) } class BankAccountEntity(id: EntityID<Int>) : IntEntity(id) { @@ -387,11 +381,10 @@ class BankAccountEntity(id: EntityID<Int>) : IntEntity(id) { var iban by BankAccountsTable.iban var bic by BankAccountsTable.bic var label by BankAccountsTable.label - var currency by BankAccountsTable.currency var isDebit by BankAccountsTable.isDebit var owner by BankAccountsTable.owner var isPublic by BankAccountsTable.isPublic - var demoBank by BankAccountsTable.demoBank + var demoBank by DemobankConfigEntity referencedOn BankAccountsTable.demoBank } object BankAccountStatementsTable : IntIdTable() { diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt @@ -98,7 +98,67 @@ fun getPersonNameFromCustomer(ownerUsername: String): String { ownerCustomer.name ?: "Name not given" } } +fun getDefaultDemobank(): DemobankConfigEntity { + return transaction { + DemobankConfigEntity.find { + DemobankConfigsTable.name eq "default" + }.firstOrNull() + } ?: throw SandboxError( + HttpStatusCode.InternalServerError, + "Default demobank is missing." + ) +} +fun maybeCreateDefaultDemobank() { + transaction { + if (DemobankConfigEntity.all().empty()) { + DemobankConfigEntity.new { + currency = "EUR" + bankDebtLimit = 1000000 + usersDebtLimit = 10000 + allowRegistrations = true + name = "default" + } + } + } +} +fun wireTransfer( + debitAccount: String, + creditAccount: String, + demobank: String, + subject: String, + amount: String +): String { + val args: Triple<BankAccountEntity, BankAccountEntity, DemobankConfigEntity> = transaction { + val debitAccount = BankAccountEntity.find { + BankAccountsTable.label eq debitAccount + }.firstOrNull() ?: throw SandboxError( + HttpStatusCode.NotFound, + "Debit account '$debitAccount' not found" + ) + val creditAccount = BankAccountEntity.find { + BankAccountsTable.label eq creditAccount + }.firstOrNull() ?: throw SandboxError( + HttpStatusCode.NotFound, + "Credit account '$creditAccount' not found" + ) + val demoBank = DemobankConfigEntity.find { + DemobankConfigsTable.name eq demobank + }.firstOrNull() ?: throw SandboxError( + HttpStatusCode.NotFound, + "Demobank '$demobank' not found" + ) + + Triple(debitAccount, creditAccount, demoBank) + } + return wireTransfer( + debitAccount = args.first, + creditAccount = args.second, + demoBank = args.third, + subject = subject, + amount = amount + ) +} /** * Book a CRDT and a DBIT transaction and return the unique reference thereof. * @@ -179,10 +239,10 @@ fun getBankAccountFromIban(iban: String): BankAccountEntity { ) } -fun getBankAccountFromLabel(label: String): BankAccountEntity { +fun getBankAccountFromLabel(label: String, demobank: DemobankConfigEntity): BankAccountEntity { return transaction { BankAccountEntity.find( - BankAccountsTable.label eq label + BankAccountsTable.label eq label and (BankAccountsTable.demoBank eq demobank.id) ) }.firstOrNull() ?: throw SandboxError( HttpStatusCode.NotFound, diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt @@ -203,31 +203,22 @@ class MakeTransaction : CliktCommand("Wire-transfer money between Sandbox bank a private val creditAccount by option(help = "Label of the bank account receiving the payment").required() private val debitAccount by option(help = "Label of the bank account issuing the payment").required() + private val demobankArg by option("demobank", help = "Which Demobank books this transaction").default("default") private val amount by argument(help = "Amount, in the \$currency:x.y format") private val subjectArg by argument(name = "subject", help = "Payment's subject") override fun run() { val dbConnString = getDbConnFromEnv(SANDBOX_DB_ENV_VAR_NAME) Database.connect(dbConnString) - transaction { - /** - * No Demobank was configured so far - for example, - * current tests do not. This branch provides a default. - * - * Not used yet. - */ - if (DemobankConfigEntity.all().empty()) { - DemobankConfigEntity.new { - currency = "EUR" - bankDebtLimit = 1000000 - usersDebtLimit = 10000 - allowRegistrations = true - name = "default" - } - } - } + /** + * The function below create a default demobank + * if the 'config' command was never run before this point. + * + * This helps porting current tests to the "demobank model". + */ + maybeCreateDefaultDemobank() try { - wireTransfer(debitAccount, creditAccount, amount, subjectArg) + wireTransfer(debitAccount, creditAccount, demobankArg, subjectArg, amount) } catch (e: SandboxError) { print(e.message) exitProcess(1) @@ -338,7 +329,6 @@ data class BankAccountInfo( val name: String, val iban: String, val bic: String, - val currency: String? ) data class BankAccountsListReponse( @@ -526,7 +516,7 @@ val sandboxApp: Application.() -> Unit = { routing { get("/") { - call.respondText("Hello, this is Sandbox\n", ContentType.Text.Plain) + call.respondText("Hello, this is the Sandbox\n", ContentType.Text.Plain) } // Respond with the last statement of the requesting account. @@ -534,7 +524,10 @@ val sandboxApp: Application.() -> Unit = { post("/admin/payments/camt") { call.request.basicAuth() val body = call.receiveJson<CamtParams>() - val bankaccount = getBankAccountFromLabel(body.bankaccount) + val bankaccount = getBankAccountFromLabel( + body.bankaccount, + getDefaultDemobank() + ) if (body.type != 53) throw SandboxError( HttpStatusCode.NotFound, "Only Camt.053 documents can be generated." @@ -562,8 +555,8 @@ val sandboxApp: Application.() -> Unit = { iban = body.iban bic = body.bic label = body.label - currency = body.currency ?: "EUR" owner = username ?: "admin" // allows + demoBank = getDefaultDemobank() } } call.respond(object {}) @@ -573,17 +566,13 @@ val sandboxApp: Application.() -> Unit = { // Information about one bank account. get("/admin/bank-accounts/{label}") { call.request.basicAuth() - val label = ensureNonNull(call.parameters["label"]) + val label = call.getUriComponent("label") val ret = transaction { - val bankAccount = tech.libeufin.sandbox.BankAccountEntity.find { - tech.libeufin.sandbox.BankAccountsTable.label eq label - }.firstOrNull() ?: throw SandboxError( - io.ktor.http.HttpStatusCode.NotFound, - "Account '$label' not found" - ) + val demobank = getDefaultDemobank() + val bankAccount = getBankAccountFromLabel(label, demobank) val balance = balanceForAccount(bankAccount) object { - val balance = "${bankAccount.currency}:${balance}" + val balance = "${bankAccount.demoBank.currency}:${balance}" val iban = bankAccount.iban val bic = bankAccount.bic val label = bankAccount.label @@ -602,21 +591,24 @@ val sandboxApp: Application.() -> Unit = { val accountLabel = ensureNonNull(call.parameters["label"]) if (!validatePlainAmount(body.amount)) { throw SandboxError( - io.ktor.http.HttpStatusCode.BadRequest, + HttpStatusCode.BadRequest, "invalid amount (should be plain amount without currency)" ) } val reqDebtorBic = body.debtorBic if (reqDebtorBic != null && !validateBic(reqDebtorBic)) { throw SandboxError( - io.ktor.http.HttpStatusCode.BadRequest, + HttpStatusCode.BadRequest, "invalid BIC" ) } transaction { - val account = getBankAccountFromLabel(accountLabel) + val demobank = getDefaultDemobank() + val account = getBankAccountFromLabel( + accountLabel, demobank + ) val randId = getRandomString(16) - tech.libeufin.sandbox.BankAccountTransactionEntity.new { + BankAccountTransactionEntity.new { creditorIban = account.iban creditorBic = account.bic creditorName = "Creditor Name" // FIXME: Waits to get this value from the DemobankCustomer type. @@ -625,11 +617,11 @@ val sandboxApp: Application.() -> Unit = { debtorName = body.debtorName subject = body.subject amount = body.amount - currency = account.currency date = getUTCnow().toInstant().toEpochMilli() accountServicerReference = "sandbox-$randId" this.account = account direction = "CRDT" + this.demobank = demobank } } call.respond(object {}) @@ -649,8 +641,18 @@ val sandboxApp: Application.() -> Unit = { body.subscriber.partnerID, body.subscriber.hostID ) + val demobank = getDefaultDemobank() + + /** + * Checking that the default demobank doesn't have already the + * requested IBAN and bank account label. + */ val check = BankAccountEntity.find { - BankAccountsTable.iban eq body.iban or (BankAccountsTable.label eq body.label) + BankAccountsTable.iban eq body.iban or ( + (BankAccountsTable.label eq body.label) and ( + BankAccountsTable.demoBank eq demobank.id + ) + ) }.count() if (check > 0) throw SandboxError( HttpStatusCode.BadRequest, @@ -660,26 +662,28 @@ val sandboxApp: Application.() -> Unit = { iban = body.iban bic = body.bic label = body.label - currency = body.currency.uppercase(java.util.Locale.ROOT) owner = username ?: "admin" + demoBank = demobank } } call.respondText("Bank account created") return@post } - // Information about all the bank accounts. + // Information about all the default demobank's bank accounts get("/admin/bank-accounts") { call.request.basicAuth() val accounts = mutableListOf<BankAccountInfo>() transaction { - BankAccountEntity.all().forEach { + val demobank = getDefaultDemobank() + BankAccountEntity.find { + BankAccountsTable.demoBank eq demobank.id + }.forEach { accounts.add( BankAccountInfo( label = it.label, bic = it.bic, iban = it.iban, - currency = it.currency, name = "Bank account owner's name" ) ) @@ -695,9 +699,10 @@ val sandboxApp: Application.() -> Unit = { transaction { val accountLabel = ensureNonNull(call.parameters["label"]) transaction { - val account = getBankAccountFromLabel(accountLabel) - tech.libeufin.sandbox.BankAccountTransactionEntity.find { - tech.libeufin.sandbox.BankAccountTransactionsTable.account eq account.id + val demobank = getDefaultDemobank() + val account = getBankAccountFromLabel(accountLabel, demobank) + BankAccountTransactionEntity.find { + BankAccountTransactionsTable.account eq account.id }.forEach { ret.payments.add( PaymentInfo( @@ -736,7 +741,8 @@ val sandboxApp: Application.() -> Unit = { call.request.basicAuth() transaction { val accountLabel = ensureNonNull(call.parameters["label"]) - val account = getBankAccountFromLabel(accountLabel) + val demobank = getDefaultDemobank() + val account = getBankAccountFromLabel(accountLabel, demobank) val transactionReferenceCrdt = getRandomString(8) val transactionReferenceDbit = getRandomString(8) @@ -751,11 +757,11 @@ val sandboxApp: Application.() -> Unit = { debtorName = "Max Mustermann" subject = "sample transaction $transactionReferenceCrdt" this.amount = amount.toString() - currency = account.currency date = getUTCnow().toInstant().toEpochMilli() accountServicerReference = transactionReferenceCrdt this.account = account direction = "CRDT" + this.demobank = demobank } } @@ -771,11 +777,11 @@ val sandboxApp: Application.() -> Unit = { creditorName = "Max Mustermann" subject = "sample transaction $transactionReferenceDbit" this.amount = amount.toString() - currency = account.currency date = getUTCnow().toInstant().toEpochMilli() accountServicerReference = transactionReferenceDbit this.account = account direction = "DBIT" + this.demobank = demobank } } } @@ -834,17 +840,17 @@ val sandboxApp: Application.() -> Unit = { }.firstOrNull() ?: throw SandboxError( io.ktor.http.HttpStatusCode.NotFound, "Host $hostID not found" ) - val pairA = tech.libeufin.util.CryptoUtil.generateRsaKeyPair(2048) - val pairB = tech.libeufin.util.CryptoUtil.generateRsaKeyPair(2048) - val pairC = tech.libeufin.util.CryptoUtil.generateRsaKeyPair(2048) + val pairA = CryptoUtil.generateRsaKeyPair(2048) + val pairB = CryptoUtil.generateRsaKeyPair(2048) + val pairC = CryptoUtil.generateRsaKeyPair(2048) host.authenticationPrivateKey = ExposedBlob(pairA.private.encoded) host.encryptionPrivateKey = ExposedBlob(pairB.private.encoded) host.signaturePrivateKey = ExposedBlob(pairC.private.encoded) } call.respondText( "Keys of '${hostID}' rotated.", - io.ktor.http.ContentType.Text.Plain, - io.ktor.http.HttpStatusCode.OK + ContentType.Text.Plain, + HttpStatusCode.OK ) return@post } @@ -853,11 +859,11 @@ val sandboxApp: Application.() -> Unit = { post("/admin/ebics/hosts") { call.request.basicAuth() val req = call.receiveJson<EbicsHostCreateRequest>() - val pairA = tech.libeufin.util.CryptoUtil.generateRsaKeyPair(2048) - val pairB = tech.libeufin.util.CryptoUtil.generateRsaKeyPair(2048) - val pairC = tech.libeufin.util.CryptoUtil.generateRsaKeyPair(2048) + val pairA = CryptoUtil.generateRsaKeyPair(2048) + val pairB = CryptoUtil.generateRsaKeyPair(2048) + val pairC = CryptoUtil.generateRsaKeyPair(2048) transaction { - tech.libeufin.sandbox.EbicsHostEntity.new { + EbicsHostEntity.new { this.ebicsVersion = req.ebicsVersion this.hostId = req.hostID this.authenticationPrivateKey = ExposedBlob(pairA.private.encoded) @@ -867,8 +873,8 @@ val sandboxApp: Application.() -> Unit = { } call.respondText( "Host '${req.hostID}' created.", - io.ktor.http.ContentType.Text.Plain, - io.ktor.http.HttpStatusCode.OK + ContentType.Text.Plain, + HttpStatusCode.OK ) return@post } @@ -877,7 +883,7 @@ val sandboxApp: Application.() -> Unit = { get("/admin/ebics/hosts") { call.request.basicAuth() val ebicsHosts = transaction { - tech.libeufin.sandbox.EbicsHostEntity.all().map { it.hostId } + EbicsHostEntity.all().map { it.hostId } } call.respond(EbicsHostsResponse(ebicsHosts)) } @@ -897,8 +903,8 @@ val sandboxApp: Application.() -> Unit = { catch (e: SandboxError) { // Should translate to EBICS error code. when (e.errorCode) { - tech.libeufin.util.LibeufinErrorCode.LIBEUFIN_EC_INVALID_STATE -> throw EbicsProcessingError("Invalid bank state.") - tech.libeufin.util.LibeufinErrorCode.LIBEUFIN_EC_INCONSISTENT_STATE -> throw EbicsProcessingError("Inconsistent bank state.") + LibeufinErrorCode.LIBEUFIN_EC_INVALID_STATE -> throw EbicsProcessingError("Invalid bank state.") + LibeufinErrorCode.LIBEUFIN_EC_INCONSISTENT_STATE -> throw EbicsProcessingError("Inconsistent bank state.") else -> throw EbicsProcessingError("Unknown LibEuFin error code: ${e.errorCode}.") } } @@ -1052,7 +1058,10 @@ val sandboxApp: Application.() -> Unit = { * this check, the withdrawal operation will be allowed only by providing its * UID. */ - val maybeOwnedAccount = getBankAccountFromLabel(call.getUriComponent("account_name")) + val maybeOwnedAccount = getBankAccountFromLabel( + call.getUriComponent("account_name"), + demobank + ) if (maybeOwnedAccount.owner != username) throw unauthorized( "Customer '$username' has no rights over bank account '${maybeOwnedAccount.label}'" ) @@ -1157,7 +1166,11 @@ val sandboxApp: Application.() -> Unit = { return@get } get("/accounts/{account_name}/history") { - val bankAccount = getBankAccountFromLabel(call.getUriComponent("account_name")) + val demobank = ensureDemobank(call) + val bankAccount = getBankAccountFromLabel( + call.getUriComponent("account_name"), + demobank + ) val authOk: Boolean = bankAccount.isPublic || (!WITH_AUTH) if (!authOk && (call.request.basicAuth() != bankAccount.owner)) throw forbidden( "Cannot access bank account ${bankAccount.label}" @@ -1219,9 +1232,8 @@ val sandboxApp: Application.() -> Unit = { BankAccountEntity.new { iban = getIban() label = req.username + "acct" // multiple accounts per username not allowed. - currency = demobank.currency owner = req.username - this.demoBank = demobank.id + this.demoBank = demobank } DemobankCustomerEntity.new { username = req.username diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt @@ -104,86 +104,4 @@ fun historyForAccount(bankAccount: BankAccountEntity): MutableList<RawPayment> { } } return history -} - -/** - * https://github.com/JetBrains/Exposed/wiki/Transactions#working-with-coroutines - * https://medium.com/androiddevelopers/threading-models-in-coroutines-and-android-sqlite-api-6cab11f7eb90 - * - * FIXME: This version will be deprecated. It was made before introducing the demobank configuration - */ -fun wireTransfer( - debitAccount: String, - creditAccount: String, - amount: String, - subjectArg: String -) { - transaction { - // check accounts exist - val credit = BankAccountEntity.find { - BankAccountsTable.label eq creditAccount - }.firstOrNull() ?: run { - throw SandboxError(HttpStatusCode.NotFound, "Credit account: $creditAccount, not found") - } - val debit = BankAccountEntity.find { - BankAccountsTable.label eq debitAccount - }.firstOrNull() ?: run { - throw SandboxError(HttpStatusCode.NotFound, "Debit account: $debitAccount, not found") - } - if (credit.currency != debit.currency) { - throw SandboxError(HttpStatusCode.InternalServerError, - "Sandbox has inconsistent state: " + - "currency of credit (${credit.currency}) and debit (${debit.currency}) account differs." - ) - } - val amountObj = try { - parseAmount(amount) - } catch (e: Exception) { - throw SandboxError(HttpStatusCode.BadRequest, "Amount given not valid: $amount") - } - // Extra check on the currency's consistency - if (credit.currency != debit.currency) throw SandboxError( - HttpStatusCode.InternalServerError, - "Credit and debit account have different currency (${credit.currency} vs ${debit.currency})!", - LibeufinErrorCode.LIBEUFIN_EC_CURRENCY_INCONSISTENT - ) - if (amountObj.currency != credit.currency || amountObj.currency != debit.currency) { - throw SandboxError( - HttpStatusCode.BadRequest, - "Currency (${amountObj.currency}) is not supported", - LibeufinErrorCode.LIBEUFIN_EC_BAD_CURRENCY - ) - } - val randId = getRandomString(16) - BankAccountTransactionEntity.new { - creditorIban = credit.iban - creditorBic = credit.bic - creditorName = "Creditor Name" - debtorIban = debit.iban - debtorBic = debit.bic - debtorName = "Debitor Name" - subject = subjectArg - this.amount = amountObj.amount.toString() - currency = amountObj.currency - date = getUTCnow().toInstant().toEpochMilli() - accountServicerReference = "sandbox-$randId" - account = debit - direction = "DBIT" - } - BankAccountTransactionEntity.new { - creditorIban = credit.iban - creditorBic = credit.bic - creditorName = "Creditor Name" - debtorIban = debit.iban - debtorBic = debit.bic - debtorName = "Debitor Name" - subject = subjectArg - this.amount = amountObj.amount.toString() - currency = amountObj.currency - date = getUTCnow().toInstant().toEpochMilli() - accountServicerReference = "sandbox-$randId" - account = credit - direction = "CRDT" - } - } -} +} +\ No newline at end of file