libeufin

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

commit 29bca27b9822b9b8a23db5ce0ba8a632fa200b26
parent a492988243a0dfcb72869c0b6a4d85cb3c1a9bcb
Author: Antoine A <>
Date:   Thu, 26 Oct 2023 09:39:24 +0000

Crate customer and bank account at the same time and clean database tests and logic

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/Authentication.kt | 18++++++++++++++++++
Mbank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt | 78+++++++++++++++++++++++++++++++++++++++---------------------------------------
Mbank/src/main/kotlin/tech/libeufin/bank/Database.kt | 156++++++++++++++++++++++++++++++++-----------------------------------------------
Abank/src/main/kotlin/tech/libeufin/bank/Error.kt | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt | 32++------------------------------
Mbank/src/main/kotlin/tech/libeufin/bank/helpers.kt | 109++++++++++++-------------------------------------------------------------------
Mbank/src/test/kotlin/CoreBankApiTest.kt | 74+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mbank/src/test/kotlin/DatabaseTest.kt | 448+------------------------------------------------------------------------------
Mbank/src/test/kotlin/WireGatewayApiTest.kt | 20++++++++++----------
Mbank/src/test/kotlin/helpers.kt | 80++++++++++++++++++++++++++++++++-----------------------------------------------
10 files changed, 363 insertions(+), 763 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt b/bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt @@ -1,3 +1,21 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2023 Stanisci and Dold. + + * LibEuFin is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation; either version 3, or + * (at your option) any later version. + + * LibEuFin is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General + * Public License for more details. + + * You should have received a copy of the GNU Affero General Public + * License along with LibEuFin; see the file COPYING. If not, see + * <http://www.gnu.org/licenses/> + */ package tech.libeufin.bank import io.ktor.http.* diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt @@ -1,3 +1,21 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2023 Stanisci and Dold. + + * LibEuFin is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation; either version 3, or + * (at your option) any later version. + + * LibEuFin is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General + * Public License for more details. + + * You should have received a copy of the GNU Affero General Public + * License along with LibEuFin; see the file COPYING. If not, see + * <http://www.gnu.org/licenses/> + */ package tech.libeufin.bank import io.ktor.http.* @@ -81,7 +99,7 @@ private fun Routing.coreBankTokenApi(db: Database) { } } val customerDbRow = - db.customerGetFromLogin(login)?.dbRowId + db.customerGetFromLogin(login)?.customerId ?: throw internalServerError( "Could not get customer '$login' database row ID" ) @@ -142,7 +160,7 @@ private fun Routing.coreBankAccountsMgmtApi(db: Database, ctx: BankConfig) { val maybeHasBankAccount = maybeCustomerExists.run { if (this == null) return@run null - db.bankAccountGetFromOwnerId(this.expectRowId()) + db.bankAccountGetFromOwnerId(this.customerId) } val internalPayto = req.internal_payto_uri ?: IbanPayTo(genIbanPaytoUri()) if (maybeCustomerExists != null && maybeHasBankAccount != null) { @@ -170,39 +188,21 @@ private fun Routing.coreBankAccountsMgmtApi(db: Database, ctx: BankConfig) { } // From here: fresh user being added. - val newCustomer = - Customer( - login = req.username, - name = req.name, - email = req.challenge_contact_data?.email, - phone = req.challenge_contact_data?.phone, - cashoutPayto = - req.cashout_payto_uri, // Following could be gone, if included in - // cashout_payto_uri - cashoutCurrency = ctx.fiatCurrency, - passwordHash = CryptoUtil.hashpw(req.password), - ) - val newCustomerRowId = - db.customerCreate(newCustomer) - ?: throw internalServerError( - "New customer INSERT failed despite the previous checks" - ) // Crashing here won't break data consistency between customers and bank - // accounts, because of the idempotency. Client will just have to retry. - val maxDebt = ctx.defaultCustomerDebtLimit - val newBankAccount = - BankAccount( - hasDebt = false, - internalPaytoUri = internalPayto, - owningCustomerId = newCustomerRowId, - isPublic = req.is_public, - isTalerExchange = req.is_taler_exchange, - maxDebt = maxDebt - ) - val newBankAccountId = - db.bankAccountCreate(newBankAccount) - ?: throw internalServerError( - "Could not INSERT bank account despite all the checks." - ) + val (_, newBankAccountId) = db.accountCreate( + login = req.username, + name = req.name, + email = req.challenge_contact_data?.email, + phone = req.challenge_contact_data?.phone, + cashoutPayto = + req.cashout_payto_uri, // Following could be gone, if included in + // cashout_payto_uri + cashoutCurrency = ctx.fiatCurrency, + passwordHash = CryptoUtil.hashpw(req.password), + internalPaytoUri = internalPayto, + isPublic = req.is_public, + isTalerExchange = req.is_taler_exchange, + maxDebt = ctx.defaultCustomerDebtLimit + ) // The new account got created, now optionally award the registration // bonus to it. @@ -214,12 +214,12 @@ private fun Routing.coreBankAccountsMgmtApi(db: Database, ctx: BankConfig) { db.customerGetFromLogin("admin") ?: throw internalServerError("Admin customer not found") val adminBankAccount = - db.bankAccountGetFromOwnerId(adminCustomer.expectRowId()) + db.bankAccountGetFromOwnerId(adminCustomer.customerId) ?: throw internalServerError("Admin bank account not found") val adminPaysBonus = BankInternalTransaction( creditorAccountId = newBankAccountId, - debtorAccountId = adminBankAccount.expectRowId(), + debtorAccountId = adminBankAccount.bankAccountId, amount = bonusAmount, subject = "Registration bonus.", transactionDate = Instant.now() @@ -295,7 +295,7 @@ private fun Routing.coreBankAccountsMgmtApi(db: Database, ctx: BankConfig) { throw forbidden("non-admin user cannot change their legal name") // Preventing identical data to be overridden. val bankAccount = - db.bankAccountGetFromOwnerId(accountCustomer.expectRowId()) + db.bankAccountGetFromOwnerId(accountCustomer.customerId) ?: throw internalServerError( "Customer '${accountCustomer.login}' lacks bank account." ) @@ -377,7 +377,7 @@ private fun Routing.coreBankAccountsMgmtApi(db: Database, ctx: BankConfig) { talerEc = TalerErrorCode.TALER_EC_END ) val bankAccountData = - db.bankAccountGetFromOwnerId(customerData.expectRowId()) + db.bankAccountGetFromOwnerId(customerData.customerId) ?: throw internalServerError( "Customer '$login' had no bank account despite they are customer.'" ) diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt @@ -36,9 +36,6 @@ import tech.libeufin.util.* private const val DB_CTR_LIMIT = 1000000 -fun Customer.expectRowId(): Long = this.dbRowId ?: throw internalServerError("Cutsomer '$login' had no DB row ID.") -fun BankAccount.expectRowId(): Long = this.bankAccountId ?: throw internalServerError("Bank account '${this.internalPaytoUri}' lacks database row ID.") - private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.Database") /** @@ -123,51 +120,6 @@ class Database(dbConfig: String, private val bankCurrency: String, private val f } // CUSTOMERS - /** - * This method INSERTs a new customer into the database and - * returns its row ID. That is useful because often a new user - * ID has to be specified in more database records, notably in - * bank accounts to point at their owners. - * - * In case of conflict, this method returns null. - */ - suspend fun customerCreate(customer: Customer): Long? = conn { conn -> - val stmt = conn.prepareStatement(""" - INSERT INTO customers ( - login - ,password_hash - ,name - ,email - ,phone - ,cashout_payto - ,cashout_currency - ) - VALUES (?, ?, ?, ?, ?, ?, ?) - RETURNING customer_id - """ - ) - stmt.setString(1, customer.login) - stmt.setString(2, customer.passwordHash) - stmt.setString(3, customer.name) - stmt.setString(4, customer.email) - stmt.setString(5, customer.phone) - stmt.setString(6, customer.cashoutPayto) - stmt.setString(7, customer.cashoutCurrency) - - val res = try { - stmt.executeQuery() - } catch (e: SQLException) { - logger.error(e.message) - if (e.errorCode == 0) return@conn null // unique constraint violation. - throw e // rethrow on other errors. - } - res.use { - when { - !it.next() -> throw internalServerError("SQL RETURNING gave no customer_id.") - else -> it.getLong("customer_id") - } - } - } /** * Deletes a customer (including its bank account row) from @@ -243,7 +195,7 @@ class Database(dbConfig: String, private val bankCurrency: String, private val f email = it.getString("email"), cashoutCurrency = it.getString("cashout_currency"), cashoutPayto = it.getString("cashout_payto"), - dbRowId = it.getLong("customer_id") + customerId = it.getLong("customer_id") ) } } @@ -311,6 +263,68 @@ class Database(dbConfig: String, private val bankCurrency: String, private val f // MIXED CUSTOMER AND BANK ACCOUNT DATA + suspend fun accountCreate( + login: String, + passwordHash: String, + name: String, + email: String? = null, + phone: String? = null, + cashoutPayto: String? = null, + cashoutCurrency: String? = null, + internalPaytoUri: IbanPayTo, + isPublic: Boolean, + isTalerExchange: Boolean, + maxDebt: TalerAmount + ): Pair<Long, Long> = conn { it -> + it.transaction { conn -> + val customerId = conn.prepareStatement(""" + INSERT INTO customers ( + login + ,password_hash + ,name + ,email + ,phone + ,cashout_payto + ,cashout_currency + ) + VALUES (?, ?, ?, ?, ?, ?, ?) + RETURNING customer_id + """ + ).run { + setString(1, login) + setString(2, passwordHash) + setString(3, name) + setString(4, email) + setString(5, phone) + setString(6, cashoutPayto) + setString(7, cashoutCurrency) + oneOrNull { it.getLong("customer_id") } + ?: throw internalServerError("SQL RETURNING gave no customer_id.") + } + + val stmt = conn.prepareStatement(""" + INSERT INTO bank_accounts + (internal_payto_uri + ,owning_customer_id + ,is_public + ,is_taler_exchange + ,max_debt + ) + VALUES (?, ?, ?, ?, (?, ?)::taler_amount) + RETURNING bank_account_id; + """) + stmt.setString(1, internalPaytoUri.canonical) + stmt.setLong(2, customerId) + stmt.setBoolean(3, isPublic) + stmt.setBoolean(4, isTalerExchange) + stmt.setLong(5, maxDebt.value) + stmt.setInt(6, maxDebt.frac) + val bankId = stmt.oneOrNull { it.getLong("bank_account_id") } + ?: throw internalServerError("SQL RETURNING gave no bank_account_id.") + Pair(customerId, bankId) + } + } + /** * Updates accounts according to the PATCH /accounts/foo endpoint. * The 'login' parameter decides which customer and bank account rows @@ -447,50 +461,6 @@ class Database(dbConfig: String, private val bankCurrency: String, private val f // BANK ACCOUNTS - /** - * Inserts a new bank account in the database, returning its - * row ID in the successful case. If of unique constrain violation, - * it returns null and any other error will be thrown as 500. - */ - suspend fun bankAccountCreate(bankAccount: BankAccount): Long? = conn { conn -> - if (bankAccount.balance != null) - throw internalServerError( - "Do not pass a balance upon bank account creation, do a wire transfer instead." - ) - val stmt = conn.prepareStatement(""" - INSERT INTO bank_accounts - (internal_payto_uri - ,owning_customer_id - ,is_public - ,is_taler_exchange - ,max_debt - ) - VALUES - (?, ?, ?, ?, (?, ?)::taler_amount) - RETURNING bank_account_id; - """) - stmt.setString(1, bankAccount.internalPaytoUri.canonical) - stmt.setLong(2, bankAccount.owningCustomerId) - stmt.setBoolean(3, bankAccount.isPublic) - stmt.setBoolean(4, bankAccount.isTalerExchange) - stmt.setLong(5, bankAccount.maxDebt.value) - stmt.setInt(6, bankAccount.maxDebt.frac) - // using the default zero value for the balance. - val res = try { - stmt.executeQuery() - } catch (e: SQLException) { - logger.error(e.message) - if (e.errorCode == 0) return@conn null // unique constraint violation. - throw e // rethrow on other errors. - } - res.use { - when { - !it.next() -> throw internalServerError("SQL RETURNING gave no bank_account_id.") - else -> it.getLong("bank_account_id") - } - } - } - suspend fun bankAccountSetMaxDebt( owningCustomerId: Long, maxDebt: TalerAmount diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Error.kt b/bank/src/main/kotlin/tech/libeufin/bank/Error.kt @@ -0,0 +1,110 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2023 Stanisci and Dold. + + * LibEuFin is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation; either version 3, or + * (at your option) any later version. + + * LibEuFin is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General + * Public License for more details. + + * You should have received a copy of the GNU Affero General Public + * License along with LibEuFin; see the file COPYING. If not, see + * <http://www.gnu.org/licenses/> + */ +package tech.libeufin.bank + +import net.taler.common.errorcodes.TalerErrorCode +import tech.libeufin.util.* +import kotlinx.serialization.Serializable +import io.ktor.http.* + +/** + * Convenience type to throw errors along the bank activity + * and that is meant to be caught by Ktor and responded to the + * client. + */ +class LibeufinBankException( + // Status code that Ktor will set for the response. + val httpStatus: HttpStatusCode, + // Error detail object, after Taler API. + val talerError: TalerError +) : Exception(talerError.hint) + +/** + * Error object to respond to the client. The + * 'code' field takes values from the GANA gnu-taler-error-code + * specification. 'hint' is a human-readable description + * of the error. + */ +@Serializable +data class TalerError( + val code: Int, + val hint: String? = null, + val detail: String? = null +) + + +fun forbidden( + hint: String = "No rights on the resource", + talerErrorCode: TalerErrorCode = TalerErrorCode.TALER_EC_END +): LibeufinBankException = LibeufinBankException( + httpStatus = HttpStatusCode.Forbidden, talerError = TalerError( + code = talerErrorCode.code, hint = hint + ) +) + +fun unauthorized(hint: String = "Login failed"): LibeufinBankException = LibeufinBankException( + httpStatus = HttpStatusCode.Unauthorized, talerError = TalerError( + code = TalerErrorCode.TALER_EC_GENERIC_UNAUTHORIZED.code, hint = hint + ) +) + +fun internalServerError(hint: String?): LibeufinBankException = LibeufinBankException( + httpStatus = HttpStatusCode.InternalServerError, talerError = TalerError( + code = TalerErrorCode.TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE.code, hint = hint + ) +) + +fun notFound( + hint: String?, + talerEc: TalerErrorCode +): LibeufinBankException = LibeufinBankException( + httpStatus = HttpStatusCode.NotFound, talerError = TalerError( + code = talerEc.code, hint = hint + ) +) + +fun conflict( + hint: String?, talerEc: TalerErrorCode +): LibeufinBankException = LibeufinBankException( + httpStatus = HttpStatusCode.Conflict, talerError = TalerError( + code = talerEc.code, hint = hint + ) +) + +fun badRequest( + hint: String? = null, talerErrorCode: TalerErrorCode = TalerErrorCode.TALER_EC_GENERIC_JSON_INVALID +): LibeufinBankException = LibeufinBankException( + httpStatus = HttpStatusCode.BadRequest, talerError = TalerError( + code = talerErrorCode.code, hint = hint + ) +) + +fun BankConfig.checkInternalCurrency(amount: TalerAmount) { + if (amount.currency != currency) throw badRequest( + "Wrong currency: expected internal currency $currency got ${amount.currency}", + talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH + ) +} + +fun BankConfig.checkFiatCurrency(amount: TalerAmount) { + if (amount.currency != fiatCurrency) throw badRequest( + "Wrong currency: expected fiat currency $fiatCurrency got ${amount.currency}", + talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH + ) +} +\ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt @@ -63,18 +63,6 @@ data class TokenSuccessResponse( val expiration: TalerProtocolTimestamp ) -/** - * Error object to respond to the client. The - * 'code' field takes values from the GANA gnu-taler-error-code - * specification. 'hint' is a human-readable description - * of the error. - */ -@Serializable -data class TalerError( - val code: Int, - val hint: String? = null, - val detail: String? = null -) /* Contains contact data to send TAN challges to the * users, to let them complete cashout operations. */ @@ -136,18 +124,6 @@ data class MonitorWithCashout( ) : MonitorResponse() /** - * Convenience type to throw errors along the bank activity - * and that is meant to be caught by Ktor and responded to the - * client. - */ -class LibeufinBankException( - // Status code that Ktor will set for the response. - val httpStatus: HttpStatusCode, - // Error detail object, after Taler API. - val talerError: TalerError -) : Exception(talerError.hint) - -/** * Convenience type to hold customer data, typically after such * data gets fetched from the database. It is also used to _insert_ * customer data to the database. @@ -156,11 +132,7 @@ data class Customer( val login: String, val passwordHash: String, val name: String, - /** - * Only non-null when this object is defined _by_ the - * database. - */ - val dbRowId: Long? = null, + val customerId: Long, val email: String? = null, val phone: String? = null, /** @@ -183,7 +155,7 @@ data class BankAccount( val internalPaytoUri: IbanPayTo, // Database row ID of the customer that owns this bank account. val owningCustomerId: Long, - val bankAccountId: Long? = null, // null at INSERT. + val bankAccountId: Long, val isPublic: Boolean = false, val isTalerExchange: Boolean = false, /** diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt @@ -70,66 +70,6 @@ suspend fun ApplicationCall.bankAccount(db: Database): BankAccount { ) } -fun forbidden( - hint: String = "No rights on the resource", - talerErrorCode: TalerErrorCode = TalerErrorCode.TALER_EC_END -): LibeufinBankException = LibeufinBankException( - httpStatus = HttpStatusCode.Forbidden, talerError = TalerError( - code = talerErrorCode.code, hint = hint - ) -) - -fun unauthorized(hint: String = "Login failed"): LibeufinBankException = LibeufinBankException( - httpStatus = HttpStatusCode.Unauthorized, talerError = TalerError( - code = TalerErrorCode.TALER_EC_GENERIC_UNAUTHORIZED.code, hint = hint - ) -) - -fun internalServerError(hint: String?): LibeufinBankException = LibeufinBankException( - httpStatus = HttpStatusCode.InternalServerError, talerError = TalerError( - code = TalerErrorCode.TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE.code, hint = hint - ) -) - -fun notFound( - hint: String?, - talerEc: TalerErrorCode -): LibeufinBankException = LibeufinBankException( - httpStatus = HttpStatusCode.NotFound, talerError = TalerError( - code = talerEc.code, hint = hint - ) -) - -fun conflict( - hint: String?, talerEc: TalerErrorCode -): LibeufinBankException = LibeufinBankException( - httpStatus = HttpStatusCode.Conflict, talerError = TalerError( - code = talerEc.code, hint = hint - ) -) - -fun badRequest( - hint: String? = null, talerErrorCode: TalerErrorCode = TalerErrorCode.TALER_EC_GENERIC_JSON_INVALID -): LibeufinBankException = LibeufinBankException( - httpStatus = HttpStatusCode.BadRequest, talerError = TalerError( - code = talerErrorCode.code, hint = hint - ) -) - -fun BankConfig.checkInternalCurrency(amount: TalerAmount) { - if (amount.currency != currency) throw badRequest( - "Wrong currency: expected internal currency $currency got ${amount.currency}", - talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH - ) -} - -fun BankConfig.checkFiatCurrency(amount: TalerAmount) { - if (amount.currency != fiatCurrency) throw badRequest( - "Wrong currency: expected fiat currency $fiatCurrency got ${amount.currency}", - talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH - ) -} - // Generates a new Payto-URI with IBAN scheme. fun genIbanPaytoUri(): String = "payto://iban/SANDBOXX/${getIban()}" @@ -280,48 +220,31 @@ data class CashoutRateParams( * * It returns false in case of problems, true otherwise. */ -suspend fun maybeCreateAdminAccount(db: Database, ctx: BankConfig): Boolean { +suspend fun maybeCreateAdminAccount(db: Database, ctx: BankConfig, pw: String? = null): Boolean { val maybeAdminCustomer = db.customerGetFromLogin("admin") - val adminCustomerId: Long = if (maybeAdminCustomer == null) { - logger.debug("Creating admin's customer row") - val pwBuf = ByteArray(32) - Random().nextBytes(pwBuf) - val adminCustomer = Customer( + if (maybeAdminCustomer == null) { + logger.debug("Creating admin's account") + var pwStr = pw; + if (pwStr == null) { + val pwBuf = ByteArray(32) + Random().nextBytes(pwBuf) + pwStr = String(pwBuf, Charsets.UTF_8) + } + + + db.accountCreate( login = "admin", /** * Hashing the password helps to avoid the "password not hashed" * error, in case the admin tries to authenticate. */ - passwordHash = CryptoUtil.hashpw(String(pwBuf, Charsets.UTF_8)), name = "Bank administrator" - ) - val rowId = db.customerCreate(adminCustomer) - if (rowId == null) { - logger.error("Could not create the admin customer row.") - return false - } - rowId - } else maybeAdminCustomer.expectRowId() - val maybeAdminBankAccount = db.bankAccountGetFromOwnerId(adminCustomerId) - if (maybeAdminBankAccount == null) { - logger.info("Creating admin bank account") - val adminMaxDebtObj = ctx.defaultAdminDebtLimit - val adminInternalPayto = stripIbanPayto(genIbanPaytoUri()) - if (adminInternalPayto == null) { - logger.error("Bank generated invalid payto URI for admin") - return false - } - val adminBankAccount = BankAccount( - hasDebt = false, - internalPaytoUri = IbanPayTo(adminInternalPayto), - owningCustomerId = adminCustomerId, + passwordHash = CryptoUtil.hashpw(pwStr), + name = "Bank administrator", + internalPaytoUri = IbanPayTo(genIbanPaytoUri()), isPublic = false, isTalerExchange = false, - maxDebt = adminMaxDebtObj + maxDebt = ctx.defaultAdminDebtLimit ) - if (db.bankAccountCreate(adminBankAccount) == null) { - logger.error("Failed to creating admin bank account.") - return false - } } return true } \ No newline at end of file diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt @@ -301,7 +301,13 @@ class CoreBankAccountsMgmtApiTest { }.assertNoContent() val nameReq = json { + "login" to "foo" "name" to "Another Foo" + "cashout_address" to "payto://cashout" + "challenge_contact_data" to json { + "phone" to "+99" + "email" to "foo@example.com" + } } // Checking ordinary user doesn't get to patch their name. client.patch("/accounts/merchant") { @@ -314,8 +320,17 @@ class CoreBankAccountsMgmtApiTest { jsonBody(nameReq) }.assertNoContent() - val fooFromDb = db.customerGetFromLogin("merchant") - assertEquals("Another Foo", fooFromDb?.name) + // Check patch + client.get("/accounts/merchant") { + basicAuth("admin", "admin-password") + }.assertOk().run { + val obj: AccountData = Json.decodeFromString(bodyAsText()) + assertEquals("Another Foo", obj.name) + assertEquals("payto://cashout", obj.cashout_payto_uri) + assertEquals("+99", obj.contact_data?.phone) + assertEquals("foo@example.com", obj.contact_data?.email) + } + } // PATCH /accounts/USERNAME/auth @@ -345,7 +360,7 @@ class CoreBankAccountsMgmtApiTest { @Test fun accountsListTest() = bankSetup { _ -> // Remove default accounts - listOf("merchant", "exchange").forEach { + listOf("merchant", "exchange", "customer").forEach { client.delete("/accounts/$it") { basicAuth("admin", "admin-password") }.assertNoContent() @@ -688,6 +703,59 @@ class CoreBankTransactionsApiTest { "payto_uri" to "payto://iban/MERCHANT-IBAN-XYZ?message=payout" }) }.assertConflict().assertErr(TalerErrorCode.TALER_EC_BANK_SAME_ACCOUNT) + + suspend fun checkBalance( + merchantDebt: Boolean, + merchantAmount: String, + customerDebt: Boolean, + customerAmount: String, + ) { + client.get("/accounts/merchant") { + basicAuth("admin", "admin-password") + }.assertOk().run { + val obj: AccountData = Json.decodeFromString(bodyAsText()) + assertEquals( + if (merchantDebt) CorebankCreditDebitInfo.debit else CorebankCreditDebitInfo.credit, + obj.balance.credit_debit_indicator) + assertEquals(TalerAmount(merchantAmount), obj.balance.amount) + } + client.get("/accounts/customer") { + basicAuth("admin", "admin-password") + }.assertOk().run { + val obj: AccountData = Json.decodeFromString(bodyAsText()) + assertEquals( + if (customerDebt) CorebankCreditDebitInfo.debit else CorebankCreditDebitInfo.credit, + obj.balance.credit_debit_indicator) + assertEquals(TalerAmount(customerAmount), obj.balance.amount) + } + } + + // Init state + checkBalance(true, "KUDOS:2.4", false, "KUDOS:0") + // Send 2 times 3 + repeat(2) { + client.post("/accounts/merchant/transactions") { + basicAuth("merchant", "merchant-password") + jsonBody(json { + "payto_uri" to "payto://iban/CUSTOMER-IBAN-XYZ?message=payout2&amount=KUDOS:3" + }) + }.assertNoContent() + } + client.post("/accounts/merchant/transactions") { + basicAuth("merchant", "merchant-password") + jsonBody(json { + "payto_uri" to "payto://iban/CUSTOMER-IBAN-XYZ?message=payout2&amount=KUDOS:3" + }) + }.assertConflict().assertErr(TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT) + checkBalance(true, "KUDOS:8.4", false, "KUDOS:6") + // Send throught debt + client.post("/accounts/customer/transactions") { + basicAuth("customer", "customer-password") + jsonBody(json { + "payto_uri" to "payto://iban/MERCHANT-IBAN-XYZ?message=payout2&amount=KUDOS:10" + }) + }.assertNoContent() + checkBalance(false, "KUDOS:1.6", true, "KUDOS:4") } } diff --git a/bank/src/test/kotlin/DatabaseTest.kt b/bank/src/test/kotlin/DatabaseTest.kt @@ -46,40 +46,6 @@ fun genTx( ) class DatabaseTest { - private val customerFoo = Customer( - login = "foo", - passwordHash = "hash", - name = "Foo", - phone = "+00", - email = "foo@b.ar", - cashoutPayto = "payto://external-IBAN", - cashoutCurrency = "KUDOS" - ) - private val customerBar = Customer( - login = "bar", - passwordHash = "hash", - name = "Bar", - phone = "+00", - email = "foo@b.ar", - cashoutPayto = "payto://external-IBAN", - cashoutCurrency = "KUDOS" - ) - private val bankAccountFoo = BankAccount( - internalPaytoUri = IbanPayTo("payto://iban/FOO-IBAN-XYZ"), - lastNexusFetchRowId = 1L, - owningCustomerId = 1L, - hasDebt = false, - maxDebt = TalerAmount(10, 1, "KUDOS"), - isTalerExchange = true - ) - private val bankAccountBar = BankAccount( - internalPaytoUri = IbanPayTo("payto://iban/BAR-IBAN-ABC"), - lastNexusFetchRowId = 1L, - owningCustomerId = 2L, - hasDebt = false, - maxDebt = TalerAmount(10, 1, "KUDOS") - ) - val fooPaysBar = genTx() // Testing the helper that creates the admin account. @Test @@ -93,7 +59,7 @@ class DatabaseTest { val yesAdminCustomer = db.customerGetFromLogin("admin") assert(yesAdminCustomer != null) // Expecting also its _bank_ account. - assert(db.bankAccountGetFromOwnerId(yesAdminCustomer!!.expectRowId()) != null) + assert(db.bankAccountGetFromOwnerId(yesAdminCustomer!!.customerId) != null) // Checking idempotency. assert(maybeCreateAdminAccount(db, ctx)) // Checking that the random password blocks a login. @@ -102,418 +68,6 @@ class DatabaseTest { yesAdminCustomer.passwordHash )) } - - /** - * Tests the SQL function that performs the instructions - * given by the exchange to pay one merchant. - */ - @Test - fun talerTransferTest() = dbSetup { db -> - val exchangeReq = TransferRequest( - amount = TalerAmount(9, 0, "KUDOS"), - credit_account = IbanPayTo("payto://iban/BAR-IBAN-ABC"), - exchange_base_url = ExchangeUrl("https://example.com/exchange"), - request_uid = randHashCode(), - wtid = randShortHashCode() - ) - val fooId = db.customerCreate(customerFoo) - assert(fooId != null) - val barId = db.customerCreate(customerBar) - assert(barId != null) - assert(db.bankAccountCreate(bankAccountFoo) != null) - assert(db.bankAccountCreate(bankAccountBar) != null) - val res = db.talerTransferCreate( - req = exchangeReq, - username = "foo", - timestamp = Instant.now() - ) - assert(res.txResult == TalerTransferResult.SUCCESS) - } - - @Test - fun bearerTokenTest() = dbSetup { db -> - val tokenBytes = ByteArray(32) - Random().nextBytes(tokenBytes) - val token = BearerToken( - bankCustomer = 1L, - content = tokenBytes, - creationTime = Instant.now(), - expirationTime = Instant.now().plusSeconds(10), - scope = TokenScope.readonly - ) - assert(db.bearerTokenGet(token.content) == null) - assert(db.customerCreate(customerBar) != null) // Tokens need owners. - assert(db.bearerTokenCreate(token)) - assert(db.bearerTokenGet(tokenBytes) != null) - } - - @Test - fun tokenDeletionTest() = dbSetup { db -> - val token = ByteArray(32) - // Token not there, must fail. - assert(!db.bearerTokenDelete(token)) - assert(db.customerCreate(customerBar) != null) // Tokens need owners. - assert(db.bearerTokenCreate( - BearerToken( - bankCustomer = 1L, - content = token, - creationTime = Instant.now(), - expirationTime = Instant.now().plusSeconds(10), - scope = TokenScope.readwrite - ) - )) - // Wrong token given, must fail - val anotherToken = token.map { - it.inv() // flipping every bit. - } - assert(!db.bearerTokenDelete(anotherToken.toByteArray())) - // Token there, must succeed. - assert(db.bearerTokenDelete(token)) - } - - @Test - fun bankTransactionsTest() = dbSetup { db -> - val fooId = db.customerCreate(customerFoo) - assert(fooId != null) - val barId = db.customerCreate(customerBar) - assert(barId != null) - assert(db.bankAccountCreate(bankAccountFoo) != null) - assert(db.bankAccountCreate(bankAccountBar) != null) - var fooAccount = db.bankAccountGetFromOwnerId(fooId!!) - assert(fooAccount?.hasDebt == false) // Foo has NO debit. - val currency = "KUDOS" - // Preparing the payment data. - db.bankAccountSetMaxDebt( - fooId, - TalerAmount(100, 0, currency) - ) - db.bankAccountSetMaxDebt( - barId!!, - TalerAmount(50, 0, currency) - ) - val firstSpending = db.bankTransactionCreate(fooPaysBar) // Foo pays Bar and goes debit. - assert(firstSpending == BankTransactionResult.SUCCESS) - fooAccount = db.bankAccountGetFromOwnerId(fooId) - // Foo: credit -> debit - assert(fooAccount?.hasDebt == true) // Asserting Foo's debit. - // Now checking that more spending doesn't get Foo out of debit. - val secondSpending = db.bankTransactionCreate(fooPaysBar) - assert(secondSpending == BankTransactionResult.SUCCESS) - fooAccount = db.bankAccountGetFromOwnerId(fooId) - // Checking that Foo's debit is two times the paid amount - // Foo: debit -> debit - assert(fooAccount?.balance?.value == 20L - && fooAccount.balance?.frac == 0 - && fooAccount.hasDebt - ) - // Asserting Bar has a positive balance and what Foo paid so far. - var barAccount = db.bankAccountGetFromOwnerId(barId) - val barBalance: TalerAmount? = barAccount?.balance - assert( - barAccount?.hasDebt == false - && barBalance?.value == 20L && barBalance.frac == 0 - ) - // Bar pays so that its balance remains positive. - val barPaysFoo = BankInternalTransaction( - creditorAccountId = 1, - debtorAccountId = 2, - subject = "test", - amount = TalerAmount(10, 0, currency), - accountServicerReference = "acct-svcr-ref", - endToEndId = "end-to-end-id", - paymentInformationId = "pmtinfid", - transactionDate = Instant.now() - ) - val barPays = db.bankTransactionCreate(barPaysFoo) - assert(barPays == BankTransactionResult.SUCCESS) - barAccount = db.bankAccountGetFromOwnerId(barId) - val barBalanceTen: TalerAmount? = barAccount?.balance - // Bar: credit -> credit - assert(barAccount?.hasDebt == false && barBalanceTen?.value == 10L && barBalanceTen.frac == 0) - // Bar pays again to let Foo return in credit. - val barPaysAgain = db.bankTransactionCreate(barPaysFoo) - assert(barPaysAgain == BankTransactionResult.SUCCESS) - // Refreshing the two accounts. - barAccount = db.bankAccountGetFromOwnerId(barId) - fooAccount = db.bankAccountGetFromOwnerId(fooId) - // Foo should have returned to zero and no debt, same for Bar. - // Foo: debit -> credit - assert(fooAccount?.hasDebt == false && barAccount?.hasDebt == false) - assert(fooAccount?.balance?.equals(TalerAmount(0, 0, "KUDOS")) == true) - assert(barAccount?.balance?.equals(TalerAmount(0, 0, "KUDOS")) == true) - // Bringing Bar to debit. - val barPaysMore = db.bankTransactionCreate(barPaysFoo) - assert(barPaysMore == BankTransactionResult.SUCCESS) - barAccount = db.bankAccountGetFromOwnerId(barId) - fooAccount = db.bankAccountGetFromOwnerId(fooId) - // Bar: credit -> debit - assert(fooAccount?.hasDebt == false && barAccount?.hasDebt == true) - assert(fooAccount?.balance?.equals(TalerAmount(10, 0, "KUDOS")) == true) - assert(barAccount?.balance?.equals(TalerAmount(10, 0, "KUDOS")) == true) - } - - @Test - fun customerCreationTest() = dbSetup { db -> - assert(db.customerGetFromLogin("foo") == null) - db.customerCreate(customerFoo) - assert(db.customerGetFromLogin("foo")?.name == "Foo") - // Trigger conflict. - assert(db.customerCreate(customerFoo) == null) - } - - @Test - fun bankAccountTest() = dbSetup { db -> - val currency = "KUDOS" - assert(db.bankAccountGetFromOwnerId(1L) == null) - assert(db.customerCreate(customerFoo) != null) - assert(db.bankAccountCreate(bankAccountFoo) != null) - assert(db.bankAccountCreate(bankAccountFoo) == null) // Triggers conflict. - assert(db.bankAccountGetFromOwnerId(1L)?.balance?.equals(TalerAmount(0, 0, currency)) == true) - } - - @Test - fun withdrawalTest() = dbSetup { db -> - val uuid = UUID.randomUUID() - val currency = "KUDOS" - assert(db.customerCreate(customerFoo) != null) - assert(db.bankAccountCreate(bankAccountFoo) != null) - assert(db.customerCreate(customerBar) != null) // plays the exchange. - assert(db.bankAccountCreate(bankAccountBar) != null) - // insert new. - assertEquals(WithdrawalCreationResult.SUCCESS, db.talerWithdrawalCreate( - "bar", - uuid, - TalerAmount(1, 0, currency) - )) - // get it. - val op = db.talerWithdrawalGet(uuid) - assert(op?.walletBankAccount == 2L && op.withdrawalUuid == uuid) - // Setting the details. - assertEquals(WithdrawalSelectionResult.SUCCESS, db.talerWithdrawalSetDetails( - opUuid = uuid, - exchangePayto = IbanPayTo("payto://iban/FOO-IBAN-XYZ"), - reservePub = randEddsaPublicKey() - ).first) - val opSelected = db.talerWithdrawalGet(uuid) - assert(opSelected?.selectionDone == true && !opSelected.confirmationDone) - assert(db.talerWithdrawalConfirm(uuid, Instant.now()) == WithdrawalConfirmationResult.SUCCESS) - // Finally confirming the operation (means customer wired funds to the exchange.) - assert(db.talerWithdrawalGet(uuid)?.confirmationDone == true) - } - // Only testing the interaction between Kotlin and the DBMS. No actual logic tested. - @Test - fun historyTest() = dbSetup { db -> - val currency = "KUDOS" - db.customerCreate(customerFoo); db.bankAccountCreate(bankAccountFoo) - db.customerCreate(customerBar); db.bankAccountCreate(bankAccountBar) - assert(db.bankAccountSetMaxDebt(1L, TalerAmount(10000000, 0, currency))) - // Foo pays Bar 100 times: - for (i in 1..100) { db.bankTransactionCreate(genTx("test-$i")) } - // Testing positive delta: - val forward = db.bankPoolHistory( - params = HistoryParams( - start = 50L, - delta = 2, - poll_ms = 0 - ), - bankAccountId = 1L // asking as Foo - ) - assert(forward[0].row_id >= 50 && forward.size == 2 && forward[0].row_id < forward[1].row_id) - val backward = db.bankPoolHistory( - params = HistoryParams( - start = 50L, - delta = -2, - poll_ms = 0 - ), - bankAccountId = 1L // asking as Foo - ) - assert(backward[0].row_id <= 50 && backward.size == 2 && backward[0].row_id > backward[1].row_id) - } - @Test - fun cashoutTest() = dbSetup { db -> - val currency = "KUDOS" - val op = Cashout( - cashoutUuid = UUID.randomUUID(), - amountDebit = TalerAmount(1, 0, currency), - amountCredit = TalerAmount(2, 0, currency), - bankAccount = 1L, - buyAtRatio = 3, - buyInFee = TalerAmount(0, 22, currency), - sellAtRatio = 2, - sellOutFee = TalerAmount(0, 44, currency), - credit_payto_uri = "IBAN", - cashoutCurrency = "KUDOS", - creationTime = Instant.now(), - subject = "31st", - tanChannel = TanChannel.sms, - tanCode = "secret" - ) - val fooId = db.customerCreate(customerFoo) - assert(fooId != null) - assert(db.bankAccountCreate(bankAccountFoo) != null) - assert(db.customerCreate(customerBar) != null) - assert(db.bankAccountCreate(bankAccountBar) != null) - assert(db.cashoutCreate(op)) - val fromDb = db.cashoutGetFromUuid(op.cashoutUuid) - assert(fromDb?.subject == op.subject && fromDb.tanConfirmationTime == null) - assert(db.cashoutDelete(op.cashoutUuid) == Database.CashoutDeleteResult.SUCCESS) - assert(db.cashoutCreate(op)) - db.bankAccountSetMaxDebt( - fooId!!, - TalerAmount(100, 0, currency) - ) - assert(db.bankTransactionCreate( - BankInternalTransaction( - creditorAccountId = 2, - debtorAccountId = 1, - subject = "backing the cash-out", - amount = TalerAmount(10, 0, currency), - accountServicerReference = "acct-svcr-ref", - endToEndId = "end-to-end-id", - paymentInformationId = "pmtinfid", - transactionDate = Instant.now() - ) - ) == BankTransactionResult.SUCCESS) - // Confirming the cash-out - assert(db.cashoutConfirm(op.cashoutUuid, 1L, 1L)) - // Checking the confirmation took place. - assert(db.cashoutGetFromUuid(op.cashoutUuid)?.tanConfirmationTime != null) - // Deleting the operation. - assert(db.cashoutDelete(op.cashoutUuid) == Database.CashoutDeleteResult.CONFLICT_ALREADY_CONFIRMED) - assert(db.cashoutGetFromUuid(op.cashoutUuid) != null) // previous didn't delete. - } - - // Tests the retrieval of many accounts, used along GET /accounts - @Test - fun accountsForAdminTest() = dbSetup { db -> - assert(db.accountsGetForAdmin().isEmpty()) // No data exists yet. - assert(db.customerCreate(customerFoo) != null) - assert(db.bankAccountCreate(bankAccountFoo) != null) - assert(db.customerCreate(customerBar) != null) - assert(db.bankAccountCreate(bankAccountBar) != null) - assert(db.accountsGetForAdmin().size == 2) - assert(db.accountsGetForAdmin("F%").size == 1) // gets Foo only - assert(db.accountsGetForAdmin("%ar").size == 1) // gets Bar only - } - - @Test - fun passwordChangeTest() = dbSetup { db -> - // foo not found, this fails. - assert(!db.customerChangePassword("foo", "won't make it")) - // creating foo. - assert(db.customerCreate(customerFoo) != null) - // foo exists, this succeeds. - assert(db.customerChangePassword("foo", CryptoUtil.hashpw("new-pw"))) - } - - @Test - fun getPublicAccountsTest() = dbSetup { db -> - // Expecting empty, no accounts exist yet. - assert(db.accountsGetPublic("KUDOS").isEmpty()) - // Make a NON-public account, so expecting still an empty result. - assert(db.customerCreate(customerFoo) != null) - assert(db.bankAccountCreate(bankAccountFoo) != null) - assert(db.accountsGetPublic("KUDOS").isEmpty()) - - // Make a public account, so expecting one result. - db.customerCreate(customerBar).apply { - assert(this != null) - assert(db.bankAccountCreate( - BankAccount( - isPublic = true, - internalPaytoUri = IbanPayTo("payto://iban/non-used"), - lastNexusFetchRowId = 1L, - owningCustomerId = this!!, - hasDebt = false, - maxDebt = TalerAmount(10, 1, "KUDOS") - ) - ) != null) - } - assert(db.accountsGetPublic("KUDOS").size == 1) - - // Same expectation, filtering on the login "bar" - assert(db.accountsGetPublic("KUDOS", "b%").size == 1) - // Expecting empty, as the filter should match nothing. - assert(db.accountsGetPublic("KUDOS", "x").isEmpty()) - } - - /** - * Tests the UPDATE-based SQL function that backs the - * PATCH /accounts/foo endpoint. - */ - @Test - fun accountReconfigTest() = dbSetup { db -> - // asserting for the customer not being found. - db.accountReconfig( - "foo", - "Foo", - "payto://cashout", - "+99", - "foo@example.com", - true - ).apply { assertEquals(AccountReconfigDBResult.CUSTOMER_NOT_FOUND, this) } - // creating the customer - assertNotNull(db.customerCreate(customerFoo)) - - // asserting for the bank account not being found. - db.accountReconfig( - "foo", - "Foo", - "payto://cashout", - "+99", - "foo@example.com", - true - ).apply { assertEquals(AccountReconfigDBResult.BANK_ACCOUNT_NOT_FOUND, this) } - // Giving foo a bank account - assert(db.bankAccountCreate(bankAccountFoo) != null) - // asserting for success. - db.accountReconfig( - "foo", - "Bar", - "payto://cashout", - "+99", - "foo@example.com", - true - ).apply { assertEquals(this, AccountReconfigDBResult.SUCCESS) } - // Getting the updated account from the database and checking values. - db.customerGetFromLogin("foo").apply { - assertNotNull(this) - assert(this.login == "foo" && - this.name == "Bar" && - this.cashoutPayto == "payto://cashout" && - this.email == "foo@example.com" && - this.phone == "+99" - ) - db.bankAccountGetFromOwnerId(this.expectRowId()).apply { - assertNotNull(this) - assertTrue(this.isTalerExchange) - } - } - // Testing the null cases. - // Sets everything to null, leaving name and the Taler exchange flag untouched. - assertEquals(db.accountReconfig( - "foo", - null, - null, - null, - null, - null), - AccountReconfigDBResult.SUCCESS - ) - db.customerGetFromLogin("foo").apply { - assertNotNull(this) - assert((this.login == "foo") && - (this.name == "Bar") && - (this.cashoutPayto) == null && - (this.email) == null && - this.phone == null - ) - db.bankAccountGetFromOwnerId(this.expectRowId()).apply { - assertNotNull(this) - assertTrue(this.isTalerExchange) - } - } - } } diff --git a/bank/src/test/kotlin/WireGatewayApiTest.kt b/bank/src/test/kotlin/WireGatewayApiTest.kt @@ -17,14 +17,14 @@ import kotlin.test.assertNotNull import randHashCode class WireGatewayApiTest { - suspend fun Database.genTransfer(from: String, to: BankAccount, amount: String = "KUDOS:10") { + suspend fun Database.genTransfer(from: String, to: IbanPayTo, amount: String = "KUDOS:10") { talerTransferCreate( req = TransferRequest( request_uid = randHashCode(), amount = TalerAmount(amount), exchange_base_url = ExchangeUrl("http://exchange.example.com/"), wtid = randShortHashCode(), - credit_account = to.internalPaytoUri + credit_account = to ), username = from, timestamp = Instant.now() @@ -33,12 +33,12 @@ class WireGatewayApiTest { } } - suspend fun Database.genIncoming(to: String, from: BankAccount) { + suspend fun Database.genIncoming(to: String, from: IbanPayTo) { talerAddIncomingCreate( req = AddIncomingRequest( reserve_pub = randShortHashCode(), amount = TalerAmount(10, 0, "KUDOS"), - debit_account = from.internalPaytoUri, + debit_account = from, ), username = to, timestamp = Instant.now() @@ -246,7 +246,7 @@ class WireGatewayApiTest { // Gen three transactions using clean add incoming logic repeat(3) { - db.genIncoming("exchange", bankAccountMerchant) + db.genIncoming("exchange", IbanPayTo("payto://iban/MERCHANT-IBAN-XYZ")) } // Should not show up in the taler wire gateway API history db.bankTransactionCreate(genTx("bogus foobar")).assertSuccess() @@ -314,7 +314,7 @@ class WireGatewayApiTest { } } delay(200) - db.genIncoming("exchange", bankAccountMerchant) + db.genIncoming("exchange", IbanPayTo("payto://iban/MERCHANT-IBAN-XYZ")) } // Test trigger by raw transaction @@ -362,7 +362,7 @@ class WireGatewayApiTest { // Testing ranges. repeat(20) { - db.genIncoming("exchange", bankAccountMerchant) + db.genIncoming("exchange", IbanPayTo("payto://iban/MERCHANT-IBAN-XYZ")) } // forward range: @@ -425,7 +425,7 @@ class WireGatewayApiTest { // Gen three transactions using clean transfer logic repeat(3) { - db.genTransfer("exchange", bankAccountMerchant) + db.genTransfer("exchange", IbanPayTo("payto://iban/MERCHANT-IBAN-XYZ")) } // Should not show up in the taler wire gateway API history db.bankTransactionCreate(genTx("bogus foobar", 1, 2)).assertSuccess() @@ -478,12 +478,12 @@ class WireGatewayApiTest { } } delay(200) - db.genTransfer("exchange", bankAccountMerchant) + db.genTransfer("exchange", IbanPayTo("payto://iban/MERCHANT-IBAN-XYZ")) } // Testing ranges. repeat(20) { - db.genTransfer("exchange", bankAccountMerchant) + db.genTransfer("exchange", IbanPayTo("payto://iban/MERCHANT-IBAN-XYZ")) } // forward range: diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt @@ -15,40 +15,6 @@ import tech.libeufin.util.* /* ----- Setup ----- */ -val customerMerchant = Customer( - login = "merchant", - passwordHash = CryptoUtil.hashpw("merchant-password"), - name = "Merchant", - phone = "+00", - email = "merchant@libeufin-bank.com", - cashoutPayto = "payto://external-IBAN", - cashoutCurrency = "KUDOS" -) -val bankAccountMerchant = BankAccount( - internalPaytoUri = IbanPayTo("payto://iban/MERCHANT-IBAN-XYZ"), - lastNexusFetchRowId = 1L, - owningCustomerId = 1L, - hasDebt = false, - maxDebt = TalerAmount(10, 1, "KUDOS"), -) -val customerExchange = Customer( - login = "exchange", - passwordHash = CryptoUtil.hashpw("exchange-password"), - name = "Exchange", - phone = "+00", - email = "exchange@libeufin-bank.com", - cashoutPayto = "payto://external-IBAN", - cashoutCurrency = "KUDOS" -) -val bankAccountExchange = BankAccount( - internalPaytoUri = IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ"), - lastNexusFetchRowId = 1L, - owningCustomerId = 2L, - hasDebt = false, - maxDebt = TalerAmount(10, 1, "KUDOS"), - isTalerExchange = true -) - fun setup( conf: String = "test.conf", lambda: suspend (Database, BankConfig) -> Unit @@ -72,19 +38,35 @@ fun bankSetup( ) { setup(conf) { db, ctx -> // Creating the exchange and merchant accounts first. - assertNotNull(db.customerCreate(customerMerchant)) - assertNotNull(db.bankAccountCreate(bankAccountMerchant)) - assertNotNull(db.customerCreate(customerExchange)) - assertNotNull(db.bankAccountCreate(bankAccountExchange)) - // Create admin account - assertNotNull(db.customerCreate( - Customer( - "admin", - CryptoUtil.hashpw("admin-password"), - "CFO" - ) + assertNotNull(db.accountCreate( + login = "merchant", + passwordHash = CryptoUtil.hashpw("merchant-password"), + name = "Merchant", + internalPaytoUri = IbanPayTo("payto://iban/MERCHANT-IBAN-XYZ"), + maxDebt = TalerAmount(10, 1, "KUDOS"), + isTalerExchange = false, + isPublic = false + )) + assertNotNull(db.accountCreate( + login = "exchange", + passwordHash = CryptoUtil.hashpw("exchange-password"), + name = "Exchange", + internalPaytoUri = IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ"), + maxDebt = TalerAmount(10, 1, "KUDOS"), + isTalerExchange = true, + isPublic = false )) - assert(maybeCreateAdminAccount(db, ctx)) + assertNotNull(db.accountCreate( + login = "customer", + passwordHash = CryptoUtil.hashpw("customer-password"), + name = "Customer", + internalPaytoUri = IbanPayTo("payto://iban/CUSTOMER-IBAN-XYZ"), + maxDebt = TalerAmount(10, 1, "KUDOS"), + isTalerExchange = false, + isPublic = false + )) + // Create admin account + assert(maybeCreateAdminAccount(db, ctx, "admin-password")) testApplication { application { corebankWebApp(db, ctx) @@ -178,12 +160,14 @@ class JsonBuilder(from: JsonObject) { /* ----- Random data generation ----- */ -fun randBase32Crockford(lenght: Int): String { +fun randBytes(lenght: Int): ByteArray { val bytes = ByteArray(lenght) kotlin.random.Random.nextBytes(bytes) - return Base32Crockford.encode(bytes) + return bytes } +fun randBase32Crockford(lenght: Int) = Base32Crockford.encode(randBytes(lenght)) + fun randHashCode(): HashCode = HashCode(randBase32Crockford(64)) fun randShortHashCode(): ShortHashCode = ShortHashCode(randBase32Crockford(32)) fun randEddsaPublicKey(): EddsaPublicKey = EddsaPublicKey(randBase32Crockford(32)) \ No newline at end of file