libeufin

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

commit dc895a89e77636bb46964ffbfd2d4d9194c6f286
parent 3de53c1b7ed09bb61d375f33770dd5dc109576cc
Author: Antoine A <>
Date:   Mon, 16 Oct 2023 11:45:45 +0000

Improve and fix transaction creation endpoint

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt | 52++++++++++++++++------------------------------------
Mbank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt | 6+++---
Abank/src/test/kotlin/CoreBankApiTest.kt | 810+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dbank/src/test/kotlin/LibeuFinApiTest.kt | 807-------------------------------------------------------------------------------
4 files changed, 829 insertions(+), 846 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt @@ -118,16 +118,9 @@ fun Routing.accountsMgmtApi(db: Database, ctx: BankApplicationContext) { val publicAccounts = db.accountsGetPublic(ctx.currency) if (publicAccounts.isEmpty()) { call.respond(HttpStatusCode.NoContent) - return@get + } else { + call.respond(PublicAccountsResponse(publicAccounts)) } - call.respond( - PublicAccountsResponse().apply { - publicAccounts.forEach { - this.public_accounts.add(it) - } - } - ) - return@get } get("/accounts") { val c = call.authenticateBankRequest(db, TokenScope.readonly) ?: throw unauthorized() @@ -138,19 +131,12 @@ fun Routing.accountsMgmtApi(db: Database, ctx: BankApplicationContext) { val queryParam = if (maybeFilter != null) { "%${maybeFilter}%" } else "%" - val dbRes = db.accountsGetForAdmin(queryParam) - if (dbRes.isEmpty()) { + val accounts = db.accountsGetForAdmin(queryParam) + if (accounts.isEmpty()) { call.respond(HttpStatusCode.NoContent) - return@get + } else { + call.respond(ListBankAccountsResponse(accounts)) } - call.respond( - ListBankAccountsResponse().apply { - dbRes.forEach { element -> - this.accounts.add(element) - } - } - ) - return@get } post("/accounts") { // check if only admin is allowed to create new accounts if (ctx.restrictRegistration) { @@ -514,21 +500,17 @@ fun Routing.accountsMgmtApi(db: Database, ctx: BankApplicationContext) { val resourceName = call.expectUriComponent("USERNAME") // admin has no rights here. if ((c.login != resourceName) && (call.getAuthToken() == null)) throw forbidden() val tx = call.receive<BankAccountTransactionCreate>() + val subject = tx.payto_uri.message ?: throw badRequest("Wire transfer lacks subject") - val debtorBankAccount = db.bankAccountGetFromOwnerId(c.expectRowId()) - ?: throw internalServerError("Debtor bank account not found") - if (tx.amount.currency != ctx.currency) throw badRequest( - "Wrong currency: ${tx.amount.currency}", + val amount = tx.payto_uri.amount ?: tx.amount + if (amount == null) throw badRequest("Wire transfer lacks amount") + if (amount.currency != ctx.currency) throw badRequest( + "Wrong currency: ${amount.currency}", talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH ) - if (!isBalanceEnough( - balance = debtorBankAccount.expectBalance(), - due = tx.amount, - hasBalanceDebt = debtorBankAccount.hasDebt, - maxDebt = debtorBankAccount.maxDebt - )) - throw conflict(hint = "Insufficient balance.", talerEc = TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT) - logger.info("creditor payto: ${tx.payto_uri}") + // TODO rewrite all thos database query in a single database function + val debtorBankAccount = db.bankAccountGetFromOwnerId(c.expectRowId()) + ?: throw internalServerError("Debtor bank account not found") val creditorBankAccount = db.bankAccountGetFromInternalPayto(tx.payto_uri) ?: throw notFound( "Creditor account not found", @@ -538,11 +520,10 @@ fun Routing.accountsMgmtApi(db: Database, ctx: BankApplicationContext) { debtorAccountId = debtorBankAccount.expectRowId(), creditorAccountId = creditorBankAccount.expectRowId(), subject = subject, - amount = tx.amount, + amount = amount, transactionDate = Instant.now() ) - val res = db.bankTransactionCreate(dbInstructions) - when (res) { + when (db.bankTransactionCreate(dbInstructions)) { BankTransactionResult.BALANCE_INSUFFICIENT -> throw conflict( "Insufficient funds", TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT @@ -555,7 +536,6 @@ fun Routing.accountsMgmtApi(db: Database, ctx: BankApplicationContext) { BankTransactionResult.NO_DEBTOR -> throw internalServerError("Debtor not found despite the request was authenticated.") BankTransactionResult.SUCCESS -> call.respond(HttpStatusCode.OK) } - return@post } get("/accounts/{USERNAME}/transactions/{T_ID}") { val c = call.authenticateBankRequest(db, TokenScope.readonly) ?: throw unauthorized() diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt @@ -335,7 +335,7 @@ data class AccountMinimalData( */ @Serializable data class ListBankAccountsResponse( - val accounts: MutableList<AccountMinimalData> = mutableListOf() + val accounts: List<AccountMinimalData> ) /** @@ -357,7 +357,7 @@ data class AccountData( @Serializable data class BankAccountTransactionCreate( val payto_uri: IbanPayTo, - val amount: TalerAmount + val amount: TalerAmount? ) /* History element, either from GET /transactions/T_ID @@ -602,7 +602,7 @@ data class TransferResponse( */ @Serializable data class PublicAccountsResponse( - val public_accounts: MutableList<PublicAccount> = mutableListOf() + val public_accounts: List<PublicAccount> ) /** diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt @@ -0,0 +1,810 @@ +import io.ktor.client.plugins.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.http.content.* +import io.ktor.server.engine.* +import io.ktor.server.testing.* +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import net.taler.wallet.crypto.Base32Crockford +import org.junit.Test +import org.postgresql.jdbc.PgConnection +import tech.libeufin.bank.* +import tech.libeufin.util.CryptoUtil +import java.sql.DriverManager +import java.time.Duration +import java.time.Instant +import java.time.temporal.ChronoUnit +import kotlin.random.Random +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull + +class LibeuFinApiTest { + private val customerFoo = Customer( + login = "foo", + passwordHash = CryptoUtil.hashpw("pw"), + name = "Foo", + phone = "+00", + email = "foo@b.ar", + cashoutPayto = "payto://external-IBAN", + cashoutCurrency = "KUDOS" + ) + private val customerBar = Customer( + login = "bar", + passwordHash = CryptoUtil.hashpw("pw"), + name = "Bar", + phone = "+99", + email = "bar@example.com", + cashoutPayto = "payto://external-IBAN", + cashoutCurrency = "KUDOS" + ) + + private fun genBankAccount(rowId: Long) = BankAccount( + hasDebt = false, + internalPaytoUri = IbanPayTo("payto://iban/ac${rowId}"), + maxDebt = TalerAmount(100, 0, "KUDOS"), + owningCustomerId = rowId + ) + + @Test + fun getConfig() = bankSetup { _ -> + val r = client.get("/config") { + expectSuccess = true + }.assertOk() + println(r.bodyAsText()) + } + + /** + * Testing GET /transactions. This test checks that the sign + * of delta gets honored by the HTTP handler, namely that the + * records appear in ASC or DESC order, according to the sign + * of delta. + */ + @Test + fun testHistory() = setup { db, ctx -> + // TODO add better tests with lon polling like Wire Gateway API + val fooId = db.customerCreate(customerFoo); assert(fooId != null) + assert(db.bankAccountCreate(genBankAccount(fooId!!)) != null) + val barId = db.customerCreate(customerBar); assert(barId != null) + assert(db.bankAccountCreate(genBankAccount(barId!!)) != null) + for (i in 1..10) { + db.bankTransactionCreate(genTx("test-$i")) + } + testApplication { + application { + corebankWebApp(db, ctx) + } + val asc = client.get("/accounts/foo/transactions?delta=2") { + basicAuth("foo", "pw") + expectSuccess = true + } + var obj = Json.decodeFromString<BankAccountTransactionsResponse>(asc.bodyAsText()) + assert(obj.transactions.size == 2) + assert(obj.transactions[0].row_id < obj.transactions[1].row_id) + val desc = client.get("/accounts/foo/transactions?delta=-2") { + basicAuth("foo", "pw") + expectSuccess = true + } + obj = Json.decodeFromString(desc.bodyAsText()) + assert(obj.transactions.size == 2) + assert(obj.transactions[0].row_id > obj.transactions[1].row_id) + } + } + + // Testing the creation of bank transactions. + @Test + fun postTransactionsTest() = bankSetup { _ -> + val valid_req = json { + "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ?message=payout" + "amount" to "KUDOS:0.3" + } + + // Check ok + client.post("/accounts/merchant/transactions") { + basicAuth("merchant", "merchant-password") + jsonBody(valid_req) + }.assertOk() + client.get("/accounts/merchant/transactions/1") { + basicAuth("merchant", "merchant-password") + }.assertOk().run { + val tx: BankAccountTransactionInfo = Json.decodeFromString(bodyAsText()) + assertEquals("payout", tx.subject) + assertEquals(TalerAmount("KUDOS:0.3"), tx.amount) + } + // Check amount in payto_uri + client.post("/accounts/merchant/transactions") { + basicAuth("merchant", "merchant-password") + jsonBody(json { + "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ?message=payout2&amount=KUDOS:1.05" + }) + }.assertOk() + client.get("/accounts/merchant/transactions/3") { + basicAuth("merchant", "merchant-password") + }.assertOk().run { + val tx: BankAccountTransactionInfo = Json.decodeFromString(bodyAsText()) + assertEquals("payout2", tx.subject) + assertEquals(TalerAmount("KUDOS:1.05"), tx.amount) + } + // Check amount in payto_uri precedence + client.post("/accounts/merchant/transactions") { + basicAuth("merchant", "merchant-password") + jsonBody(json { + "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ?message=payout3&amount=KUDOS:1.05" + "amount" to "KUDOS:10.003" + }) + }.assertOk() + client.get("/accounts/merchant/transactions/5") { + basicAuth("merchant", "merchant-password") + }.assertOk().run { + val tx: BankAccountTransactionInfo = Json.decodeFromString(bodyAsText()) + assertEquals("payout3", tx.subject) + assertEquals(TalerAmount("KUDOS:1.05"), tx.amount) + } + // Testing the wrong currency + client.post("/accounts/merchant/transactions") { + basicAuth("merchant", "merchant-password") + jsonBody(json(valid_req) { + "amount" to "EUR:3.3" + }) + }.assertBadRequest() + // Surpassing the debt limit + client.post("/accounts/merchant/transactions") { + basicAuth("merchant", "merchant-password") + contentType(ContentType.Application.Json) + jsonBody(json(valid_req) { + "amount" to "KUDOS:555" + }) + }.assertStatus(HttpStatusCode.Conflict) + // Missing message + client.post("/accounts/merchant/transactions") { + basicAuth("merchant", "merchant-password") + contentType(ContentType.Application.Json) + jsonBody(json(valid_req) { + "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ" + }) + }.assertBadRequest() + // Unknown account + client.post("/accounts/merchant/transactions") { + basicAuth("merchant", "merchant-password") + contentType(ContentType.Application.Json) + jsonBody(json(valid_req) { + "payto_uri" to "payto://iban/UNKNOWN-IBAN-XYZ?message=payout" + }) + }.assertStatus(HttpStatusCode.NotFound) + // Transaction to self + client.post("/accounts/merchant/transactions") { + basicAuth("merchant", "merchant-password") + contentType(ContentType.Application.Json) + jsonBody(json(valid_req) { + "payto_uri" to "payto://iban/MERCHANT-IBAN-XYZ?message=payout" + }) + }.assertStatus(HttpStatusCode.Conflict) + } + + @Test + fun passwordChangeTest() = setup { db, ctx -> + assert(db.customerCreate(customerFoo) != null) + testApplication { + application { + corebankWebApp(db, ctx) + } + // Changing the password. + client.patch("/accounts/foo/auth") { + expectSuccess = true + contentType(ContentType.Application.Json) + basicAuth("foo", "pw") + setBody("""{"new_password": "bar"}""") + } + // Previous password should fail. + client.patch("/accounts/foo/auth") { + expectSuccess = false + contentType(ContentType.Application.Json) + basicAuth("foo", "pw") + setBody("""{"new_password": "not-even-parsed"}""") + }.apply { + assert(this.status == HttpStatusCode.Unauthorized) + } + // New password should succeed. + client.patch("/accounts/foo/auth") { + expectSuccess = true + contentType(ContentType.Application.Json) + basicAuth("foo", "bar") + setBody("""{"new_password": "not-used"}""") + } + } + } + @Test + fun tokenDeletionTest() = setup { db, ctx -> + assert(db.customerCreate(customerFoo) != null) + val token = ByteArray(32) + Random.nextBytes(token) + assert(db.bearerTokenCreate( + BearerToken( + bankCustomer = 1L, + content = token, + creationTime = Instant.now(), + expirationTime = Instant.now().plusSeconds(10), + scope = TokenScope.readwrite + ) + )) + testApplication { + application { + corebankWebApp(db, ctx) + } + // Legitimate first attempt, should succeed + client.delete("/accounts/foo/token") { + expectSuccess = true + headers["Authorization"] = "Bearer secret-token:${Base32Crockford.encode(token)}" + }.apply { + assert(this.status == HttpStatusCode.NoContent) + } + // Trying after deletion should hit 404. + client.delete("/accounts/foo/token") { + expectSuccess = false + headers["Authorization"] = "Bearer secret-token:${Base32Crockford.encode(token)}" + }.apply { + assert(this.status == HttpStatusCode.Unauthorized) + } + // Checking foo can still be served by basic auth, after token deletion. + assert(db.bankAccountCreate( + BankAccount( + hasDebt = false, + internalPaytoUri = IbanPayTo("payto://iban/DE1234"), + maxDebt = TalerAmount(100, 0, "KUDOS"), + owningCustomerId = 1 + ) + ) != null) + client.get("/accounts/foo") { + expectSuccess = true + basicAuth("foo", "pw") + } + } + } + + @Test + fun publicAccountsTest() = setup { db, ctx -> + testApplication { + application { + corebankWebApp(db, ctx) + } + client.get("/public-accounts").apply { + assert(this.status == HttpStatusCode.NoContent) + } + // Make one public account. + 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 + ) + } + client.get("/public-accounts").apply { + assert(this.status == HttpStatusCode.OK) + val obj = Json.decodeFromString<PublicAccountsResponse>(this.bodyAsText()) + assert(obj.public_accounts.size == 1) + assert(obj.public_accounts[0].account_name == "bar") + } + } + } + // Creating token with "forever" duration. + @Test + fun tokenForeverTest() = setup { db, ctx -> + assert(db.customerCreate(customerFoo) != null) + testApplication { + application { + corebankWebApp(db, ctx) + } + val newTok = client.post("/accounts/foo/token") { + expectSuccess = true + contentType(ContentType.Application.Json) + basicAuth("foo", "pw") + setBody( + """ + {"duration": {"d_us": "forever"}, "scope": "readonly"} + """.trimIndent() + ) + } + val newTokObj = Json.decodeFromString<TokenSuccessResponse>(newTok.bodyAsText()) + assert(newTokObj.expiration.t_s == Instant.MAX) + } + } + + // Testing that too big or invalid durations fail the request. + @Test + fun tokenInvalidDurationTest() = setup { db, ctx -> + assert(db.customerCreate(customerFoo) != null) + testApplication { + application { + corebankWebApp(db, ctx) + } + var r = client.post("/accounts/foo/token") { + expectSuccess = false + contentType(ContentType.Application.Json) + basicAuth("foo", "pw") + setBody("""{ + "duration": {"d_us": "invalid"}, + "scope": "readonly"}""".trimIndent()) + } + assert(r.status == HttpStatusCode.BadRequest) + r = client.post("/accounts/foo/token") { + expectSuccess = false + contentType(ContentType.Application.Json) + basicAuth("foo", "pw") + setBody("""{ + "duration": {"d_us": ${Long.MAX_VALUE}}, + "scope": "readonly"}""".trimIndent()) + } + assert(r.status == HttpStatusCode.BadRequest) + r = client.post("/accounts/foo/token") { + expectSuccess = false + contentType(ContentType.Application.Json) + basicAuth("foo", "pw") + setBody("""{ + "duration": {"d_us": -1}, + "scope": "readonly"}""".trimIndent()) + } + assert(r.status == HttpStatusCode.BadRequest) + } + } + // Checking the POST /token handling. + @Test + fun tokenTest() = setup { db, ctx -> + assert(db.customerCreate(customerFoo) != null) + testApplication { + application { + corebankWebApp(db, ctx) + } + val newTok = client.post("/accounts/foo/token") { + expectSuccess = true + contentType(ContentType.Application.Json) + basicAuth("foo", "pw") + setBody( + """ + {"scope": "readonly"} + """.trimIndent() + ) + } + // Checking that the token lifetime defaulted to 24 hours. + val newTokObj = Json.decodeFromString<TokenSuccessResponse>(newTok.bodyAsText()) + val newTokDb = db.bearerTokenGet(Base32Crockford.decode(newTokObj.access_token)) + val lifeTime = Duration.between(newTokDb!!.creationTime, newTokDb.expirationTime) + assert(lifeTime == Duration.ofDays(1)) + + // foo tries to create a token on behalf of bar, expect 403. + val r = client.post("/accounts/bar/token") { + expectSuccess = false + basicAuth("foo", "pw") + } + assert(r.status == HttpStatusCode.Forbidden) + // Make ad-hoc token for foo. + val fooTok = ByteArray(32).apply { Random.nextBytes(this) } + assert( + db.bearerTokenCreate( + BearerToken( + content = fooTok, + bankCustomer = 1L, // only foo exists. + scope = TokenScope.readonly, + creationTime = Instant.now(), + isRefreshable = true, + expirationTime = Instant.now().plus(1, ChronoUnit.DAYS) + ) + ) + ) + // Testing the secret-token:-scheme. + client.post("/accounts/foo/token") { + headers.set("Authorization", "Bearer secret-token:${Base32Crockford.encode(fooTok)}") + contentType(ContentType.Application.Json) + setBody("{\"scope\": \"readonly\"}") + expectSuccess = true + } + // Testing the 'forever' case. + val forever = client.post("/accounts/foo/token") { + expectSuccess = true + contentType(ContentType.Application.Json) + basicAuth("foo", "pw") + setBody("""{ + "scope": "readonly", + "duration": {"d_us": "forever"} + }""".trimIndent()) + } + val never: TokenSuccessResponse = Json.decodeFromString(forever.bodyAsText()) + assert(never.expiration.t_s == Instant.MAX) + } + } + + /** + * Testing the retrieval of account information. + * The tested logic is the one usually needed by SPAs + * to show customers their status. + */ + @Test + fun getAccountTest() = setup { db, ctx -> + // Artificially insert a customer and bank account in the database. + val customerRowId = db.customerCreate( + Customer( + "foo", + CryptoUtil.hashpw("pw"), + "Foo" + ) + ) + assert(customerRowId != null) + assert( + db.bankAccountCreate( + BankAccount( + hasDebt = false, + internalPaytoUri = IbanPayTo("payto://iban/DE1234"), + maxDebt = TalerAmount(100, 0, "KUDOS"), + owningCustomerId = customerRowId!! + ) + ) != null + ) + testApplication { + application { + corebankWebApp(db, ctx) + } + val r = client.get("/accounts/foo") { + expectSuccess = true + basicAuth("foo", "pw") + } + val obj: AccountData = Json.decodeFromString(r.bodyAsText()) + assert(obj.name == "Foo") + // Checking admin can. + val adminRowId = db.customerCreate( + Customer( + "admin", + CryptoUtil.hashpw("admin"), + "Admin" + ) + ) + assert(adminRowId != null) + assert( + db.bankAccountCreate( + BankAccount( + hasDebt = false, + internalPaytoUri = IbanPayTo("payto://iban/SANDBOXX/ADMIN-IBAN"), + maxDebt = TalerAmount(100, 0, "KUDOS"), + owningCustomerId = adminRowId!! + ) + ) != null + ) + client.get("/accounts/foo") { + expectSuccess = true + basicAuth("admin", "admin") + } + val shouldNot = client.get("/accounts/foo") { + basicAuth("not", "not") + expectSuccess = false + } + assert(shouldNot.status == HttpStatusCode.Unauthorized) + } + } + + /** + * Testing the account creation and its idempotency + */ + @Test + fun createAccountTest() = setup { db, ctx -> + testApplication { + val ibanPayto = genIbanPaytoUri() + application { + corebankWebApp(db, ctx) + } + var resp = client.post("/accounts") { + expectSuccess = false + contentType(ContentType.Application.Json) + setBody( + """{ + "username": "foo", + "password": "bar", + "name": "Jane", + "is_public": true, + "internal_payto_uri": "$ibanPayto" + }""".trimIndent() + ) + } + assert(resp.status == HttpStatusCode.Created) + // Testing idempotency. + resp = client.post("/accounts") { + expectSuccess = false + contentType(ContentType.Application.Json) + setBody( + """{ + "username": "foo", + "password": "bar", + "name": "Jane", + "is_public": true, + "internal_payto_uri": "$ibanPayto" + }""".trimIndent() + ) + } + assert(resp.status == HttpStatusCode.Created) + } + } + + /** + * Testing the account creation and its idempotency + */ + @Test + fun createTwoAccountsTest() = setup { db, ctx -> + testApplication { + application { + corebankWebApp(db, ctx) + } + var resp = client.post("/accounts") { + expectSuccess = false + contentType(ContentType.Application.Json) + setBody( + """{ + "username": "foo", + "password": "bar", + "name": "Jane" + }""".trimIndent() + ) + } + assert(resp.status == HttpStatusCode.Created) + // Test creating another account. + resp = client.post("/accounts") { + expectSuccess = false + contentType(ContentType.Application.Json) + setBody( + """{ + "username": "joe", + "password": "bar", + "name": "Joe" + }""".trimIndent() + ) + } + assert(resp.status == HttpStatusCode.Created) + } + } + + /** + * Test admin-only account creation + */ + @Test + fun createAccountRestrictedTest() = setup(conf = "test_restrict.conf") { db, ctx -> + testApplication { + application { + corebankWebApp(db, ctx) + } + + // Ordinary user tries, should fail. + var resp = client.post("/accounts") { + expectSuccess = false + basicAuth("foo", "bar") + contentType(ContentType.Application.Json) + setBody( + """{ + "username": "baz", + "password": "xyz", + "name": "Mallory" + }""".trimIndent() + ) + } + assert(resp.status == HttpStatusCode.Unauthorized) + // Creating the administrator. + assert( + db.customerCreate( + Customer( + "admin", + CryptoUtil.hashpw("pass"), + "CFO" + ) + ) != null + ) + // customer exists, this makes only the bank account: + assert(maybeCreateAdminAccount(db, ctx)) + resp = client.post("/accounts") { + expectSuccess = false + basicAuth("admin", "pass") + contentType(ContentType.Application.Json) + setBody( + """{ + "username": "baz", + "password": "xyz", + "name": "Mallory" + }""".trimIndent() + ) + } + assert(resp.status == HttpStatusCode.Created) + } + } + + /** + * Tests DELETE /accounts/foo + */ + @Test + fun deleteAccount() = setup { db, ctx -> + val adminCustomer = Customer( + "admin", + CryptoUtil.hashpw("pass"), + "CFO" + ) + db.customerCreate(adminCustomer) + testApplication { + application { + corebankWebApp(db, ctx) + } + // account to delete doesn't exist. + client.delete("/accounts/foo") { + basicAuth("admin", "pass") + expectSuccess = false + }.apply { + assert(this.status == HttpStatusCode.NotFound) + } + // account to delete is reserved. + client.delete("/accounts/admin") { + basicAuth("admin", "pass") + expectSuccess = false + }.apply { + assert(this.status == HttpStatusCode.Forbidden) + } + // successful deletion + db.customerCreate(customerFoo).apply { + assert(this != null) + assert(db.bankAccountCreate(genBankAccount(this!!)) != null) + } + client.delete("/accounts/foo") { + basicAuth("admin", "pass") + expectSuccess = true + }.apply { + assert(this.status == HttpStatusCode.NoContent) + } + // Trying again must yield 404 + client.delete("/accounts/foo") { + basicAuth("admin", "pass") + expectSuccess = false + }.apply { + assert(this.status == HttpStatusCode.NotFound) + } + // fail to delete, due to a non-zero balance. + db.customerCreate(customerBar).apply { + assert(this != null) + db.bankAccountCreate(genBankAccount(this!!)).apply { + assert(this != null) + val conn = DriverManager.getConnection("jdbc:postgresql:///libeufincheck").unwrap(PgConnection::class.java) + conn.execSQLUpdate("UPDATE libeufin_bank.bank_accounts SET balance.val = 1 WHERE bank_account_id = $this") + } + } + client.delete("/accounts/bar") { + basicAuth("admin", "pass") + expectSuccess = false + }.apply { + assert(this.status == HttpStatusCode.PreconditionFailed) + } + } + } + + /** + * Tests reconfiguration of account data. + */ + @Test + fun accountReconfig() = setup { db, ctx -> + testApplication { + application { + corebankWebApp(db, ctx) + } + assertNotNull(db.customerCreate(customerFoo)) + // First call expects 500, because foo lacks a bank account + client.patch("/accounts/foo") { + basicAuth("foo", "pw") + jsonBody(json { + "is_exchange" to true + }) + }.assertStatus(HttpStatusCode.InternalServerError) + // Creating foo's bank account. + assertNotNull(db.bankAccountCreate(genBankAccount(1L))) + // Successful attempt now. + val validReq = AccountReconfiguration( + cashout_address = "payto://new-cashout-address", + challenge_contact_data = ChallengeContactData( + email = "new@example.com", + phone = "+987" + ), + is_exchange = true, + name = null + ) + client.patch("/accounts/foo") { + basicAuth("foo", "pw") + jsonBody(validReq) + }.assertStatus(HttpStatusCode.NoContent) + // Checking idempotence. + client.patch("/accounts/foo") { + basicAuth("foo", "pw") + jsonBody(validReq) + }.assertStatus(HttpStatusCode.NoContent) + // Checking ordinary user doesn't get to patch their name. + client.patch("/accounts/foo") { + basicAuth("foo", "pw") + jsonBody(json { + "name" to "Another Foo" + }) + }.assertStatus(HttpStatusCode.Forbidden) + // Finally checking that admin does get to patch foo's name. + assertNotNull(db.customerCreate(Customer( + login = "admin", + passwordHash = CryptoUtil.hashpw("secret"), + name = "CFO" + ))) + client.patch("/accounts/foo") { + basicAuth("admin", "secret") + jsonBody(json { + "name" to "Another Foo" + }) + }.assertStatus(HttpStatusCode.NoContent) + val fooFromDb = db.customerGetFromLogin("foo") + assertNotNull(fooFromDb) + assertEquals("Another Foo", fooFromDb.name) + } + } + + /** + * Tests the GET /accounts endpoint. + */ + @Test + fun getAccountsList() = setup { db, ctx -> + val adminCustomer = Customer( + "admin", + CryptoUtil.hashpw("pass"), + "CFO" + ) + assert(db.customerCreate(adminCustomer) != null) + testApplication { + application { + corebankWebApp(db, ctx) + } + // No users registered, expect no data. + client.get("/accounts") { + basicAuth("admin", "pass") + expectSuccess = true + }.apply { + assert(this.status == HttpStatusCode.NoContent) + } + // foo account + db.customerCreate(customerFoo).apply { + assert(this != null) + assert(db.bankAccountCreate(genBankAccount(this!!)) != null) + } + // bar account + db.customerCreate(customerBar).apply { + assert(this != null) + assert(db.bankAccountCreate(genBankAccount(this!!)) != null) + } + // Two users registered, requesting all of them. + client.get("/accounts") { + basicAuth("admin", "pass") + expectSuccess = true + }.apply { + println(this.bodyAsText()) + assert(this.status == HttpStatusCode.OK) + val obj = Json.decodeFromString<ListBankAccountsResponse>(this.bodyAsText()) + assert(obj.accounts.size == 2) + // Order unreliable, just checking they're different. + assert(obj.accounts[0].username != obj.accounts[1].username) + } + // Filtering on bar. + client.get("/accounts?filter_name=ar") { + basicAuth("admin", "pass") + expectSuccess = true + }.apply { + assert(this.status == HttpStatusCode.OK) + val obj = Json.decodeFromString<ListBankAccountsResponse>(this.bodyAsText()) + assert(obj.accounts.size == 1) { + println("Wrong size of filtered query: ${obj.accounts.size}") + } + assert(obj.accounts[0].username == "bar") + } + } + } + +} diff --git a/bank/src/test/kotlin/LibeuFinApiTest.kt b/bank/src/test/kotlin/LibeuFinApiTest.kt @@ -1,807 +0,0 @@ -import io.ktor.client.plugins.* -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.http.* -import io.ktor.http.content.* -import io.ktor.server.engine.* -import io.ktor.server.testing.* -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json -import net.taler.wallet.crypto.Base32Crockford -import org.junit.Test -import org.postgresql.jdbc.PgConnection -import tech.libeufin.bank.* -import tech.libeufin.util.CryptoUtil -import java.sql.DriverManager -import java.time.Duration -import java.time.Instant -import java.time.temporal.ChronoUnit -import kotlin.random.Random -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals -import kotlin.test.assertNotNull - -class LibeuFinApiTest { - private val customerFoo = Customer( - login = "foo", - passwordHash = CryptoUtil.hashpw("pw"), - name = "Foo", - phone = "+00", - email = "foo@b.ar", - cashoutPayto = "payto://external-IBAN", - cashoutCurrency = "KUDOS" - ) - private val customerBar = Customer( - login = "bar", - passwordHash = CryptoUtil.hashpw("pw"), - name = "Bar", - phone = "+99", - email = "bar@example.com", - cashoutPayto = "payto://external-IBAN", - cashoutCurrency = "KUDOS" - ) - - private fun genBankAccount(rowId: Long) = BankAccount( - hasDebt = false, - internalPaytoUri = IbanPayTo("payto://iban/ac${rowId}"), - maxDebt = TalerAmount(100, 0, "KUDOS"), - owningCustomerId = rowId - ) - - @Test - fun getConfig() = setup { db, ctx -> - testApplication { - application { corebankWebApp(db, ctx) } - val r = client.get("/config") { - expectSuccess = true - } - println(r.bodyAsText()) - } - - } - - /** - * Testing GET /transactions. This test checks that the sign - * of delta gets honored by the HTTP handler, namely that the - * records appear in ASC or DESC order, according to the sign - * of delta. - */ - @Test - fun testHistory() = setup { db, ctx -> - // TODO add better tests with lon polling like Wire Gateway API - val fooId = db.customerCreate(customerFoo); assert(fooId != null) - assert(db.bankAccountCreate(genBankAccount(fooId!!)) != null) - val barId = db.customerCreate(customerBar); assert(barId != null) - assert(db.bankAccountCreate(genBankAccount(barId!!)) != null) - for (i in 1..10) { - db.bankTransactionCreate(genTx("test-$i")) - } - testApplication { - application { - corebankWebApp(db, ctx) - } - val asc = client.get("/accounts/foo/transactions?delta=2") { - basicAuth("foo", "pw") - expectSuccess = true - } - var obj = Json.decodeFromString<BankAccountTransactionsResponse>(asc.bodyAsText()) - assert(obj.transactions.size == 2) - assert(obj.transactions[0].row_id < obj.transactions[1].row_id) - val desc = client.get("/accounts/foo/transactions?delta=-2") { - basicAuth("foo", "pw") - expectSuccess = true - } - obj = Json.decodeFromString(desc.bodyAsText()) - assert(obj.transactions.size == 2) - assert(obj.transactions[0].row_id > obj.transactions[1].row_id) - } - } - - // Testing the creation of bank transactions. - @Test - fun postTransactionsTest() = setup { db, ctx -> - // foo account - val fooId = db.customerCreate(customerFoo); - assert(fooId != null) - assert(db.bankAccountCreate(genBankAccount(fooId!!)) != null) - // bar account - val barId = db.customerCreate(customerBar); - assert(barId != null) - assert(db.bankAccountCreate(genBankAccount(barId!!)) != null) - // accounts exist, now create one transaction. - testApplication { - application { - corebankWebApp(db, ctx) - } - client.post("/accounts/foo/transactions") { - expectSuccess = true - basicAuth("foo", "pw") - contentType(ContentType.Application.Json) - // expectSuccess = true - setBody( - """{ - "payto_uri": "payto://iban/AC${barId}?message=payout", - "amount": "KUDOS:3.3" - } - """.trimIndent() - ) - } - // Getting the only tx that exists in the DB, hence has ID == 1. - val r = client.get("/accounts/foo/transactions/1") { - basicAuth("foo", "pw") - expectSuccess = true - } - val obj: BankAccountTransactionInfo = Json.decodeFromString(r.bodyAsText()) - assert(obj.subject == "payout") - // Testing the wrong currency. - val wrongCurrencyResp = client.post("/accounts/foo/transactions") { - expectSuccess = false - basicAuth("foo", "pw") - contentType(ContentType.Application.Json) - // expectSuccess = true - setBody( - """{ - "payto_uri": "payto://iban/AC${barId}?message=payout", - "amount": "EUR:3.3" - } - """.trimIndent() - ) - } - assert(wrongCurrencyResp.status == HttpStatusCode.BadRequest) - // Surpassing the debt limit. - val unallowedDebtResp = client.post("/accounts/foo/transactions") { - expectSuccess = false - basicAuth("foo", "pw") - contentType(ContentType.Application.Json) - // expectSuccess = true - setBody( - """{ - "payto_uri": "payto://iban/AC${barId}?message=payout", - "amount": "KUDOS:555" - } - """.trimIndent() - ) - } - assert(unallowedDebtResp.status == HttpStatusCode.Conflict) - val bigAmount = client.post("/accounts/foo/transactions") { - expectSuccess = false - basicAuth("foo", "pw") - contentType(ContentType.Application.Json) - // expectSuccess = true - setBody( - """{ - "payto_uri": "payto://iban/AC${barId}?message=payout", - "amount": "KUDOS:${"5".repeat(200)}" - } - """.trimIndent() - ) - } - assert(bigAmount.status == HttpStatusCode.BadRequest) - } - } - - @Test - fun passwordChangeTest() = setup { db, ctx -> - assert(db.customerCreate(customerFoo) != null) - testApplication { - application { - corebankWebApp(db, ctx) - } - // Changing the password. - client.patch("/accounts/foo/auth") { - expectSuccess = true - contentType(ContentType.Application.Json) - basicAuth("foo", "pw") - setBody("""{"new_password": "bar"}""") - } - // Previous password should fail. - client.patch("/accounts/foo/auth") { - expectSuccess = false - contentType(ContentType.Application.Json) - basicAuth("foo", "pw") - setBody("""{"new_password": "not-even-parsed"}""") - }.apply { - assert(this.status == HttpStatusCode.Unauthorized) - } - // New password should succeed. - client.patch("/accounts/foo/auth") { - expectSuccess = true - contentType(ContentType.Application.Json) - basicAuth("foo", "bar") - setBody("""{"new_password": "not-used"}""") - } - } - } - @Test - fun tokenDeletionTest() = setup { db, ctx -> - assert(db.customerCreate(customerFoo) != null) - val token = ByteArray(32) - Random.nextBytes(token) - assert(db.bearerTokenCreate( - BearerToken( - bankCustomer = 1L, - content = token, - creationTime = Instant.now(), - expirationTime = Instant.now().plusSeconds(10), - scope = TokenScope.readwrite - ) - )) - testApplication { - application { - corebankWebApp(db, ctx) - } - // Legitimate first attempt, should succeed - client.delete("/accounts/foo/token") { - expectSuccess = true - headers["Authorization"] = "Bearer secret-token:${Base32Crockford.encode(token)}" - }.apply { - assert(this.status == HttpStatusCode.NoContent) - } - // Trying after deletion should hit 404. - client.delete("/accounts/foo/token") { - expectSuccess = false - headers["Authorization"] = "Bearer secret-token:${Base32Crockford.encode(token)}" - }.apply { - assert(this.status == HttpStatusCode.Unauthorized) - } - // Checking foo can still be served by basic auth, after token deletion. - assert(db.bankAccountCreate( - BankAccount( - hasDebt = false, - internalPaytoUri = IbanPayTo("payto://iban/DE1234"), - maxDebt = TalerAmount(100, 0, "KUDOS"), - owningCustomerId = 1 - ) - ) != null) - client.get("/accounts/foo") { - expectSuccess = true - basicAuth("foo", "pw") - } - } - } - - @Test - fun publicAccountsTest() = setup { db, ctx -> - testApplication { - application { - corebankWebApp(db, ctx) - } - client.get("/public-accounts").apply { - assert(this.status == HttpStatusCode.NoContent) - } - // Make one public account. - 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 - ) - } - client.get("/public-accounts").apply { - assert(this.status == HttpStatusCode.OK) - val obj = Json.decodeFromString<PublicAccountsResponse>(this.bodyAsText()) - assert(obj.public_accounts.size == 1) - assert(obj.public_accounts[0].account_name == "bar") - } - } - } - // Creating token with "forever" duration. - @Test - fun tokenForeverTest() = setup { db, ctx -> - assert(db.customerCreate(customerFoo) != null) - testApplication { - application { - corebankWebApp(db, ctx) - } - val newTok = client.post("/accounts/foo/token") { - expectSuccess = true - contentType(ContentType.Application.Json) - basicAuth("foo", "pw") - setBody( - """ - {"duration": {"d_us": "forever"}, "scope": "readonly"} - """.trimIndent() - ) - } - val newTokObj = Json.decodeFromString<TokenSuccessResponse>(newTok.bodyAsText()) - assert(newTokObj.expiration.t_s == Instant.MAX) - } - } - - // Testing that too big or invalid durations fail the request. - @Test - fun tokenInvalidDurationTest() = setup { db, ctx -> - assert(db.customerCreate(customerFoo) != null) - testApplication { - application { - corebankWebApp(db, ctx) - } - var r = client.post("/accounts/foo/token") { - expectSuccess = false - contentType(ContentType.Application.Json) - basicAuth("foo", "pw") - setBody("""{ - "duration": {"d_us": "invalid"}, - "scope": "readonly"}""".trimIndent()) - } - assert(r.status == HttpStatusCode.BadRequest) - r = client.post("/accounts/foo/token") { - expectSuccess = false - contentType(ContentType.Application.Json) - basicAuth("foo", "pw") - setBody("""{ - "duration": {"d_us": ${Long.MAX_VALUE}}, - "scope": "readonly"}""".trimIndent()) - } - assert(r.status == HttpStatusCode.BadRequest) - r = client.post("/accounts/foo/token") { - expectSuccess = false - contentType(ContentType.Application.Json) - basicAuth("foo", "pw") - setBody("""{ - "duration": {"d_us": -1}, - "scope": "readonly"}""".trimIndent()) - } - assert(r.status == HttpStatusCode.BadRequest) - } - } - // Checking the POST /token handling. - @Test - fun tokenTest() = setup { db, ctx -> - assert(db.customerCreate(customerFoo) != null) - testApplication { - application { - corebankWebApp(db, ctx) - } - val newTok = client.post("/accounts/foo/token") { - expectSuccess = true - contentType(ContentType.Application.Json) - basicAuth("foo", "pw") - setBody( - """ - {"scope": "readonly"} - """.trimIndent() - ) - } - // Checking that the token lifetime defaulted to 24 hours. - val newTokObj = Json.decodeFromString<TokenSuccessResponse>(newTok.bodyAsText()) - val newTokDb = db.bearerTokenGet(Base32Crockford.decode(newTokObj.access_token)) - val lifeTime = Duration.between(newTokDb!!.creationTime, newTokDb.expirationTime) - assert(lifeTime == Duration.ofDays(1)) - - // foo tries to create a token on behalf of bar, expect 403. - val r = client.post("/accounts/bar/token") { - expectSuccess = false - basicAuth("foo", "pw") - } - assert(r.status == HttpStatusCode.Forbidden) - // Make ad-hoc token for foo. - val fooTok = ByteArray(32).apply { Random.nextBytes(this) } - assert( - db.bearerTokenCreate( - BearerToken( - content = fooTok, - bankCustomer = 1L, // only foo exists. - scope = TokenScope.readonly, - creationTime = Instant.now(), - isRefreshable = true, - expirationTime = Instant.now().plus(1, ChronoUnit.DAYS) - ) - ) - ) - // Testing the secret-token:-scheme. - client.post("/accounts/foo/token") { - headers.set("Authorization", "Bearer secret-token:${Base32Crockford.encode(fooTok)}") - contentType(ContentType.Application.Json) - setBody("{\"scope\": \"readonly\"}") - expectSuccess = true - } - // Testing the 'forever' case. - val forever = client.post("/accounts/foo/token") { - expectSuccess = true - contentType(ContentType.Application.Json) - basicAuth("foo", "pw") - setBody("""{ - "scope": "readonly", - "duration": {"d_us": "forever"} - }""".trimIndent()) - } - val never: TokenSuccessResponse = Json.decodeFromString(forever.bodyAsText()) - assert(never.expiration.t_s == Instant.MAX) - } - } - - /** - * Testing the retrieval of account information. - * The tested logic is the one usually needed by SPAs - * to show customers their status. - */ - @Test - fun getAccountTest() = setup { db, ctx -> - // Artificially insert a customer and bank account in the database. - val customerRowId = db.customerCreate( - Customer( - "foo", - CryptoUtil.hashpw("pw"), - "Foo" - ) - ) - assert(customerRowId != null) - assert( - db.bankAccountCreate( - BankAccount( - hasDebt = false, - internalPaytoUri = IbanPayTo("payto://iban/DE1234"), - maxDebt = TalerAmount(100, 0, "KUDOS"), - owningCustomerId = customerRowId!! - ) - ) != null - ) - testApplication { - application { - corebankWebApp(db, ctx) - } - val r = client.get("/accounts/foo") { - expectSuccess = true - basicAuth("foo", "pw") - } - val obj: AccountData = Json.decodeFromString(r.bodyAsText()) - assert(obj.name == "Foo") - // Checking admin can. - val adminRowId = db.customerCreate( - Customer( - "admin", - CryptoUtil.hashpw("admin"), - "Admin" - ) - ) - assert(adminRowId != null) - assert( - db.bankAccountCreate( - BankAccount( - hasDebt = false, - internalPaytoUri = IbanPayTo("payto://iban/SANDBOXX/ADMIN-IBAN"), - maxDebt = TalerAmount(100, 0, "KUDOS"), - owningCustomerId = adminRowId!! - ) - ) != null - ) - client.get("/accounts/foo") { - expectSuccess = true - basicAuth("admin", "admin") - } - val shouldNot = client.get("/accounts/foo") { - basicAuth("not", "not") - expectSuccess = false - } - assert(shouldNot.status == HttpStatusCode.Unauthorized) - } - } - - /** - * Testing the account creation and its idempotency - */ - @Test - fun createAccountTest() = setup { db, ctx -> - testApplication { - val ibanPayto = genIbanPaytoUri() - application { - corebankWebApp(db, ctx) - } - var resp = client.post("/accounts") { - expectSuccess = false - contentType(ContentType.Application.Json) - setBody( - """{ - "username": "foo", - "password": "bar", - "name": "Jane", - "is_public": true, - "internal_payto_uri": "$ibanPayto" - }""".trimIndent() - ) - } - assert(resp.status == HttpStatusCode.Created) - // Testing idempotency. - resp = client.post("/accounts") { - expectSuccess = false - contentType(ContentType.Application.Json) - setBody( - """{ - "username": "foo", - "password": "bar", - "name": "Jane", - "is_public": true, - "internal_payto_uri": "$ibanPayto" - }""".trimIndent() - ) - } - assert(resp.status == HttpStatusCode.Created) - } - } - - /** - * Testing the account creation and its idempotency - */ - @Test - fun createTwoAccountsTest() = setup { db, ctx -> - testApplication { - application { - corebankWebApp(db, ctx) - } - var resp = client.post("/accounts") { - expectSuccess = false - contentType(ContentType.Application.Json) - setBody( - """{ - "username": "foo", - "password": "bar", - "name": "Jane" - }""".trimIndent() - ) - } - assert(resp.status == HttpStatusCode.Created) - // Test creating another account. - resp = client.post("/accounts") { - expectSuccess = false - contentType(ContentType.Application.Json) - setBody( - """{ - "username": "joe", - "password": "bar", - "name": "Joe" - }""".trimIndent() - ) - } - assert(resp.status == HttpStatusCode.Created) - } - } - - /** - * Test admin-only account creation - */ - @Test - fun createAccountRestrictedTest() = setup(conf = "test_restrict.conf") { db, ctx -> - testApplication { - application { - corebankWebApp(db, ctx) - } - - // Ordinary user tries, should fail. - var resp = client.post("/accounts") { - expectSuccess = false - basicAuth("foo", "bar") - contentType(ContentType.Application.Json) - setBody( - """{ - "username": "baz", - "password": "xyz", - "name": "Mallory" - }""".trimIndent() - ) - } - assert(resp.status == HttpStatusCode.Unauthorized) - // Creating the administrator. - assert( - db.customerCreate( - Customer( - "admin", - CryptoUtil.hashpw("pass"), - "CFO" - ) - ) != null - ) - // customer exists, this makes only the bank account: - assert(maybeCreateAdminAccount(db, ctx)) - resp = client.post("/accounts") { - expectSuccess = false - basicAuth("admin", "pass") - contentType(ContentType.Application.Json) - setBody( - """{ - "username": "baz", - "password": "xyz", - "name": "Mallory" - }""".trimIndent() - ) - } - assert(resp.status == HttpStatusCode.Created) - } - } - - /** - * Tests DELETE /accounts/foo - */ - @Test - fun deleteAccount() = setup { db, ctx -> - val adminCustomer = Customer( - "admin", - CryptoUtil.hashpw("pass"), - "CFO" - ) - db.customerCreate(adminCustomer) - testApplication { - application { - corebankWebApp(db, ctx) - } - // account to delete doesn't exist. - client.delete("/accounts/foo") { - basicAuth("admin", "pass") - expectSuccess = false - }.apply { - assert(this.status == HttpStatusCode.NotFound) - } - // account to delete is reserved. - client.delete("/accounts/admin") { - basicAuth("admin", "pass") - expectSuccess = false - }.apply { - assert(this.status == HttpStatusCode.Forbidden) - } - // successful deletion - db.customerCreate(customerFoo).apply { - assert(this != null) - assert(db.bankAccountCreate(genBankAccount(this!!)) != null) - } - client.delete("/accounts/foo") { - basicAuth("admin", "pass") - expectSuccess = true - }.apply { - assert(this.status == HttpStatusCode.NoContent) - } - // Trying again must yield 404 - client.delete("/accounts/foo") { - basicAuth("admin", "pass") - expectSuccess = false - }.apply { - assert(this.status == HttpStatusCode.NotFound) - } - // fail to delete, due to a non-zero balance. - db.customerCreate(customerBar).apply { - assert(this != null) - db.bankAccountCreate(genBankAccount(this!!)).apply { - assert(this != null) - val conn = DriverManager.getConnection("jdbc:postgresql:///libeufincheck").unwrap(PgConnection::class.java) - conn.execSQLUpdate("UPDATE libeufin_bank.bank_accounts SET balance.val = 1 WHERE bank_account_id = $this") - } - } - client.delete("/accounts/bar") { - basicAuth("admin", "pass") - expectSuccess = false - }.apply { - assert(this.status == HttpStatusCode.PreconditionFailed) - } - } - } - - /** - * Tests reconfiguration of account data. - */ - @Test - fun accountReconfig() = setup { db, ctx -> - testApplication { - application { - corebankWebApp(db, ctx) - } - assertNotNull(db.customerCreate(customerFoo)) - // First call expects 500, because foo lacks a bank account - client.patch("/accounts/foo") { - basicAuth("foo", "pw") - jsonBody(json { - "is_exchange" to true - }) - }.assertStatus(HttpStatusCode.InternalServerError) - // Creating foo's bank account. - assertNotNull(db.bankAccountCreate(genBankAccount(1L))) - // Successful attempt now. - val validReq = AccountReconfiguration( - cashout_address = "payto://new-cashout-address", - challenge_contact_data = ChallengeContactData( - email = "new@example.com", - phone = "+987" - ), - is_exchange = true, - name = null - ) - client.patch("/accounts/foo") { - basicAuth("foo", "pw") - jsonBody(validReq) - }.assertStatus(HttpStatusCode.NoContent) - // Checking idempotence. - client.patch("/accounts/foo") { - basicAuth("foo", "pw") - jsonBody(validReq) - }.assertStatus(HttpStatusCode.NoContent) - // Checking ordinary user doesn't get to patch their name. - client.patch("/accounts/foo") { - basicAuth("foo", "pw") - jsonBody(json { - "name" to "Another Foo" - }) - }.assertStatus(HttpStatusCode.Forbidden) - // Finally checking that admin does get to patch foo's name. - assertNotNull(db.customerCreate(Customer( - login = "admin", - passwordHash = CryptoUtil.hashpw("secret"), - name = "CFO" - ))) - client.patch("/accounts/foo") { - basicAuth("admin", "secret") - jsonBody(json { - "name" to "Another Foo" - }) - }.assertStatus(HttpStatusCode.NoContent) - val fooFromDb = db.customerGetFromLogin("foo") - assertNotNull(fooFromDb) - assertEquals("Another Foo", fooFromDb.name) - } - } - - /** - * Tests the GET /accounts endpoint. - */ - @Test - fun getAccountsList() = setup { db, ctx -> - val adminCustomer = Customer( - "admin", - CryptoUtil.hashpw("pass"), - "CFO" - ) - assert(db.customerCreate(adminCustomer) != null) - testApplication { - application { - corebankWebApp(db, ctx) - } - // No users registered, expect no data. - client.get("/accounts") { - basicAuth("admin", "pass") - expectSuccess = true - }.apply { - assert(this.status == HttpStatusCode.NoContent) - } - // foo account - db.customerCreate(customerFoo).apply { - assert(this != null) - assert(db.bankAccountCreate(genBankAccount(this!!)) != null) - } - // bar account - db.customerCreate(customerBar).apply { - assert(this != null) - assert(db.bankAccountCreate(genBankAccount(this!!)) != null) - } - // Two users registered, requesting all of them. - client.get("/accounts") { - basicAuth("admin", "pass") - expectSuccess = true - }.apply { - println(this.bodyAsText()) - assert(this.status == HttpStatusCode.OK) - val obj = Json.decodeFromString<ListBankAccountsResponse>(this.bodyAsText()) - assert(obj.accounts.size == 2) - // Order unreliable, just checking they're different. - assert(obj.accounts[0].username != obj.accounts[1].username) - } - // Filtering on bar. - client.get("/accounts?filter_name=ar") { - basicAuth("admin", "pass") - expectSuccess = true - }.apply { - assert(this.status == HttpStatusCode.OK) - val obj = Json.decodeFromString<ListBankAccountsResponse>(this.bodyAsText()) - assert(obj.accounts.size == 1) { - println("Wrong size of filtered query: ${obj.accounts.size}") - } - assert(obj.accounts[0].username == "bar") - } - } - } - -}