diff options
author | Antoine A <> | 2023-10-16 10:48:56 +0000 |
---|---|---|
committer | Antoine A <> | 2023-10-16 10:48:56 +0000 |
commit | e900f224cb9c8e5046b8bbb9db46f96cb40c3b9c (patch) | |
tree | 06a9128a12d1788a917d49ed576d89246b5524df | |
parent | db76884dee5897197ef0f3a0d8f72c58ea7e7723 (diff) | |
download | libeufin-e900f224cb9c8e5046b8bbb9db46f96cb40c3b9c.tar.gz libeufin-e900f224cb9c8e5046b8bbb9db46f96cb40c3b9c.tar.bz2 libeufin-e900f224cb9c8e5046b8bbb9db46f96cb40c3b9c.zip |
Cleanup and improve tests, fix procedures.sql
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt (renamed from bank/src/main/kotlin/tech/libeufin/bank/IntegrationApiHandlers.kt) | 4 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt (renamed from bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt) | 7 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/Database.kt | 45 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/Main.kt | 6 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt | 46 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt (renamed from bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt) | 2 | ||||
-rw-r--r-- | bank/src/test/kotlin/BankIntegrationApiTest.kt | 145 | ||||
-rw-r--r-- | bank/src/test/kotlin/TalerApiTest.kt | 751 | ||||
-rw-r--r-- | bank/src/test/kotlin/WireGatewayApiTest.kt | 529 | ||||
-rw-r--r-- | bank/src/test/kotlin/helpers.kt | 54 | ||||
-rw-r--r-- | database-versioning/procedures.sql | 4 |
11 files changed, 760 insertions, 833 deletions
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/IntegrationApiHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt index 7a28618e..f607317e 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/IntegrationApiHandlers.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt @@ -27,7 +27,7 @@ import io.ktor.server.response.* import io.ktor.server.routing.* import net.taler.common.errorcodes.TalerErrorCode -fun Routing.talerIntegrationHandlers(db: Database, ctx: BankApplicationContext) { +fun Routing.bankIntegrationApi(db: Database, ctx: BankApplicationContext) { get("/taler-integration/config") { val internalCurrency: String = ctx.currency call.respond(TalerIntegrationConfigResponse( @@ -55,7 +55,7 @@ fun Routing.talerIntegrationHandlers(db: Database, ctx: BankApplicationContext) selection_done = op.selectionDone, transfer_done = op.confirmationDone, amount = op.amount, - sender_wire = relatedBankAccount.internalPaytoUri.stripped, + sender_wire = relatedBankAccount.internalPaytoUri.canonical, suggested_exchange = suggestedExchange, confirm_transfer_url = confirmUrl ) diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt index 6cf0d96d..3526e8c3 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt @@ -14,6 +14,7 @@ import java.time.Duration import java.time.Instant import java.time.temporal.ChronoUnit import java.util.* +import kotlin.random.Random private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.accountsMgmtHandlers") @@ -22,7 +23,7 @@ private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.account * create, update, delete, show bank accounts. No histories * and wire transfers should belong here. */ -fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { +fun Routing.accountsMgmtApi(db: Database, ctx: BankApplicationContext) { // TOKEN ENDPOINTS delete("/accounts/{USERNAME}/token") { @@ -72,7 +73,7 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { ) } val tokenBytes = ByteArray(32).apply { - Random().nextBytes(this) + Random.nextBytes(this) } val tokenDuration: Duration = req.duration?.d_us ?: TOKEN_DEFAULT_DURATION @@ -187,7 +188,7 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) { CryptoUtil.checkpw(req.password, maybeCustomerExists.passwordHash) && maybeHasBankAccount.isPublic == req.is_public && maybeHasBankAccount.isTalerExchange == req.is_taler_exchange && - maybeHasBankAccount.internalPaytoUri.stripped == internalPayto.stripped + maybeHasBankAccount.internalPaytoUri.canonical == internalPayto.canonical if (isIdentic) { call.respond(HttpStatusCode.Created) return@post diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt index c3e97102..a7fbbb42 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt @@ -391,11 +391,7 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos creationTime = it.getLong("creation_time").microsToJavaInstant() ?: throw faultyTimestampByBank(), expirationTime = it.getLong("expiration_time").microsToJavaInstant() ?: throw faultyDurationByClient(), bankCustomer = it.getLong("bank_customer"), - scope = when (it.getString("scope")) { - TokenScope.readwrite.name -> TokenScope.readwrite - TokenScope.readonly.name -> TokenScope.readonly - else -> throw internalServerError("Wrong token scope found in the database: $this") - }, + scope = TokenScope.valueOf(it.getString("scope")), isRefreshable = it.getBoolean("is_refreshable") ) } @@ -575,7 +571,7 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos (?, ?, ?, ?, (?, ?)::taler_amount) RETURNING bank_account_id; """) - stmt.setString(1, bankAccount.internalPaytoUri.stripped) + stmt.setString(1, bankAccount.internalPaytoUri.canonical) stmt.setLong(2, bankAccount.owningCustomerId) stmt.setBoolean(3, bankAccount.isPublic) stmt.setBoolean(4, bankAccount.isTalerExchange) @@ -701,7 +697,7 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos FROM bank_accounts WHERE internal_payto_uri=? """) - stmt.setString(1, internalPayto.stripped) + stmt.setString(1, internalPayto.canonical) stmt.oneOrNull { BankAccount( @@ -863,13 +859,7 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos ), accountServicerReference = it.getString("account_servicer_reference"), endToEndId = it.getString("end_to_end_id"), - direction = it.getString("direction").run { - when(this) { - "credit" -> TransactionDirection.credit - "debit" -> TransactionDirection.debit - else -> throw internalServerError("Wrong direction in transaction: $this") - } - }, + direction = TransactionDirection.valueOf(it.getString("direction")), bankAccountId = it.getLong("bank_account_id"), paymentInformationId = it.getString("payment_information_id"), subject = it.getString("subject"), @@ -989,13 +979,7 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos getCurrency() ), subject = it.getString("subject"), - direction = it.getString("direction").run { - when(this) { - "credit" -> TransactionDirection.credit - "debit" -> TransactionDirection.debit - else -> throw internalServerError("Wrong direction in transaction: $this") - } - } + direction = TransactionDirection.valueOf(it.getString("direction")) ) } } @@ -1117,11 +1101,7 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos ), accountServicerReference = it.getString("account_servicer_reference"), endToEndId = it.getString("end_to_end_id"), - direction = when (it.getString("direction")) { - "credit" -> TransactionDirection.credit - "debit" -> TransactionDirection.debit - else -> throw internalServerError("Wrong direction in transaction: $this") - }, + direction = TransactionDirection.valueOf(it.getString("direction")), bankAccountId = it.getLong("bank_account_id"), paymentInformationId = it.getString("payment_information_id"), subject = it.getString("subject"), @@ -1217,7 +1197,7 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos WHERE withdrawal_uuid=? """ ) - stmt.setString(1, exchangePayto.stripped) + stmt.setString(1, exchangePayto.canonical) stmt.setString(2, reservePub) stmt.setObject(3, opUuid) stmt.executeUpdateViolation() @@ -1427,12 +1407,7 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos getCurrency() ), subject = it.getString("subject"), - tanChannel = when(it.getString("tan_channel")) { - "sms" -> TanChannel.sms - "email" -> TanChannel.email - "file" -> TanChannel.file - else -> throw internalServerError("TAN channel $this unsupported") - }, + tanChannel = TanChannel.valueOf(it.getString("tan_channel")), tanCode = it.getString("tan_code"), localTransaction = it.getLong("local_transaction"), tanConfirmationTime = when (val timestamp = it.getLong("tan_confirmation_time")) { @@ -1516,7 +1491,7 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos stmt.setLong(4, req.amount.value) stmt.setInt(5, req.amount.frac) stmt.setString(6, req.exchange_base_url.url) - stmt.setString(7, req.credit_account.stripped) + stmt.setString(7, req.credit_account.canonical) stmt.setString(8, username) stmt.setLong(9, timestamp.toDbMicros() ?: throw faultyTimestampByBank()) stmt.setString(10, acctSvcrRef) @@ -1601,7 +1576,7 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos stmt.setString(2, subject) stmt.setLong(3, req.amount.value) stmt.setInt(4, req.amount.frac) - stmt.setString(5, req.debit_account.stripped) + stmt.setString(5, req.debit_account.canonical) stmt.setString(6, username) stmt.setLong(7, timestamp.toDbMicros() ?: throw faultyTimestampByBank()) stmt.setString(8, acctSvcrRef) diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt index 60eae30f..5e21826a 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -259,9 +259,9 @@ fun Application.corebankWebApp(db: Database, ctx: BankApplicationContext) { call.respond(Config(ctx.currencySpecification)) return@get } - this.accountsMgmtHandlers(db, ctx) - this.talerIntegrationHandlers(db, ctx) - this.talerWireGatewayHandlers(db, ctx) + this.accountsMgmtApi(db, ctx) + this.bankIntegrationApi(db, ctx) + this.wireGatewayApi(db, ctx) } } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt index 2c5a996a..c7fb82c7 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt @@ -384,50 +384,26 @@ class ExchangeUrl { @Serializable(with = IbanPayTo.Serializer::class) class IbanPayTo { val parsed: URI - val stripped: String - // represent query param "sender-name" or "receiver-name". - val receiverName: String? - val iban: String - val bic: String? - // Typically, a wire transfer's subject. + val canonical: String + val amount: TalerAmount? val message: String? - val amount: String? + val receiverName: String? constructor(raw: String) { parsed = URI(raw) require(parsed.scheme == "payto") { "expect a payto URI" } require(parsed.host == "iban") { "expect a IBAN payto URI" } + val splitPath = parsed.path.split("/").filter { it.isNotEmpty() } require(splitPath.size < 3 && splitPath.isNotEmpty()) { "too many path segments" } - val parts = if (splitPath.size == 1) { - Pair(splitPath[0], null) - } else Pair(splitPath[1], splitPath[0]) - // TODO normalize IBAN & BIC ? - iban = parts.first.uppercase() - bic = parts.second?.uppercase() - stripped = "payto://iban/$iban" - - val params: List<Pair<String, String>>? = if (parsed.query != null) { - val queryString: List<String> = parsed.query.split("&") - queryString.map { - val split = it.split("="); - require(split.size == 2) { "parameter '$it' was malformed" } - Pair(split[0], split[1]) - } - } else null + val iban = (if (splitPath.size == 1) splitPath[0] else splitPath[1]).replace("-", "").uppercase() + // TODO normalize && check IBAN ? + canonical = "payto://iban/$iban" - // Return the value of query string parameter 'name', or null if not found. - // 'params' is the list of key-value elements of all the query parameters found in the URI. - fun getQueryParamOrNull(name: String): String? { - if (params == null) return null - return params.firstNotNullOfOrNull { pair -> - URLDecoder.decode(pair.second, Charsets.UTF_8).takeIf { pair.first == name } - } - } - - amount = getQueryParamOrNull("amount") - message = getQueryParamOrNull("message") - receiverName = getQueryParamOrNull("receiver-name") + val params = (parsed.query ?: "").parseUrlEncodedParameters(); + amount = params["amount"]?.run { TalerAmount(this) } + message = params["message"] + receiverName = params["receiver-name"] } internal object Serializer : KSerializer<IbanPayTo> { diff --git a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt index 653ef732..9b0f6d64 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt @@ -36,7 +36,7 @@ import kotlin.math.abs private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.nexus") -fun Routing.talerWireGatewayHandlers(db: Database, ctx: BankApplicationContext) { +fun Routing.wireGatewayApi(db: Database, ctx: BankApplicationContext) { /** Authenticate and check access rights */ suspend fun ApplicationCall.authCheck(scope: TokenScope, withAdmin: Boolean): String { val authCustomer = authenticateBankRequest(db, scope) ?: throw unauthorized("Bad login") diff --git a/bank/src/test/kotlin/BankIntegrationApiTest.kt b/bank/src/test/kotlin/BankIntegrationApiTest.kt new file mode 100644 index 00000000..a4b36575 --- /dev/null +++ b/bank/src/test/kotlin/BankIntegrationApiTest.kt @@ -0,0 +1,145 @@ +import io.ktor.client.plugins.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.client.HttpClient +import io.ktor.http.* +import io.ktor.server.testing.* +import kotlinx.serialization.json.* +import kotlinx.coroutines.* +import net.taler.wallet.crypto.Base32Crockford +import org.junit.Test +import tech.libeufin.bank.* +import tech.libeufin.util.CryptoUtil +import tech.libeufin.util.stripIbanPayto +import java.util.* +import java.time.Instant +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import randHashCode + +class BankIntegrationApiTest { + // Selecting withdrawal details from the Integration API endpoint. + @Test + fun intSelect() = bankSetup { db -> + val uuid = UUID.randomUUID() + // insert new. + assert(db.talerWithdrawalCreate( + opUUID = uuid, + walletBankAccount = 1L, + amount = TalerAmount(1, 0, "KUDOS") + )) + + val r = client.post("/taler-integration/withdrawal-operation/${uuid}") { + jsonBody(BankWithdrawalOperationPostRequest( + reserve_pub = "RESERVE-FOO", + selected_exchange = IbanPayTo("payto://iban/ABC123") + )) + }.assertOk() + println(r.bodyAsText()) + } + + // Showing withdrawal details from the Integrtion API endpoint. + @Test + fun intGet() = bankSetup { db -> + val uuid = UUID.randomUUID() + // insert new. + assert(db.talerWithdrawalCreate( + opUUID = uuid, + walletBankAccount = 1L, + amount = TalerAmount(1, 0, "KUDOS") + )) + + val r = client.get("/taler-integration/withdrawal-operation/${uuid}").assertOk() + println(r.bodyAsText()) + } + + // Testing withdrawal abort + @Test + fun withdrawalAbort() = bankSetup { db -> + val uuid = UUID.randomUUID() + // insert new. + assert(db.talerWithdrawalCreate( + opUUID = uuid, + walletBankAccount = 1L, + amount = TalerAmount(1, 0, "KUDOS") + )) + val op = db.talerWithdrawalGet(uuid) + assert(op?.aborted == false) + assert(db.talerWithdrawalSetDetails(uuid, IbanPayTo("payto://iban/exchange-payto"), "reserve_pub")) + + client.post("/withdrawals/${uuid}/abort") { + basicAuth("merchant", "merchant-password") + }.assertOk() + + val opAbo = db.talerWithdrawalGet(uuid) + assert(opAbo?.aborted == true && opAbo.selectionDone == true) + } + + // Testing withdrawal creation + @Test + fun withdrawalCreation() = bankSetup { _ -> + // Creating the withdrawal as if the SPA did it. + val r = client.post("/accounts/merchant/withdrawals") { + basicAuth("merchant", "merchant-password") + jsonBody(BankAccountCreateWithdrawalRequest(TalerAmount(value = 9, frac = 0, currency = "KUDOS"))) + }.assertOk() + val opId = Json.decodeFromString<BankAccountCreateWithdrawalResponse>(r.bodyAsText()) + // Getting the withdrawal from the bank. Throws (failing the test) if not found. + client.get("/withdrawals/${opId.withdrawal_id}") { + basicAuth("merchant", "merchant-password") + }.assertOk() + } + + // Testing withdrawal confirmation + @Test + fun withdrawalConfirmation() = bankSetup { db -> + // Artificially making a withdrawal operation for merchant. + val uuid = UUID.randomUUID() + assert(db.talerWithdrawalCreate( + opUUID = uuid, + walletBankAccount = 1L, + amount = TalerAmount(1, 0, "KUDOS") + )) + // Specifying the exchange via its Payto URI. + assert(db.talerWithdrawalSetDetails( + opUuid = uuid, + exchangePayto = IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ"), + reservePub = "UNCHECKED-RESERVE-PUB" + )) + + // Starting the bank and POSTing as Foo to /confirm the operation. + client.post("/withdrawals/${uuid}/confirm") { + basicAuth("merchant", "merchant-password") + }.assertOk() + } + + // Testing the generation of taler://withdraw-URIs. + @Test + fun testWithdrawUri() { + // Checking the taler+http://-style. + val withHttp = getTalerWithdrawUri( + "http://example.com", + "my-id" + ) + assertEquals(withHttp, "taler+http://withdraw/example.com/taler-integration/my-id") + // Checking the taler://-style + val onlyTaler = getTalerWithdrawUri( + "https://example.com/", + "my-id" + ) + // Note: this tests as well that no double slashes belong to the result + assertEquals(onlyTaler, "taler://withdraw/example.com/taler-integration/my-id") + // Checking the removal of subsequent slashes + val manySlashes = getTalerWithdrawUri( + "https://www.example.com//////", + "my-id" + ) + assertEquals(manySlashes, "taler://withdraw/www.example.com/taler-integration/my-id") + // Checking with specified port number + val withPort = getTalerWithdrawUri( + "https://www.example.com:9876", + "my-id" + ) + assertEquals(withPort, "taler://withdraw/www.example.com:9876/taler-integration/my-id") + } +}
\ No newline at end of file diff --git a/bank/src/test/kotlin/TalerApiTest.kt b/bank/src/test/kotlin/TalerApiTest.kt deleted file mode 100644 index 59da1a94..00000000 --- a/bank/src/test/kotlin/TalerApiTest.kt +++ /dev/null @@ -1,751 +0,0 @@ -import io.ktor.client.plugins.* -import io.ktor.client.request.* -import io.ktor.client.statement.* -import io.ktor.client.HttpClient -import io.ktor.http.* -import io.ktor.server.testing.* -import kotlinx.serialization.json.* -import kotlinx.coroutines.* -import net.taler.wallet.crypto.Base32Crockford -import org.junit.Test -import tech.libeufin.bank.* -import tech.libeufin.util.CryptoUtil -import tech.libeufin.util.stripIbanPayto -import java.util.* -import java.time.Instant -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import randHashCode - -class TalerApiTest { - 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 bankAccountFoo = BankAccount( - internalPaytoUri = IbanPayTo("payto://iban/FOO-IBAN-XYZ"), - lastNexusFetchRowId = 1L, - owningCustomerId = 1L, - hasDebt = false, - maxDebt = TalerAmount(10, 1, "KUDOS"), - isTalerExchange = false - ) - val customerBar = Customer( - login = "bar", - passwordHash = CryptoUtil.hashpw("secret"), - name = "Bar", - phone = "+00", - email = "foo@b.ar", - cashoutPayto = "payto://external-IBAN", - cashoutCurrency = "KUDOS" - ) - val bankAccountBar = BankAccount( - internalPaytoUri = IbanPayTo("payto://iban/BAR-IBAN-ABC"), - lastNexusFetchRowId = 1L, - owningCustomerId = 2L, - hasDebt = false, - maxDebt = TalerAmount(10, 1, "KUDOS"), - isTalerExchange = true - ) - - - suspend fun Database.genTransfer(from: String, to: BankAccount, 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 - ), - username = from, - timestamp = Instant.now() - ).run { - assertEquals(TalerTransferResult.SUCCESS, txResult) - } - } - - suspend fun Database.genIncoming(to: String, from: BankAccount) { - talerAddIncomingCreate( - req = AddIncomingRequest( - reserve_pub = randShortHashCode(), - amount = TalerAmount( 10, 0, "KUDOS"), - debit_account = from.internalPaytoUri, - ), - username = to, - timestamp = Instant.now() - ).run { - assertEquals(TalerAddIncomingResult.SUCCESS, txResult) - } - } - - fun commonSetup(lambda: suspend (Database, BankApplicationContext) -> Unit) { - setup { db, ctx -> - // Creating the exchange and merchant accounts first. - assertNotNull(db.customerCreate(customerFoo)) - assertNotNull(db.bankAccountCreate(bankAccountFoo)) - assertNotNull(db.customerCreate(customerBar)) - assertNotNull(db.bankAccountCreate(bankAccountBar)) - lambda(db, ctx) - } - } - - // Test endpoint is correctly authenticated - suspend fun authRoutine(client: HttpClient, path: String, body: JsonObject? = null, method: HttpMethod = HttpMethod.Post) { - // No body when authentication must happen before parsing the body - - // Unknown account - client.request(path) { - this.method = method - basicAuth("unknown", "password") - }.assertStatus(HttpStatusCode.Unauthorized) - - // Wrong password - client.request(path) { - this.method = method - basicAuth("foo", "wrong_password") - }.assertStatus(HttpStatusCode.Unauthorized) - - // Wrong account - client.request(path) { - this.method = method - basicAuth("bar", "secret") - }.assertStatus(HttpStatusCode.Unauthorized) - - // Not exchange account - client.request(path) { - this.method = method - if (body != null) jsonBody(body) - basicAuth("foo", "pw") - }.assertStatus(HttpStatusCode.Conflict) - } - - // Testing the POST /transfer call from the TWG API. - @Test - fun transfer() = commonSetup { db, ctx -> - // Do POST /transfer. - testApplication { - application { - corebankWebApp(db, ctx) - } - - val valid_req = json { - "request_uid" to randHashCode() - "amount" to "KUDOS:55" - "exchange_base_url" to "http://exchange.example.com/" - "wtid" to randShortHashCode() - "credit_account" to bankAccountFoo.internalPaytoUri - }; - - authRoutine(client, "/accounts/foo/taler-wire-gateway/transfer", valid_req) - - // Checking exchange debt constraint. - client.post("/accounts/bar/taler-wire-gateway/transfer") { - basicAuth("bar", "secret") - jsonBody(valid_req) - }.assertStatus(HttpStatusCode.Conflict) - - // Giving debt allowance and checking the OK case. - assert(db.bankAccountSetMaxDebt( - 2L, - TalerAmount(1000, 0, "KUDOS") - )) - client.post("/accounts/bar/taler-wire-gateway/transfer") { - basicAuth("bar", "secret") - jsonBody(valid_req) - }.assertOk() - - // check idempotency - client.post("/accounts/bar/taler-wire-gateway/transfer") { - basicAuth("bar", "secret") - jsonBody(valid_req) - }.assertOk() - - // Trigger conflict due to reused request_uid - client.post("/accounts/bar/taler-wire-gateway/transfer") { - basicAuth("bar", "secret") - jsonBody( - json(valid_req) { - "wtid" to randShortHashCode() - "exchange_base_url" to "http://different-exchange.example.com/" - } - ) - }.assertStatus(HttpStatusCode.Conflict) - - // Currency mismatch - client.post("/accounts/bar/taler-wire-gateway/transfer") { - basicAuth("bar", "secret") - jsonBody( - json(valid_req) { - "amount" to "EUR:33" - } - ) - }.assertBadRequest() - - // Unknown account - client.post("/accounts/bar/taler-wire-gateway/transfer") { - basicAuth("bar", "secret") - jsonBody( - json(valid_req) { - "request_uid" to randHashCode() - "wtid" to randShortHashCode() - "credit_account" to "payto://iban/UNKNOWN-IBAN-XYZ" - } - ) - }.assertStatus(HttpStatusCode.NotFound) - - // Bad BASE32 wtid - client.post("/accounts/bar/taler-wire-gateway/transfer") { - basicAuth("bar", "secret") - jsonBody( - json(valid_req) { - "wtid" to "I love chocolate" - } - ) - }.assertBadRequest() - - // Bad BASE32 len wtid - client.post("/accounts/bar/taler-wire-gateway/transfer") { - basicAuth("bar", "secret") - jsonBody( - json(valid_req) { - "wtid" to randBase32Crockford(31) - } - ) - }.assertBadRequest() - - // Bad BASE32 request_uid - client.post("/accounts/bar/taler-wire-gateway/transfer") { - basicAuth("bar", "secret") - jsonBody( - json(valid_req) { - "request_uid" to "I love chocolate" - } - ) - }.assertBadRequest() - - // Bad BASE32 len wtid - client.post("/accounts/bar/taler-wire-gateway/transfer") { - basicAuth("bar", "secret") - jsonBody( - json(valid_req) { - "request_uid" to randBase32Crockford(65) - } - ) - }.assertBadRequest() - } - } - - /** - * Testing the /history/incoming call from the TWG API. - */ - @Test - fun historyIncoming() = commonSetup { db, ctx -> - // Give Foo reasonable debt allowance: - assert( - db.bankAccountSetMaxDebt( - 1L, - TalerAmount(1000000, 0, "KUDOS") - ) - ) - - suspend fun HttpResponse.assertHistory(size: Int) { - assertOk() - val txt = this.bodyAsText() - val history = Json.decodeFromString<IncomingHistory>(txt) - val params = getHistoryParams(this.call.request.url.parameters) - - // testing the size is like expected. - assert(history.incoming_transactions.size == size) { - println("incoming_transactions has wrong size: ${history.incoming_transactions.size}") - println("Response was: ${txt}") - } - if (params.delta < 0) { - // testing that the first row_id is at most the 'start' query param. - assert(history.incoming_transactions[0].row_id <= params.start) - // testing that the row_id decreases. - assert(history.incoming_transactions.windowed(2).all { (a, b) -> a.row_id > b.row_id }) - } else { - // testing that the first row_id is at least the 'start' query param. - assert(history.incoming_transactions[0].row_id >= params.start) - // testing that the row_id increases. - assert(history.incoming_transactions.windowed(2).all { (a, b) -> a.row_id < b.row_id }) - } - } - - testApplication { - application { - corebankWebApp(db, ctx) - } - - authRoutine(client, "/accounts/foo/taler-wire-gateway/history/incoming?delta=7", method = HttpMethod.Get) - - // Check error when no transactions - client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=7") { - basicAuth("bar", "secret") - }.assertStatus(HttpStatusCode.NoContent) - - // Gen three transactions using clean add incoming logic - repeat(3) { - db.genIncoming("bar", bankAccountFoo) - } - // Should not show up in the taler wire gateway API history - db.bankTransactionCreate(genTx("bogus foobar")).assertSuccess() - // Bar pays Foo once, but that should not appear in the result. - db.bankTransactionCreate(genTx("payout", creditorId = 1, debtorId = 2)).assertSuccess() - // Gen two transactions using row bank transaction logic - repeat(2) { - db.bankTransactionCreate( - genTx(IncomingTxMetadata(randShortHashCode()).encode(), 2, 1) - ).assertSuccess() - } - - // Check ignore bogus subject - client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=7") { - basicAuth("bar", "secret") - }.assertHistory(5) - - // Check skip bogus subject - client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=5") { - basicAuth("bar", "secret") - }.assertHistory(5) - - // Check no useless polling - assertTime(0, 300) { - client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=-6&start=15&long_poll_ms=1000") { - basicAuth("bar", "secret") - }.assertHistory(5) - } - - // Check polling end - client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=6&long_poll_ms=60") { - basicAuth("bar", "secret") - }.assertHistory(5) - - runBlocking { - joinAll( - launch { // Check polling succeed forward - assertTime(200, 1000) { - client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=6&long_poll_ms=1000") { - basicAuth("bar", "secret") - }.assertHistory(6) - } - }, - launch { // Check polling succeed backward - assertTime(200, 1000) { - client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=-6&long_poll_ms=1000") { - basicAuth("bar", "secret") - }.assertHistory(6) - } - }, - launch { // Check polling timeout forward - assertTime(200, 400) { - client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=8&long_poll_ms=300") { - basicAuth("bar", "secret") - }.assertHistory(6) - } - }, - launch { // Check polling timeout backward - assertTime(200, 400) { - client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=-8&long_poll_ms=300") { - basicAuth("bar", "secret") - }.assertHistory(6) - } - }, - launch { - delay(200) - db.genIncoming("bar", bankAccountFoo) - } - ) - } - - // Testing ranges. - repeat(300) { - db.genIncoming("bar", bankAccountFoo) - } - - // forward range: - client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=10&start=30") { - basicAuth("bar", "secret") - }.assertHistory(10) - - // backward range: - client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=-10&start=300") { - basicAuth("bar", "secret") - }.assertHistory(10) - } - } - - - /** - * Testing the /history/outgoing call from the TWG API. - */ - @Test - fun historyOutgoing() = commonSetup { db, ctx -> - // Give Bar reasonable debt allowance: - assert( - db.bankAccountSetMaxDebt( - 2L, - TalerAmount(1000000, 0, "KUDOS") - ) - ) - - suspend fun HttpResponse.assertHistory(size: Int) { - assertOk() - val txt = this.bodyAsText() - val history = Json.decodeFromString<OutgoingHistory>(txt) - val params = getHistoryParams(this.call.request.url.parameters) - - // testing the size is like expected. - assert(history.outgoing_transactions.size == size) { - println("outgoing_transactions has wrong size: ${history.outgoing_transactions.size}") - println("Response was: ${txt}") - } - if (params.delta < 0) { - // testing that the first row_id is at most the 'start' query param. - assert(history.outgoing_transactions[0].row_id <= params.start) - // testing that the row_id decreases. - assert(history.outgoing_transactions.windowed(2).all { (a, b) -> a.row_id > b.row_id }) - } else { - // testing that the first row_id is at least the 'start' query param. - assert(history.outgoing_transactions[0].row_id >= params.start) - // testing that the row_id increases. - assert(history.outgoing_transactions.windowed(2).all { (a, b) -> a.row_id < b.row_id }) - } - } - - testApplication { - application { - corebankWebApp(db, ctx) - } - - authRoutine(client, "/accounts/foo/taler-wire-gateway/history/outgoing?delta=7", method = HttpMethod.Get) - - // Check error when no transactions - client.get("/accounts/bar/taler-wire-gateway/history/outgoing?delta=7") { - basicAuth("bar", "secret") - }.assertStatus(HttpStatusCode.NoContent) - - // Gen three transactions using clean transfer logic - repeat(3) { - db.genTransfer("bar", bankAccountFoo) - } - // Should not show up in the taler wire gateway API history - db.bankTransactionCreate(genTx("bogus foobar", 1, 2)).assertSuccess() - // Foo pays Bar once, but that should not appear in the result. - db.bankTransactionCreate(genTx("payout")).assertSuccess() - // Gen two transactions using row bank transaction logic - repeat(2) { - db.bankTransactionCreate( - genTx(OutgoingTxMetadata(randShortHashCode(), ExchangeUrl("http://exchange.example.com/")).encode(), 1, 2) - ).assertSuccess() - } - - // Check ignore bogus subject - client.get("/accounts/bar/taler-wire-gateway/history/outgoing?delta=7") { - basicAuth("bar", "secret") - }.assertHistory(5) - - // Check skip bogus subject - client.get("/accounts/bar/taler-wire-gateway/history/outgoing?delta=5") { - basicAuth("bar", "secret") - }.assertHistory(5) - - // Check no useless polling - assertTime(0, 300) { - client.get("/accounts/bar/taler-wire-gateway/history/outgoing?delta=-6&start=15&long_poll_ms=1000") { - basicAuth("bar", "secret") - }.assertHistory(5) - } - - // Check polling end - client.get("/accounts/bar/taler-wire-gateway/history/outgoing?delta=6&long_poll_ms=60") { - basicAuth("bar", "secret") - }.assertHistory(5) - - runBlocking { - joinAll( - launch { // Check polling succeed forward - assertTime(200, 1000) { - client.get("/accounts/bar/taler-wire-gateway/history/outgoing?delta=6&long_poll_ms=1000") { - basicAuth("bar", "secret") - }.assertHistory(6) - } - }, - launch { // Check polling succeed backward - assertTime(200, 1000) { - client.get("/accounts/bar/taler-wire-gateway/history/outgoing?delta=-6&long_poll_ms=1000") { - basicAuth("bar", "secret") - }.assertHistory(6) - } - }, - launch { // Check polling timeout forward - assertTime(200, 400) { - client.get("/accounts/bar/taler-wire-gateway/history/outgoing?delta=8&long_poll_ms=300") { - basicAuth("bar", "secret") - }.assertHistory(6) - } - }, - launch { // Check polling timeout backward - assertTime(200, 400) { - client.get("/accounts/bar/taler-wire-gateway/history/outgoing?delta=-8&long_poll_ms=300") { - basicAuth("bar", "secret") - }.assertHistory(6) - } - }, - launch { - delay(200) - db.genTransfer("bar", bankAccountFoo) - } - ) - } - - // Testing ranges. - repeat(300) { - db.genTransfer("bar", bankAccountFoo) - } - - // forward range: - client.get("/accounts/bar/taler-wire-gateway/history/outgoing?delta=10&start=30") { - basicAuth("bar", "secret") - }.assertHistory(10) - - // backward range: - client.get("/accounts/bar/taler-wire-gateway/history/outgoing?delta=-10&start=300") { - basicAuth("bar", "secret") - }.assertHistory(10) - } - } - - // Testing the /admin/add-incoming call from the TWG API. - @Test - fun addIncoming() = commonSetup { db, ctx -> - testApplication { - application { - corebankWebApp(db, ctx) - } - - val valid_req = json { - "amount" to "KUDOS:44" - "reserve_pub" to randEddsaPublicKey() - "debit_account" to bankAccountFoo.internalPaytoUri - }; - - authRoutine(client, "/accounts/foo/taler-wire-gateway/admin/add-incoming", valid_req) - - // Checking exchange debt constraint. - client.post("/accounts/bar/taler-wire-gateway/admin/add-incoming") { - basicAuth("bar", "secret") - jsonBody(valid_req) - }.assertStatus(HttpStatusCode.Conflict) - - // Giving debt allowance and checking the OK case. - assert(db.bankAccountSetMaxDebt( - 1L, - TalerAmount(1000, 0, "KUDOS") - )) - - client.post("/accounts/bar/taler-wire-gateway/admin/add-incoming") { - basicAuth("bar", "secret") - jsonBody(valid_req, deflate = true) - }.assertOk() - - // Trigger conflict due to reused reserve_pub - client.post("/accounts/bar/taler-wire-gateway/admin/add-incoming") { - basicAuth("bar", "secret") - jsonBody(valid_req) - }.assertStatus(HttpStatusCode.Conflict) - - // Currency mismatch - client.post("/accounts/bar/taler-wire-gateway/admin/add-incoming") { - basicAuth("bar", "secret") - jsonBody( - json(valid_req) { - "amount" to "EUR:33" - } - ) - }.assertBadRequest() - - // Unknown account - client.post("/accounts/bar/taler-wire-gateway/admin/add-incoming") { - basicAuth("bar", "secret") - jsonBody( - json(valid_req) { - "reserve_pub" to randEddsaPublicKey() - "debit_account" to "payto://iban/UNKNOWN-IBAN-XYZ" - } - ) - }.assertStatus(HttpStatusCode.NotFound) - - // Bad BASE32 reserve_pub - client.post("/accounts/bar/taler-wire-gateway/admin/add-incoming") { - basicAuth("bar", "secret") - jsonBody(json(valid_req) { - "reserve_pub" to "I love chocolate" - }) - }.assertBadRequest() - - // Bad BASE32 len reserve_pub - client.post("/accounts/bar/taler-wire-gateway/admin/add-incoming") { - basicAuth("bar", "secret") - jsonBody(json(valid_req) { - "reserve_pub" to randBase32Crockford(31) - }) - }.assertBadRequest() - } - } - // Selecting withdrawal details from the Integration API endpoint. - @Test - fun intSelect() = setup { db, ctx -> - val uuid = UUID.randomUUID() - assertNotNull(db.customerCreate(customerFoo)) - assertNotNull(db.bankAccountCreate(bankAccountFoo)) - // insert new. - assert(db.talerWithdrawalCreate( - opUUID = uuid, - walletBankAccount = 1L, - amount = TalerAmount(1, 0, "KUDOS") - )) - testApplication { - application { - corebankWebApp(db, ctx) - } - val r = client.post("/taler-integration/withdrawal-operation/${uuid}") { - jsonBody(BankWithdrawalOperationPostRequest( - reserve_pub = "RESERVE-FOO", - selected_exchange = IbanPayTo("payto://iban/ABC123") - )) - }.assertOk() - println(r.bodyAsText()) - } - } - // Showing withdrawal details from the Integrtion API endpoint. - @Test - fun intGet() = setup { db, ctx -> - val uuid = UUID.randomUUID() - assert(db.customerCreate(customerFoo) != null) - assert(db.bankAccountCreate(bankAccountFoo) != null) - // insert new. - assert(db.talerWithdrawalCreate( - opUUID = uuid, - walletBankAccount = 1L, - amount = TalerAmount(1, 0, "KUDOS") - )) - testApplication { - application { - corebankWebApp(db, ctx) - } - val r = client.get("/taler-integration/withdrawal-operation/${uuid}").assertOk() - println(r.bodyAsText()) - } - } - // Testing withdrawal abort - @Test - fun withdrawalAbort() = setup { db, ctx -> - val uuid = UUID.randomUUID() - assert(db.customerCreate(customerFoo) != null) - assert(db.bankAccountCreate(bankAccountFoo) != null) - // insert new. - assert(db.talerWithdrawalCreate( - opUUID = uuid, - walletBankAccount = 1L, - amount = TalerAmount(1, 0, "KUDOS") - )) - val op = db.talerWithdrawalGet(uuid) - assert(op?.aborted == false) - assert(db.talerWithdrawalSetDetails(uuid, IbanPayTo("payto://iban/exchange-payto"), "reserve_pub")) - testApplication { - application { - corebankWebApp(db, ctx) - } - client.post("/withdrawals/${uuid}/abort") { - basicAuth("foo", "pw") - }.assertOk() - } - val opAbo = db.talerWithdrawalGet(uuid) - assert(opAbo?.aborted == true && opAbo.selectionDone == true) - } - // Testing withdrawal creation - @Test - fun withdrawalCreation() = setup { db, ctx -> - assertNotNull(db.customerCreate(customerFoo)) - assertNotNull(db.bankAccountCreate(bankAccountFoo)) - testApplication { - application { - corebankWebApp(db, ctx) - } - // Creating the withdrawal as if the SPA did it. - val r = client.post("/accounts/foo/withdrawals") { - basicAuth("foo", "pw") - jsonBody(BankAccountCreateWithdrawalRequest(TalerAmount(value = 9, frac = 0, currency = "KUDOS"))) - }.assertOk() - val opId = Json.decodeFromString<BankAccountCreateWithdrawalResponse>(r.bodyAsText()) - // Getting the withdrawal from the bank. Throws (failing the test) if not found. - client.get("/withdrawals/${opId.withdrawal_id}") { - basicAuth("foo", "pw") - }.assertOk() - } - } - // Testing withdrawal confirmation - @Test - fun withdrawalConfirmation() = commonSetup { db, ctx -> - - // Artificially making a withdrawal operation for Foo. - val uuid = UUID.randomUUID() - assert(db.talerWithdrawalCreate( - opUUID = uuid, - walletBankAccount = 1L, - amount = TalerAmount(1, 0, "KUDOS") - )) - // Specifying Bar as the exchange, via its Payto URI. - assert(db.talerWithdrawalSetDetails( - opUuid = uuid, - exchangePayto = IbanPayTo("payto://iban/BAR-IBAN-ABC"), - reservePub = "UNCHECKED-RESERVE-PUB" - )) - - // Starting the bank and POSTing as Foo to /confirm the operation. - testApplication { - application { - corebankWebApp(db, ctx) - } - client.post("/withdrawals/${uuid}/confirm") { - basicAuth("foo", "pw") - }.assertOk() - } - } - // Testing the generation of taler://withdraw-URIs. - @Test - fun testWithdrawUri() { - // Checking the taler+http://-style. - val withHttp = getTalerWithdrawUri( - "http://example.com", - "my-id" - ) - assertEquals(withHttp, "taler+http://withdraw/example.com/taler-integration/my-id") - // Checking the taler://-style - val onlyTaler = getTalerWithdrawUri( - "https://example.com/", - "my-id" - ) - // Note: this tests as well that no double slashes belong to the result - assertEquals(onlyTaler, "taler://withdraw/example.com/taler-integration/my-id") - // Checking the removal of subsequent slashes - val manySlashes = getTalerWithdrawUri( - "https://www.example.com//////", - "my-id" - ) - assertEquals(manySlashes, "taler://withdraw/www.example.com/taler-integration/my-id") - // Checking with specified port number - val withPort = getTalerWithdrawUri( - "https://www.example.com:9876", - "my-id" - ) - assertEquals(withPort, "taler://withdraw/www.example.com:9876/taler-integration/my-id") - } -}
\ No newline at end of file diff --git a/bank/src/test/kotlin/WireGatewayApiTest.kt b/bank/src/test/kotlin/WireGatewayApiTest.kt new file mode 100644 index 00000000..34b32296 --- /dev/null +++ b/bank/src/test/kotlin/WireGatewayApiTest.kt @@ -0,0 +1,529 @@ +import io.ktor.client.plugins.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.client.HttpClient +import io.ktor.http.* +import io.ktor.server.testing.* +import kotlinx.serialization.json.* +import kotlinx.coroutines.* +import org.junit.Test +import tech.libeufin.bank.* +import tech.libeufin.util.CryptoUtil +import java.util.* +import java.time.Instant +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import randHashCode + +class WireGatewayApiTest { + suspend fun Database.genTransfer(from: String, to: BankAccount, 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 + ), + username = from, + timestamp = Instant.now() + ).run { + assertEquals(TalerTransferResult.SUCCESS, txResult) + } + } + + suspend fun Database.genIncoming(to: String, from: BankAccount) { + talerAddIncomingCreate( + req = AddIncomingRequest( + reserve_pub = randShortHashCode(), + amount = TalerAmount(10, 0, "KUDOS"), + debit_account = from.internalPaytoUri, + ), + username = to, + timestamp = Instant.now() + ).run { + assertEquals(TalerAddIncomingResult.SUCCESS, txResult) + } + } + + // Test endpoint is correctly authenticated + suspend fun authRoutine(client: HttpClient, path: String, body: JsonObject? = null, method: HttpMethod = HttpMethod.Post) { + // No body when authentication must happen before parsing the body + + // Unknown account + client.request(path) { + this.method = method + basicAuth("unknown", "password") + }.assertStatus(HttpStatusCode.Unauthorized) + + // Wrong password + client.request(path) { + this.method = method + basicAuth("merchant", "wrong-password") + }.assertStatus(HttpStatusCode.Unauthorized) + + // Wrong account + client.request(path) { + this.method = method + basicAuth("exchange", "merchant-password") + }.assertStatus(HttpStatusCode.Unauthorized) + + // Not exchange account + client.request(path) { + this.method = method + if (body != null) jsonBody(body) + basicAuth("merchant", "merchant-password") + }.assertStatus(HttpStatusCode.Conflict) + } + + // Testing the POST /transfer call from the TWG API. + @Test + fun transfer() = bankSetup { db -> + val valid_req = json { + "request_uid" to randHashCode() + "amount" to "KUDOS:55" + "exchange_base_url" to "http://exchange.example.com/" + "wtid" to randShortHashCode() + "credit_account" to "payto://iban/MERCHANT-IBAN-XYZ" + }; + + authRoutine(client, "/accounts/merchant/taler-wire-gateway/transfer", valid_req) + + // Checking exchange debt constraint. + client.post("/accounts/exchange/taler-wire-gateway/transfer") { + basicAuth("exchange", "exchange-password") + jsonBody(valid_req) + }.assertStatus(HttpStatusCode.Conflict) + + // Giving debt allowance and checking the OK case. + assert(db.bankAccountSetMaxDebt( + 2L, + TalerAmount(1000, 0, "KUDOS") + )) + client.post("/accounts/exchange/taler-wire-gateway/transfer") { + basicAuth("exchange", "exchange-password") + jsonBody(valid_req) + }.assertOk() + + // check idempotency + client.post("/accounts/exchange/taler-wire-gateway/transfer") { + basicAuth("exchange", "exchange-password") + jsonBody(valid_req) + }.assertOk() + + // Trigger conflict due to reused request_uid + client.post("/accounts/exchange/taler-wire-gateway/transfer") { + basicAuth("exchange", "exchange-password") + jsonBody( + json(valid_req) { + "wtid" to randShortHashCode() + "exchange_base_url" to "http://different-exchange.example.com/" + } + ) + }.assertStatus(HttpStatusCode.Conflict) + + // Currency mismatch + client.post("/accounts/exchange/taler-wire-gateway/transfer") { + basicAuth("exchange", "exchange-password") + jsonBody( + json(valid_req) { + "amount" to "EUR:33" + } + ) + }.assertBadRequest() + + // Unknown account + client.post("/accounts/exchange/taler-wire-gateway/transfer") { + basicAuth("exchange", "exchange-password") + jsonBody( + json(valid_req) { + "request_uid" to randHashCode() + "wtid" to randShortHashCode() + "credit_account" to "payto://iban/UNKNOWN-IBAN-XYZ" + } + ) + }.assertStatus(HttpStatusCode.NotFound) + + // Bad BASE32 wtid + client.post("/accounts/exchange/taler-wire-gateway/transfer") { + basicAuth("exchange", "exchange-password") + jsonBody( + json(valid_req) { + "wtid" to "I love chocolate" + } + ) + }.assertBadRequest() + + // Bad BASE32 len wtid + client.post("/accounts/exchange/taler-wire-gateway/transfer") { + basicAuth("exchange", "exchange-password") + jsonBody( + json(valid_req) { + "wtid" to randBase32Crockford(31) + } + ) + }.assertBadRequest() + + // Bad BASE32 request_uid + client.post("/accounts/exchange/taler-wire-gateway/transfer") { + basicAuth("exchange", "exchange-password") + jsonBody( + json(valid_req) { + "request_uid" to "I love chocolate" + } + ) + }.assertBadRequest() + + // Bad BASE32 len wtid + client.post("/accounts/exchange/taler-wire-gateway/transfer") { + basicAuth("exchange", "exchange-password") + jsonBody( + json(valid_req) { + "request_uid" to randBase32Crockford(65) + } + ) + }.assertBadRequest() + } + + /** + * Testing the /history/incoming call from the TWG API. + */ + @Test + fun historyIncoming() = bankSetup { db -> + // Give Foo reasonable debt allowance: + assert( + db.bankAccountSetMaxDebt( + 1L, + TalerAmount(1000000, 0, "KUDOS") + ) + ) + + suspend fun HttpResponse.assertHistory(size: Int) { + assertOk() + val txt = this.bodyAsText() + val history = Json.decodeFromString<IncomingHistory>(txt) + val params = getHistoryParams(this.call.request.url.parameters) + + // testing the size is like expected. + assert(history.incoming_transactions.size == size) { + println("incoming_transactions has wrong size: ${history.incoming_transactions.size}") + println("Response was: ${txt}") + } + if (params.delta < 0) { + // testing that the first row_id is at most the 'start' query param. + assert(history.incoming_transactions[0].row_id <= params.start) + // testing that the row_id decreases. + assert(history.incoming_transactions.windowed(2).all { (a, b) -> a.row_id > b.row_id }) + } else { + // testing that the first row_id is at least the 'start' query param. + assert(history.incoming_transactions[0].row_id >= params.start) + // testing that the row_id increases. + assert(history.incoming_transactions.windowed(2).all { (a, b) -> a.row_id < b.row_id }) + } + } + + + authRoutine(client, "/accounts/merchant/taler-wire-gateway/history/incoming?delta=7", method = HttpMethod.Get) + + // Check error when no transactions + client.get("/accounts/exchange/taler-wire-gateway/history/incoming?delta=7") { + basicAuth("exchange", "exchange-password") + }.assertStatus(HttpStatusCode.NoContent) + + // Gen three transactions using clean add incoming logic + repeat(3) { + db.genIncoming("exchange", bankAccountMerchant) + } + // Should not show up in the taler wire gateway API history + db.bankTransactionCreate(genTx("bogus foobar")).assertSuccess() + // Bar pays Foo once, but that should not appear in the result. + db.bankTransactionCreate(genTx("payout", creditorId = 1, debtorId = 2)).assertSuccess() + // Gen two transactions using row bank transaction logic + repeat(2) { + db.bankTransactionCreate( + genTx(IncomingTxMetadata(randShortHashCode()).encode(), 2, 1) + ).assertSuccess() + } + + // Check ignore bogus subject + client.get("/accounts/exchange/taler-wire-gateway/history/incoming?delta=7") { + basicAuth("exchange", "exchange-password") + }.assertHistory(5) + + // Check skip bogus subject + client.get("/accounts/exchange/taler-wire-gateway/history/incoming?delta=5") { + basicAuth("exchange", "exchange-password") + }.assertHistory(5) + + // Check no useless polling + assertTime(0, 300) { + client.get("/accounts/exchange/taler-wire-gateway/history/incoming?delta=-6&start=15&long_poll_ms=1000") { + basicAuth("exchange", "exchange-password") + }.assertHistory(5) + } + + // Check polling end + client.get("/accounts/exchange/taler-wire-gateway/history/incoming?delta=6&long_poll_ms=60") { + basicAuth("exchange", "exchange-password") + }.assertHistory(5) + + runBlocking { + joinAll( + launch { // Check polling succeed forward + assertTime(200, 1000) { + client.get("/accounts/exchange/taler-wire-gateway/history/incoming?delta=6&long_poll_ms=1000") { + basicAuth("exchange", "exchange-password") + }.assertHistory(6) + } + }, + launch { // Check polling succeed backward + assertTime(200, 1000) { + client.get("/accounts/exchange/taler-wire-gateway/history/incoming?delta=-6&long_poll_ms=1000") { + basicAuth("exchange", "exchange-password") + }.assertHistory(6) + } + }, + launch { // Check polling timeout forward + assertTime(200, 400) { + client.get("/accounts/exchange/taler-wire-gateway/history/incoming?delta=8&long_poll_ms=300") { + basicAuth("exchange", "exchange-password") + }.assertHistory(6) + } + }, + launch { // Check polling timeout backward + assertTime(200, 400) { + client.get("/accounts/exchange/taler-wire-gateway/history/incoming?delta=-8&long_poll_ms=300") { + basicAuth("exchange", "exchange-password") + }.assertHistory(6) + } + }, + launch { + delay(200) + db.genIncoming("exchange", bankAccountMerchant) + } + ) + } + + // Testing ranges. + repeat(300) { + db.genIncoming("exchange", bankAccountMerchant) + } + + // forward range: + client.get("/accounts/exchange/taler-wire-gateway/history/incoming?delta=10&start=30") { + basicAuth("exchange", "exchange-password") + }.assertHistory(10) + + // backward range: + client.get("/accounts/exchange/taler-wire-gateway/history/incoming?delta=-10&start=300") { + basicAuth("exchange", "exchange-password") + }.assertHistory(10) + } + + + /** + * Testing the /history/outgoing call from the TWG API. + */ + @Test + fun historyOutgoing() = bankSetup { db -> + // Give Bar reasonable debt allowance: + assert( + db.bankAccountSetMaxDebt( + 2L, + TalerAmount(1000000, 0, "KUDOS") + ) + ) + + suspend fun HttpResponse.assertHistory(size: Int) { + assertOk() + val txt = this.bodyAsText() + val history = Json.decodeFromString<OutgoingHistory>(txt) + val params = getHistoryParams(this.call.request.url.parameters) + + // testing the size is like expected. + assert(history.outgoing_transactions.size == size) { + println("outgoing_transactions has wrong size: ${history.outgoing_transactions.size}") + println("Response was: ${txt}") + } + if (params.delta < 0) { + // testing that the first row_id is at most the 'start' query param. + assert(history.outgoing_transactions[0].row_id <= params.start) + // testing that the row_id decreases. + assert(history.outgoing_transactions.windowed(2).all { (a, b) -> a.row_id > b.row_id }) + } else { + // testing that the first row_id is at least the 'start' query param. + assert(history.outgoing_transactions[0].row_id >= params.start) + // testing that the row_id increases. + assert(history.outgoing_transactions.windowed(2).all { (a, b) -> a.row_id < b.row_id }) + } + } + + authRoutine(client, "/accounts/merchant/taler-wire-gateway/history/outgoing?delta=7", method = HttpMethod.Get) + + // Check error when no transactions + client.get("/accounts/exchange/taler-wire-gateway/history/outgoing?delta=7") { + basicAuth("exchange", "exchange-password") + }.assertStatus(HttpStatusCode.NoContent) + + // Gen three transactions using clean transfer logic + repeat(3) { + db.genTransfer("exchange", bankAccountMerchant) + } + // Should not show up in the taler wire gateway API history + db.bankTransactionCreate(genTx("bogus foobar", 1, 2)).assertSuccess() + // Foo pays Bar once, but that should not appear in the result. + db.bankTransactionCreate(genTx("payout")).assertSuccess() + // Gen two transactions using row bank transaction logic + repeat(2) { + db.bankTransactionCreate( + genTx(OutgoingTxMetadata(randShortHashCode(), ExchangeUrl("http://exchange.example.com/")).encode(), 1, 2) + ).assertSuccess() + } + + // Check ignore bogus subject + client.get("/accounts/exchange/taler-wire-gateway/history/outgoing?delta=7") { + basicAuth("exchange", "exchange-password") + }.assertHistory(5) + + // Check skip bogus subject + client.get("/accounts/exchange/taler-wire-gateway/history/outgoing?delta=5") { + basicAuth("exchange", "exchange-password") + }.assertHistory(5) + + // Check no useless polling + assertTime(0, 300) { + client.get("/accounts/exchange/taler-wire-gateway/history/outgoing?delta=-6&start=15&long_poll_ms=1000") { + basicAuth("exchange", "exchange-password") + }.assertHistory(5) + } + + // Check polling end + client.get("/accounts/exchange/taler-wire-gateway/history/outgoing?delta=6&long_poll_ms=60") { + basicAuth("exchange", "exchange-password") + }.assertHistory(5) + + runBlocking { + joinAll( + launch { // Check polling succeed forward + assertTime(200, 1000) { + client.get("/accounts/exchange/taler-wire-gateway/history/outgoing?delta=6&long_poll_ms=1000") { + basicAuth("exchange", "exchange-password") + }.assertHistory(6) + } + }, + launch { // Check polling succeed backward + assertTime(200, 1000) { + client.get("/accounts/exchange/taler-wire-gateway/history/outgoing?delta=-6&long_poll_ms=1000") { + basicAuth("exchange", "exchange-password") + }.assertHistory(6) + } + }, + launch { // Check polling timeout forward + assertTime(200, 400) { + client.get("/accounts/exchange/taler-wire-gateway/history/outgoing?delta=8&long_poll_ms=300") { + basicAuth("exchange", "exchange-password") + }.assertHistory(6) + } + }, + launch { // Check polling timeout backward + assertTime(200, 400) { + client.get("/accounts/exchange/taler-wire-gateway/history/outgoing?delta=-8&long_poll_ms=300") { + basicAuth("exchange", "exchange-password") + }.assertHistory(6) + } + }, + launch { + delay(200) + db.genTransfer("exchange", bankAccountMerchant) + } + ) + } + + // Testing ranges. + repeat(300) { + db.genTransfer("exchange", bankAccountMerchant) + } + + // forward range: + client.get("/accounts/exchange/taler-wire-gateway/history/outgoing?delta=10&start=30") { + basicAuth("exchange", "exchange-password") + }.assertHistory(10) + + // backward range: + client.get("/accounts/exchange/taler-wire-gateway/history/outgoing?delta=-10&start=300") { + basicAuth("exchange", "exchange-password") + }.assertHistory(10) + } + + // Testing the /admin/add-incoming call from the TWG API. + @Test + fun addIncoming() = bankSetup { db -> + val valid_req = json { + "amount" to "KUDOS:44" + "reserve_pub" to randEddsaPublicKey() + "debit_account" to "payto://iban/MERCHANT-IBAN-XYZ" + }; + + authRoutine(client, "/accounts/merchant/taler-wire-gateway/admin/add-incoming", valid_req) + + // Checking exchange debt constraint. + client.post("/accounts/exchange/taler-wire-gateway/admin/add-incoming") { + basicAuth("exchange", "exchange-password") + jsonBody(valid_req) + }.assertStatus(HttpStatusCode.Conflict) + + // Giving debt allowance and checking the OK case. + assert(db.bankAccountSetMaxDebt( + 1L, + TalerAmount(1000, 0, "KUDOS") + )) + + client.post("/accounts/exchange/taler-wire-gateway/admin/add-incoming") { + basicAuth("exchange", "exchange-password") + jsonBody(valid_req, deflate = true) + }.assertOk() + + // Trigger conflict due to reused reserve_pub + client.post("/accounts/exchange/taler-wire-gateway/admin/add-incoming") { + basicAuth("exchange", "exchange-password") + jsonBody(valid_req) + }.assertStatus(HttpStatusCode.Conflict) + + // Currency mismatch + client.post("/accounts/exchange/taler-wire-gateway/admin/add-incoming") { + basicAuth("exchange", "exchange-password") + jsonBody( + json(valid_req) { + "amount" to "EUR:33" + } + ) + }.assertBadRequest() + + // Unknown account + client.post("/accounts/exchange/taler-wire-gateway/admin/add-incoming") { + basicAuth("exchange", "exchange-password") + jsonBody( + json(valid_req) { + "reserve_pub" to randEddsaPublicKey() + "debit_account" to "payto://iban/UNKNOWN-IBAN-XYZ" + } + ) + }.assertStatus(HttpStatusCode.NotFound) + + // Bad BASE32 reserve_pub + client.post("/accounts/exchange/taler-wire-gateway/admin/add-incoming") { + basicAuth("exchange", "exchange-password") + jsonBody(json(valid_req) { + "reserve_pub" to "I love chocolate" + }) + }.assertBadRequest() + + // Bad BASE32 len reserve_pub + client.post("/accounts/exchange/taler-wire-gateway/admin/add-incoming") { + basicAuth("exchange", "exchange-password") + jsonBody(json(valid_req) { + "reserve_pub" to randBase32Crockford(31) + }) + }.assertBadRequest() + } +}
\ No newline at end of file diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt index d878ad2c..12dc7434 100644 --- a/bank/src/test/kotlin/helpers.kt +++ b/bank/src/test/kotlin/helpers.kt @@ -1,16 +1,68 @@ import io.ktor.http.* import io.ktor.client.statement.* import io.ktor.client.request.* +import io.ktor.server.testing.* import kotlinx.coroutines.* import kotlinx.serialization.json.* import net.taler.wallet.crypto.Base32Crockford -import kotlin.test.assertEquals +import kotlin.test.* import tech.libeufin.bank.* import java.io.ByteArrayOutputStream import java.util.zip.DeflaterOutputStream +import tech.libeufin.util.CryptoUtil /* ----- 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 bankSetup(lambda: suspend ApplicationTestBuilder.(Database) -> Unit) { + setup { 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)) + testApplication { + application { + corebankWebApp(db, ctx) + } + lambda(db) + } + } +} + fun setup( conf: String = "test.conf", lambda: suspend (Database, BankApplicationContext) -> Unit diff --git a/database-versioning/procedures.sql b/database-versioning/procedures.sql index 63ce8fa1..5a619e40 100644 --- a/database-versioning/procedures.sql +++ b/database-versioning/procedures.sql @@ -11,7 +11,7 @@ BEGIN normalized.val = amount.val + amount.frac / 100000000; normalized.frac = amount.frac % 100000000; END $$; -COMMENT ON PROCEDURE amount_normalize +COMMENT ON FUNCTION amount_normalize IS 'Returns the normalized amount by adding to the .val the value of (.frac / 100000000) and removing the modulus 100000000 from .frac.'; CREATE OR REPLACE FUNCTION amount_add( @@ -29,7 +29,7 @@ BEGIN RAISE EXCEPTION 'addition overflow'; END IF; END $$; -COMMENT ON PROCEDURE amount_add +COMMENT ON FUNCTION amount_add IS 'Returns the normalized sum of two amounts. It raises an exception when the resulting .val is larger than 2^52'; CREATE OR REPLACE FUNCTION amount_left_minus_right( |