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:
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