libeufin

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

commit bb3d2eea0671952b509f16b99f4aba6ef33142fb
parent 8e2097ff41ee020d764cc14a3d14654c71f97906
Author: MS <ms@taler.net>
Date:   Fri, 24 Feb 2023 19:39:20 +0100

Cascade-deleting when deleting a user.

Diffstat:
Mnexus/src/test/kotlin/SandboxCircuitApiTest.kt | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt | 16+++++++++++-----
Msandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt | 17++++++++++++++---
3 files changed, 97 insertions(+), 12 deletions(-)

diff --git a/nexus/src/test/kotlin/SandboxCircuitApiTest.kt b/nexus/src/test/kotlin/SandboxCircuitApiTest.kt @@ -11,6 +11,7 @@ import org.junit.Ignore import org.junit.Test import tech.libeufin.sandbox.* import java.io.File +import java.math.BigDecimal import java.util.* class SandboxCircuitApiTest { @@ -28,6 +29,10 @@ class SandboxCircuitApiTest { } } + /** + * Checking that the ordinary user foo doesn't get to access bar's + * data, but admin does. + */ @Test fun accessAccountsTest() { withTestDatabase { @@ -258,12 +263,12 @@ class SandboxCircuitApiTest { basicAuth("shop", "secret") setBody("""{ "amount_debit": "TESTKUDOS:20", - "amount_credit": "KUDOS:19", + "amount_credit": "CHF:19", "tan_channel": "file" }""".trimIndent()) } assert(R.status.value == HttpStatusCode.Accepted.value) - var operationUuid = mapper.readTree(R.readBytes()).get("uuid").asText() + val operationUuid = mapper.readTree(R.readBytes()).get("uuid").asText() // Check that the operation is found by the bank. R = client.get("/demobanks/default/circuit-api/cashouts/${operationUuid}") { // Asking as the Admin but for the 'shop' account. @@ -290,14 +295,37 @@ class SandboxCircuitApiTest { } respJson = mapper.readTree(R.bodyAsText()) assert(respJson.get("balance").get("amount").asText() == balanceAfterCashout) - + // Attempt to cash-out with wrong regional currency. + R = client.post("/demobanks/default/circuit-api/cashouts") { + contentType(ContentType.Application.Json) + basicAuth("shop", "secret") + setBody("""{ + "amount_debit": "NOTFOUND:20", + "amount_credit": "CHF:19", + "tan_channel": "file" + }""".trimIndent()) + expectSuccess = false + } + assert(R.status.value == HttpStatusCode.BadRequest.value) + // Attempt to cash-out with wrong fiat currency. + R = client.post("/demobanks/default/circuit-api/cashouts") { + contentType(ContentType.Application.Json) + basicAuth("shop", "secret") + setBody("""{ + "amount_debit": "TESTKUDOS:20", + "amount_credit": "NOTFOUND:19", + "tan_channel": "file" + }""".trimIndent()) + expectSuccess = false + } + assert(R.status.value == HttpStatusCode.BadRequest.value) // Create a new cash-out and delete it. R = client.post("/demobanks/default/circuit-api/cashouts") { contentType(ContentType.Application.Json) basicAuth("shop", "secret") setBody("""{ "amount_debit": "TESTKUDOS:20", - "amount_credit": "KUDOS:19", + "amount_credit": "CHF:19", "tan_channel": "file" }""".trimIndent()) } @@ -451,6 +479,46 @@ class SandboxCircuitApiTest { } } + /** + * Testing that deleting a user doesn't cause a _different_ user + * to lose data. + */ + @Test + fun deletionIsolation() { + withTestDatabase { + prepSandboxDb() + transaction { + // Admin makes sure foo has balance 100. + wireTransfer( + "admin", + "foo", + subject = "set to 100", + amount = "TESTKUDOS:100" + ) + val fooBalance = getBalance("foo") + assert(fooBalance == BigDecimal("100")) + // Foo pays 3 to bar. + wireTransfer( + "foo", + "bar", + subject = "donation", + amount = "TESTKUDOS:3" + ) + val barBalance = getBalance("bar") + assert(barBalance == BigDecimal("3")) + // Deleting foo from the system. + transaction { + val uBankAccount = getBankAccountFromLabel("foo") + val uCustomerProfile = getCustomer("foo") + uBankAccount.delete() + uCustomerProfile.delete() + } + val barBalanceUpdate = getBalance("bar") + assert(barBalance == BigDecimal("3")) + } + } + } + @Test fun tanCommandTest() { /** diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt @@ -33,13 +33,13 @@ data class CircuitCashoutRequest( */ val tan_channel: String? ) - +const val FIAT_CURRENCY = "CHF" // FIXME: make configurable. // Configuration response: data class ConfigResp( val name: String = "circuit", val version: String = SANDBOX_VERSION, val ratios_and_fees: RatioAndFees, - val fiat_currency: String = "CHF" // FIXME: make configurable. + val fiat_currency: String = FIAT_CURRENCY ) // After fixing #7527, the values held by this @@ -370,10 +370,16 @@ fun circuitApi(circuitRoute: Route) { val amountDebit = parseAmount(req.amount_debit) // amount before rates. val amountCredit = parseAmount(req.amount_credit) // amount after rates, as expected by the client val demobank = ensureDemobank(call) + // Currency check of the cash-out's circuit part. if (amountDebit.currency != demobank.currency) - throw badRequest("The '${req::amount_debit.name}' field has the wrong currency") - if (amountCredit.currency == demobank.currency) - throw badRequest("The '${req::amount_credit.name}' field didn't change the currency.") + throw badRequest("'${req::amount_debit.name}' (${req.amount_debit})" + + " doesn't match the regional currency (${demobank.currency})" + ) + // Currency check of the cash-out's fiat part. + if (amountCredit.currency != FIAT_CURRENCY) + throw badRequest("'${req::amount_credit.name}' (${req.amount_credit})" + + " doesn't match the fiat currency ($FIAT_CURRENCY)." + ) // check if TAN is supported. Default to SMS, if that's missing. val tanChannel = req.tan_channel?.uppercase() ?: SupportedTanChannels.SMS.name if (!isTanChannelSupported(tanChannel)) diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt @@ -182,7 +182,11 @@ object EbicsSubscribersTable : IntIdTable() { val authenticationKey = reference("authorizationKey", EbicsSubscriberPublicKeysTable).nullable() val nextOrderID = integer("nextOrderID") val state = enumeration("state", SubscriberState::class) - val bankAccount = reference("bankAccount", BankAccountsTable).nullable() + val bankAccount = reference( + "bankAccount", + BankAccountsTable, + onDelete = ReferenceOption.CASCADE + ).nullable() } class EbicsSubscriberEntity(id: EntityID<Int>) : IntEntity(id) { @@ -297,7 +301,11 @@ class EbicsUploadTransactionChunkEntity(id: EntityID<String>) : Entity<String>(i * to the main ledger. */ object BankAccountFreshTransactionsTable : LongIdTable() { - val transactionRef = reference("transaction", BankAccountTransactionsTable) + val transactionRef = reference( + "transaction", + BankAccountTransactionsTable, + onDelete = ReferenceOption.CASCADE + ) } class BankAccountFreshTransactionEntity(id: EntityID<Long>) : LongEntity(id) { companion object : LongEntityClass<BankAccountFreshTransactionEntity>(BankAccountFreshTransactionsTable) @@ -331,7 +339,10 @@ object BankAccountTransactionsTable : LongIdTable() { * Bank account of the party whose 'direction' refers. This version allows * only both parties to be registered at the running Sandbox. */ - val account = reference("account", BankAccountsTable) + val account = reference( + "account", BankAccountsTable, + onDelete = ReferenceOption.CASCADE + ) // Redundantly storing the demobank for query convenience. val demobank = reference("demobank", DemobankConfigsTable) }