libeufin

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

commit 101fa06fd5fa863a90eb4cbc0b4fcd2e28fda583
parent f2710520b2a162b7c162e5f375cb08f952f2b739
Author: MS <ms@taler.net>
Date:   Fri,  7 Apr 2023 17:14:50 +0200

Addressing #7788

Diffstat:
Mcli/tests/launch_services.sh | 6+++---
Mnexus/src/test/kotlin/MakeEnv.kt | 18+++++++++++++++---
Mnexus/src/test/kotlin/SandboxCircuitApiTest.kt | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mnexus/src/test/kotlin/SandboxLegacyApiTest.kt | 1-
Msandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt | 36++++++++++++++++++++++++++----------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt | 2+-
Msandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt | 14+++++++++++++-
7 files changed, 124 insertions(+), 19 deletions(-)

diff --git a/cli/tests/launch_services.sh b/cli/tests/launch_services.sh @@ -4,8 +4,8 @@ # connected through x-libeufin-bank. set -eu -WITH_TASKS=1 -# WITH_TASKS=0 +# WITH_TASKS=1 +WITH_TASKS=0 function exit_cleanup() { echo "Running exit-cleanup" @@ -41,7 +41,7 @@ libeufin-sandbox \ echo DONE echo -n Start the bank... export LIBEUFIN_SANDBOX_ADMIN_PASSWORD=foo -libeufin-sandbox serve &> sandbox.log & +libeufin-sandbox serve > sandbox.log 2>&1 & SANDBOX_PID=$! echo DONE echo -n Wait for the bank... diff --git a/nexus/src/test/kotlin/MakeEnv.kt b/nexus/src/test/kotlin/MakeEnv.kt @@ -278,11 +278,23 @@ fun prepSandboxDb(usersDebtLimit: Int = 1000) { name = "Bar" cashout_address = "payto://iban/FIAT" } + // Note: exchange doesn't have the cash-out address. DemobankCustomerEntity.new { - username = "baz" + username = "exchange-0" passwordHash = CryptoUtil.hashpw("foo") - name = "Baz" - cashout_address = "payto://iban/OTHERBANK" + name = "Exchange" + } + BankAccountEntity.new { + iban = "AT561936082973364859" + /** + * For now, keep same semantics of Pybank: a username + * is AS WELL a bank account label. In other words, it + * identifies a customer AND a bank account. + */ + label = "exchange-0" + owner = "exchange-0" + this.demoBank = demoBank + isPublic = false } } } diff --git a/nexus/src/test/kotlin/SandboxCircuitApiTest.kt b/nexus/src/test/kotlin/SandboxCircuitApiTest.kt @@ -11,12 +11,78 @@ import org.jetbrains.exposed.sql.transactions.transaction import org.junit.Ignore import org.junit.Test import tech.libeufin.sandbox.* +import tech.libeufin.util.getIban import tech.libeufin.util.parseAmount import java.io.File import java.math.BigDecimal import java.util.* class SandboxCircuitApiTest { + + /** + * Testing that the admin is able to conduct ordinary + * account operations even on non-circuit accounts. Recall: + * such accounts are just those without the cash-out address. + */ + @Test + fun opOnNonCircuitAccounts() { + withTestDatabase { + testApplication { + prepSandboxDb() + testApplication { + application(sandboxApp) + // Only testing that this doesn't except. + client.get("/demobanks/default/circuit-api/accounts") { + expectSuccess = true + basicAuth("admin", "foo") + } + // Trying to PATCH non circuit account + client.patch("/demobanks/default/circuit-api/accounts/exchange-0") { + expectSuccess = true + basicAuth("admin", "foo") + contentType(ContentType.Application.Json) + setBody(""" + {"name": "Exchange 0", + "contact_data": {}, + "cashout_address": "payto://iban/SANDBOXX/${getIban()}" + } + """.trimIndent()) + } + // PATCH it again passing a null name and cashout-address. + client.patch("/demobanks/default/circuit-api/accounts/exchange-0") { + expectSuccess = true + basicAuth("admin", "foo") + contentType(ContentType.Application.Json) + setBody("{ \"contact_data\": {} }") + } + // PATCH the password. + client.patch("/demobanks/default/circuit-api/accounts/exchange-0/auth") { + expectSuccess = true + basicAuth("admin", "foo") + contentType(ContentType.Application.Json) + setBody("{ \"new_password\": \"secret\" }") + } + // Check that PATCHing worked. + client.get("/demobanks/default/access-api/accounts/exchange-0") { + expectSuccess = true + basicAuth("exchange-0", "secret") + contentType(ContentType.Application.Json) + } + // Deleting the account. + client.delete("/demobanks/default/circuit-api/accounts/exchange-0") { + expectSuccess = true + basicAuth("admin", "foo") + } + // Checking actual deletion. + val R = client.get("/demobanks/default/circuit-api/accounts/exchange-0") { + expectSuccess = false + basicAuth("admin", "foo") + } + assert(R.status.value == HttpStatusCode.NotFound.value) + } + } + } + } // Get /config, fails if != 200. @Test fun config() { diff --git a/nexus/src/test/kotlin/SandboxLegacyApiTest.kt b/nexus/src/test/kotlin/SandboxLegacyApiTest.kt @@ -112,7 +112,6 @@ class SandboxLegacyApiTest { }) client.post("/admin/ebics/bank-accounts") { expectSuccess = true - expectSuccess = true contentType(ContentType.Application.Json) basicAuth("admin", "foo") setBody(body) diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt @@ -6,6 +6,8 @@ import io.ktor.http.* import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.like import org.jetbrains.exposed.sql.transactions.transaction import tech.libeufin.sandbox.CashoutOperationsTable.uuid import tech.libeufin.util.* @@ -79,7 +81,7 @@ data class CircuitContactData( data class CircuitAccountReconfiguration( val contact_data: CircuitContactData, - val cashout_address: String, + val cashout_address: String?, val name: String? = null ) @@ -96,7 +98,7 @@ data class CircuitAccountInfo( val iban: String, val contact_data: CircuitContactData, val name: String, - val cashout_address: String + val cashout_address: String? ) data class CashoutOperationInfo( @@ -631,11 +633,12 @@ fun circuitApi(circuitRoute: Route) { * that the customer was indeed added via the Circuit API, as opposed * to the Access API. */ - val maybeError = "$resourceName not managed by the Circuit API." call.respond(CircuitAccountInfo( username = customer.username, - name = customer.name ?: throw notFound(maybeError), - cashout_address = customer.cashout_address ?: throw notFound(maybeError), + name = customer.name ?: throw internalServerError( + "Account '$resourceName' was found without owner's name." + ), + cashout_address = customer.cashout_address, contact_data = CircuitContactData( email = customer.email, phone = customer.phone @@ -659,10 +662,22 @@ fun circuitApi(circuitRoute: Route) { val customers = mutableListOf<Any>() val demobank = ensureDemobank(call) transaction { - DemobankCustomerEntity.find{ - // like() is case insensitive. - DemobankCustomersTable.name.like(filter) - }.forEach { + /** + * This block builds the DB query so that IF the %-wildcard was + * given, then BOTH name and name-less accounts are returned. + */ + val query: Op<Boolean> = SqlExpressionBuilder.run { + val like = DemobankCustomersTable.name.like(filter) + /** + * This IF statement is needed because Postgres would NOT + * match a null column even with the %-wildcard. + */ + if (filter == "%") { + return@run like.or(DemobankCustomersTable.name.isNull()) + } + return@run like + } + DemobankCustomerEntity.find { query }.forEach { customers.add(object { val username = it.username val name = it.name @@ -676,6 +691,7 @@ fun circuitApi(circuitRoute: Route) { ) }) } + StdOutSqlLogger } if (customers.size == 0) { call.respond(HttpStatusCode.NoContent) @@ -727,7 +743,7 @@ fun circuitApi(circuitRoute: Route) { throw badRequest("Invalid e-mail address: ${req.contact_data.email}") if ((req.contact_data.phone != null) && (!checkPhoneNumber(req.contact_data.phone))) throw badRequest("Invalid phone number: ${req.contact_data.phone}") - try { parsePayto(req.cashout_address) } + try { if (req.cashout_address != null) parsePayto(req.cashout_address) } catch (e: InvalidPaytoError) { throw badRequest("Invalid cash-out address: ${req.cashout_address}") } diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt @@ -80,7 +80,7 @@ data class AccountPair( ) fun insertNewAccount(username: String, password: String, - name: String? = null, // tests do not usually give one. + name: String? = null, // tests and access API may not give one. iban: String? = null, demobank: String = "default", isPublic: Boolean = false): AccountPair { diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt @@ -111,7 +111,19 @@ fun getBalance(accountLabel: String, HttpStatusCode.InternalServerError, "Demobank '$demobankName' not found" ) - val account = getBankAccountFromLabel(accountLabel, demobank) + + /** + * Setting withBankFault to true for the following reason: + * when asking for a balance, the bank should have made sure + * that the user has a bank account (together with a customer profile). + * If that's not the case, it's bank's fault, since it didn't check + * earlier. + */ + val account = getBankAccountFromLabel( + accountLabel, + demobank, + withBankFault = true + ) return getBalance(account, withPending) }