commit c562b7612b4a305540487f0828d8edec4c8e10b7 parent 25ad86d9c9d41bf168f21c8339ad17569ea98f8f Author: Antoine A <> Date: Thu, 8 Aug 2024 12:28:57 +0200 common: support the extended wire gateway API Diffstat:
18 files changed, 465 insertions(+), 42 deletions(-)
diff --git a/Makefile b/Makefile @@ -120,11 +120,11 @@ doc: ./gradlew dokkaHtmlMultiModule echo "Open build/dokka/htmlMultiModule/index.html" -.PHONY: bench-db +.PHONY: bank-bench-db bank-bench-db: install-nobuild-files ./gradlew cleanTest :bank:test --tests Bench.benchDb -i --no-build-cache -.PHONY: bench-db +.PHONY: nexus-bench-db nexus-bench-db: install-nobuild-files ./gradlew cleanTest :nexus:test --tests Bench.benchDb -i --no-build-cache diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/BankIntegrationApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/api/BankIntegrationApi.kt @@ -30,9 +30,7 @@ import tech.libeufin.bank.* import tech.libeufin.bank.db.AbortResult import tech.libeufin.bank.db.Database import tech.libeufin.bank.db.WithdrawalDAO.WithdrawalSelectionResult -import tech.libeufin.common.TalerErrorCode -import tech.libeufin.common.conflict -import tech.libeufin.common.notFound +import tech.libeufin.common.* fun Routing.bankIntegrationApi(db: Database, ctx: BankConfig) { get("/taler-integration/config") { diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt @@ -448,9 +448,9 @@ private fun Routing.coreBankTransactionsApi(db: Database, ctx: BankConfig) { get("/accounts/{USERNAME}/transactions/{T_ID}") { val tId = call.longPath("T_ID") val tx = db.transaction.get(tId, username, ctx.payto) ?: throw notFound( - "Bank transaction '$tId' not found", - TalerErrorCode.BANK_TRANSACTION_NOT_FOUND - ) + "Bank transaction '$tId' not found", + TalerErrorCode.BANK_TRANSACTION_NOT_FOUND + ) call.respond(tx) } } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/WireGatewayApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/api/WireGatewayApi.kt @@ -110,6 +110,42 @@ fun Routing.wireGatewayApi(db: Database, ctx: BankConfig) { get("/accounts/{USERNAME}/taler-wire-gateway/history/outgoing") { historyEndpoint(::OutgoingHistory, ExchangeDAO::outgoingHistory) } + get("/accounts/{USERNAME}/taler-wire-gateway/transfers") { + val params = TransferParams.extract(context.request.queryParameters) + val bankAccount = call.bankInfo(db, ctx.payto) + if (!bankAccount.isTalerExchange) + throw conflict( + "$username is not an exchange account.", + TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE + ) + + if (params.status != null && params.status != TransferStatusState.success) { + call.respond(HttpStatusCode.NoContent) + } else { + val items = db.exchange.pageTransfer(params.page, bankAccount.bankAccountId, ctx.payto) + + if (items.isEmpty()) { + call.respond(HttpStatusCode.NoContent) + } else { + call.respond(TransferList(items, bankAccount.payto)) + } + } + } + get("/accounts/{USERNAME}/taler-wire-gateway/transfers/{ROW_ID}") { + val bankAccount = call.bankInfo(db, ctx.payto) + if (!bankAccount.isTalerExchange) + throw conflict( + "$username is not an exchange account.", + TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE + ) + + val txId = call.longPath("ROW_ID") + val transfer = db.exchange.getTransfer(bankAccount.bankAccountId, txId, ctx.payto) ?: throw notFound( + "Transfer '$txId' not found", + TalerErrorCode.BANK_TRANSACTION_NOT_FOUND + ) + call.respond(transfer) + } } authAdmin(db, ctx.pwCrypto, TokenScope.readwrite) { suspend fun ApplicationCall.addIncoming( diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt @@ -163,6 +163,75 @@ class ExchangeDAO(private val db: Database) { } } + /** Get status of transfer [txId] of account [exchangeId] */ + suspend fun getTransfer( + exchangeId: Long, + txId: Long, + ctx: BankPaytoCtx + ): TransferStatus? = db.serializable( + """ + SELECT + wtid + ,exchange_base_url + ,transaction_date + ,(amount).val AS amount_val + ,(amount).frac AS amount_frac + ,creditor_payto_uri + ,creditor_name + FROM taler_exchange_outgoing + JOIN bank_account_transactions ON bank_transaction=bank_transaction_id + WHERE bank_transaction_id=? AND bank_account_id=? + """ + ) { + setLong(1, txId) + setLong(2, exchangeId) + oneOrNull { + TransferStatus( + status = TransferStatusState.success, + status_msg = null, + amount = it.getAmount("amount", db.bankCurrency), + origin_exchange_url = it.getString("exchange_base_url"), + wtid = ShortHashCode(it.getBytes("wtid")), + credit_account = it.getBankPayto("creditor_payto_uri", "creditor_name", ctx), + timestamp = it.getTalerTimestamp("transaction_date"), + ) + } + } + + /** Get a page of transfers status of account [exchangeId] */ + suspend fun pageTransfer( + params: PageParams, + exchangeId: Long, + ctx: BankPaytoCtx + ): List<TransferListStatus> = db.page( + params, + "bank_transaction_id", + """ + SELECT + bank_transaction_id + ,transaction_date + ,(amount).val AS amount_val + ,(amount).frac AS amount_frac + ,creditor_payto_uri + ,creditor_name + FROM taler_exchange_outgoing + JOIN bank_account_transactions ON bank_transaction=bank_transaction_id + WHERE bank_account_id=? AND + """, + { + setLong(1, exchangeId) + 1 + } + ) { + TransferListStatus( + row_id = it.getLong("bank_transaction_id"), + status = TransferStatusState.success, + amount = it.getAmount("amount", db.bankCurrency), + credit_account = it.getBankPayto("creditor_payto_uri", "creditor_name", ctx), + timestamp = it.getTalerTimestamp("transaction_date"), + ) + } + /** Result of taler add incoming transaction creation */ sealed interface AddIncomingResult { /** Transaction [id] and wire transfer [timestamp] */ diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt @@ -39,24 +39,6 @@ import tech.libeufin.common.* import tech.libeufin.common.api.intercept import java.util.* -fun ApplicationCall.uuidPath(name: String): UUID { - val value = parameters[name]!! - try { - return UUID.fromString(value) - } catch (e: Exception) { - throw badRequest("UUID uri component malformed: ${e.message}", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) // TODO better error ? - } -} - -fun ApplicationCall.longPath(name: String): Long { - val value = parameters[name]!! - try { - return value.toLong() - } catch (e: Exception) { - throw badRequest("Long uri component malformed: ${e.message}", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) // TODO better error ? - } -} - /** Retrieve the bank account info for the selected username*/ suspend fun ApplicationCall.bankInfo(db: Database, ctx: BankPaytoCtx): BankInfo = db.account.bankInfo(username, ctx) ?: throw unknownAccount(username) diff --git a/bank/src/test/kotlin/WireGatewayApiTest.kt b/bank/src/test/kotlin/WireGatewayApiTest.kt @@ -122,13 +122,77 @@ class WireGatewayApiTest { } }.assertBadRequest() } + + // GET /accounts/{USERNAME}/taler-wire-gateway/transfers/{ROW_ID} + @Test + fun transferById() = bankSetup { + val wtid = ShortHashCode.rand() + val valid_req = obj { + "request_uid" to HashCode.rand() + "amount" to "KUDOS:0.12" + "exchange_base_url" to "http://exchange.example.com/" + "wtid" to wtid + "credit_account" to merchantPayto.canonical + } + + authRoutine(HttpMethod.Get, "/accounts/merchant/taler-wire-gateway/transfers/1", requireExchange = true) + + val resp = client.postA("/accounts/exchange/taler-wire-gateway/transfer") { + json(valid_req) + }.assertOkJson<TransferResponse>() + + // Check OK + client.getA("/accounts/exchange/taler-wire-gateway/transfers/${resp.row_id}") + .assertOkJson<TransferStatus> { tx -> + assertEquals(TransferStatusState.success, tx.status) + assertEquals(TalerAmount("KUDOS:0.12"), tx.amount) + assertEquals("http://exchange.example.com/", tx.origin_exchange_url) + assertEquals(wtid, tx.wtid) + assertEquals(resp.timestamp, tx.timestamp) + } + // Check unknown transaction + client.getA("/accounts/exchange/taler-wire-gateway/transfers/42") + .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) + // Check another user's transaction + client.getA("/accounts/merchant/taler-wire-gateway/transfers/${resp.row_id}") + .assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE) + } + + // GET /accounts/{USERNAME}/taler-wire-gateway/transfers + @Test + fun transferPage() = bankSetup { + authRoutine(HttpMethod.Get, "/accounts/merchant/taler-wire-gateway/transfers", requireExchange = true) + + client.getA("/accounts/exchange/taler-wire-gateway/transfers").assertNoContent() + + repeat(5) { + client.postA("/accounts/exchange/taler-wire-gateway/transfer") { + json { + "request_uid" to HashCode.rand() + "amount" to "KUDOS:0.12" + "exchange_base_url" to "http://exchange.example.com/" + "wtid" to ShortHashCode.rand() + "credit_account" to merchantPayto.canonical + } + }.assertOkJson<TransferResponse>() + } + client.getA("/accounts/exchange/taler-wire-gateway/transfers") + .assertOkJson<TransferList> { + assertEquals(5, it.transfers.size) + assertEquals( + it, + client.getA("/accounts/exchange/taler-wire-gateway/transfers?status=success").assertOkJson<TransferList>() + ) + } + client.getA("/accounts/exchange/taler-wire-gateway/transfers?status=pending").assertNoContent() + } // GET /accounts/{USERNAME}/taler-wire-gateway/history/incoming @Test fun historyIncoming() = bankSetup { // Give Foo reasonable debt allowance: setMaxDebt("merchant", "KUDOS:1000") - authRoutine(HttpMethod.Get, "/accounts/merchant/taler-wire-gateway/history/incoming") + authRoutine(HttpMethod.Get, "/accounts/merchant/taler-wire-gateway/history/incoming", requireExchange = true) historyRoutine<IncomingHistory>( url = "/accounts/exchange/taler-wire-gateway/history/incoming", ids = { it.incoming_transactions.map { it.row_id } }, @@ -162,7 +226,7 @@ class WireGatewayApiTest { @Test fun historyOutgoing() = bankSetup { setMaxDebt("exchange", "KUDOS:1000000") - authRoutine(HttpMethod.Get, "/accounts/merchant/taler-wire-gateway/history/outgoing") + authRoutine(HttpMethod.Get, "/accounts/merchant/taler-wire-gateway/history/outgoing", requireExchange = true) historyRoutine<OutgoingHistory>( url = "/accounts/exchange/taler-wire-gateway/history/outgoing", ids = { it.outgoing_transactions.map { it.row_id } }, diff --git a/bank/src/test/kotlin/bench.kt b/bank/src/test/kotlin/bench.kt @@ -69,8 +69,8 @@ class Bench { }, "bank_account_transactions(creditor_payto_uri, creditor_name, debtor_payto_uri, debtor_name, subject, amount, transaction_date, direction, bank_account_id)" to { val account = if (it > mid) customerAccount else it+4 - "creditor_payto\tcreditor_name\tdebtor_payto\tdebtor_name\tsubject\t(42,0)\t0\tcredit\t$exchangeAccount\n" + - "creditor_payto\tcreditor_name\tdebtor_payto\tdebtor_name\tsubject\t(42,0)\t0\tdebit\t$account\n" + "$unknownPayto\tcreditor_name\t$unknownPayto\tdebtor_name\tsubject\t(42,0)\t0\tcredit\t$exchangeAccount\n" + + "$unknownPayto\tcreditor_name\t$unknownPayto\tdebtor_name\tsubject\t(42,0)\t0\tdebit\t$account\n" }, "bank_transaction_operations" to { val hex = token32.rand().encodeHex() @@ -294,6 +294,12 @@ class Bench { } }.assertOk() } + measureAction("wg_transfer_get") { + client.getA("/accounts/exchange/taler-wire-gateway/transfers/1").assertOk() + } + measureAction("wg_transfer_page") { + client.getA("/accounts/exchange/taler-wire-gateway/transfers").assertOk() + } measureAction("wg_add") { client.postA("/accounts/exchange/taler-wire-gateway/admin/add-incoming") { json { diff --git a/common/src/main/kotlin/Constants.kt b/common/src/main/kotlin/Constants.kt @@ -26,7 +26,7 @@ const val SERIALIZATION_RETRY: Int = 10 const val MAX_BODY_LENGTH: Int = 4 * 1024 // 4kB // API version -const val WIRE_GATEWAY_API_VERSION: String = "2:0:1" +const val WIRE_GATEWAY_API_VERSION: String = "3:0:2" const val REVENUE_API_VERSION: String = "1:0:1" // HTTP headers diff --git a/common/src/main/kotlin/TalerMessage.kt b/common/src/main/kotlin/TalerMessage.kt @@ -27,6 +27,13 @@ enum class TalerIncomingType { wad } +enum class TransferStatusState { + pending, + transient_failure, + permanent_failure, + success +} + /** Response GET /taler-wire-gateway/config */ @Serializable data class WireGatewayConfig( @@ -53,6 +60,33 @@ data class TransferResponse( val row_id: Long ) +/** Request GET /taler-wire-gateway/transfers */ +@Serializable +data class TransferList( + val transfers: List<TransferListStatus>, + val debit_account: String +) +@Serializable +data class TransferListStatus( + val row_id: Long, + val status: TransferStatusState, + val amount: TalerAmount, + val credit_account: String, + val timestamp: TalerProtocolTimestamp +) + +/** Request GET /taler-wire-gateway/transfers/{ROW_iD} */ +@Serializable +data class TransferStatus( + val status: TransferStatusState, + val status_msg: String? = null, + val amount: TalerAmount, + val origin_exchange_url: String, + val wtid: ShortHashCode, + val credit_account: String, + val timestamp: TalerProtocolTimestamp +) + /** Request POST /taler-wire-gateway/admin/add-incoming */ @Serializable data class AddIncomingRequest( diff --git a/common/src/main/kotlin/helpers.kt b/common/src/main/kotlin/helpers.kt @@ -19,6 +19,7 @@ package tech.libeufin.common +import io.ktor.server.application.* import java.io.ByteArrayOutputStream import java.io.FilterInputStream import java.io.InputStream @@ -143,4 +144,24 @@ fun Throwable.fmtLog(logger: Logger) { inline fun Logger.debug(lambda: () -> String) { if (isDebugEnabled()) debug(lambda()) +} + +/* ----- KTOR ----- */ + +fun ApplicationCall.uuidPath(name: String): UUID { + val value = parameters[name]!! + try { + return UUID.fromString(value) + } catch (e: Exception) { + throw badRequest("UUID uri component malformed: ${e.message}", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) // TODO better error ? + } +} + +fun ApplicationCall.longPath(name: String): Long { + val value = parameters[name]!! + try { + return value.toLong() + } catch (e: Exception) { + throw badRequest("Long uri component malformed: ${e.message}", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) // TODO better error ? + } } \ No newline at end of file diff --git a/common/src/main/kotlin/params.kt b/common/src/main/kotlin/params.kt @@ -77,6 +77,24 @@ data class PageParams( } } +data class TransferParams( + val page: PageParams, val status: TransferStatusState? +) { + companion object { + private val names = TransferStatusState.entries.map { it.name } + private val names_fmt = names.joinToString() + fun extract(params: Parameters): TransferParams { + val status = params["status"]?.let { + if (!names.contains(it)) { + throw badRequest("Param 'status' must be one of $names_fmt", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) + } + TransferStatusState.valueOf(it) + } + return TransferParams(PageParams.extract(params), status) + } + } +} + data class PollingParams( val timeout_ms: Long ) { diff --git a/common/src/main/kotlin/test/bench.kt b/common/src/main/kotlin/test/bench.kt @@ -20,7 +20,8 @@ package tech.libeufin.common.test import tech.libeufin.common.* -import org.postgresql.jdbc.PgConnection +import org.postgresql.jdbc.* +import org.postgresql.copy.* import java.time.Instant import java.time.LocalDateTime import java.time.ZoneId @@ -33,16 +34,15 @@ import kotlin.time.measureTime import kotlin.time.toDuration fun PgConnection.genData(amount: Int, generators: Sequence<Pair<String, (Int) -> String>>) { - val copyManager = this.getCopyAPI() - for ((table, generator) in generators) { println("Gen rows for $table") - val full = buildString(150*amount) { + PGCopyOutputStream(this, "COPY $table FROM STDIN", 1024 * 1024).use { out -> repeat(amount) { - append(generator(it+1)) + val str = generator(it+1) + val bytes = str.toByteArray() + out.write(bytes) } } - copyManager.copyIn("COPY $table FROM STDIN", full.reader()) } // Update database statistics for better perf diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/api/WireGatewayApi.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/api/WireGatewayApi.kt @@ -68,6 +68,24 @@ fun Routing.wireGatewayApi(db: Database, cfg: NexusConfig) = authApi(cfg.wireGat ) } } + get("/taler-wire-gateway/transfers") { + val params = TransferParams.extract(context.request.queryParameters) + val items = db.exchange.pageTransfer(params) + + if (items.isEmpty()) { + call.respond(HttpStatusCode.NoContent) + } else { + call.respond(TransferList(items, cfg.ebics.payto)) + } + } + get("/taler-wire-gateway/transfers/{ROW_ID}") { + val id = call.longPath("ROW_ID") + val transfer = db.exchange.getTransfer(id) ?: throw notFound( + "Transfer '$id' not found", + TalerErrorCode.BANK_TRANSACTION_NOT_FOUND + ) + call.respond(transfer) + } suspend fun <T> PipelineContext<Unit, ApplicationCall>.historyEndpoint( reduce: (List<T>, String) -> Any, dbLambda: suspend ExchangeDAO.(HistoryParams) -> List<T> diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/db/Database.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/db/Database.kt @@ -40,6 +40,13 @@ data class InitiatedPayment( val requestUid: String ) +enum class SubmissionState { + unsubmitted, + transient_failure, + permanent_failure, + success +} + /** * Collects database connection steps and any operation on the Nexus tables. */ diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/db/ExchangeDAO.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/db/ExchangeDAO.kt @@ -70,10 +70,10 @@ class ExchangeDAO(private val db: Database) { = db.poolHistoryGlobal(params, db::listenOutgoing, """ SELECT outgoing_transaction_id - ,execution_time AS execution_time + ,execution_time ,(amount).val AS amount_val ,(amount).frac AS amount_frac - ,credit_payto_uri AS credit_payto_uri + ,credit_payto_uri ,wtid ,exchange_base_url FROM talerable_outgoing_transactions @@ -108,12 +108,11 @@ class ExchangeDAO(private val db: Database) { out_request_uid_reuse ,out_tx_row_id ,out_timestamp - FROM - taler_transfer ( + FROM taler_transfer ( ?, ?, ?, (?,?)::taler_amount, ?, ?, ?, ? - ); + ) """ ) { val subject = "${req.wtid} ${req.exchange_base_url.url}" @@ -137,4 +136,90 @@ class ExchangeDAO(private val db: Database) { } } } + + /** Get status of transfer [id] */ + suspend fun getTransfer( + id: Long + ): TransferStatus? = db.serializable( + """ + SELECT + wtid + ,exchange_base_url + ,(amount).val AS amount_val + ,(amount).frac AS amount_frac + ,submitted + ,credit_payto_uri + ,initiation_time + ,failure_message + FROM transfer_operations + JOIN initiated_outgoing_transactions USING (initiated_outgoing_transaction_id) + WHERE initiated_outgoing_transaction_id=? + """ + ) { + setLong(1, id) + oneOrNull { + TransferStatus( + status = when (it.getEnum<SubmissionState>("submitted")) { + SubmissionState.unsubmitted -> TransferStatusState.pending + SubmissionState.transient_failure -> TransferStatusState.transient_failure + SubmissionState.permanent_failure -> TransferStatusState.permanent_failure + SubmissionState.success -> TransferStatusState.success + }, + status_msg = it.getString("failure_message"), + amount = it.getAmount("amount", db.bankCurrency), + origin_exchange_url = it.getString("exchange_base_url"), + wtid = ShortHashCode(it.getBytes("wtid")), + credit_account = it.getString("credit_payto_uri"), + timestamp = it.getTalerTimestamp("initiation_time"), + ) + } + } + + /** Get a page of transfers status */ + suspend fun pageTransfer( + params: TransferParams + ): List<TransferListStatus> = db.page( + params.page, + "initiated_outgoing_transaction_id", + """ + SELECT + initiated_outgoing_transaction_id + ,(amount).val AS amount_val + ,(amount).frac AS amount_frac + ,submitted + ,credit_payto_uri + ,initiation_time + FROM transfer_operations + JOIN initiated_outgoing_transactions USING (initiated_outgoing_transaction_id) + WHERE ${if (params.status != null) "submitted=?::submission_state AND" else ""} + """, + { + val state = when (params.status) { + TransferStatusState.pending -> SubmissionState.unsubmitted + TransferStatusState.transient_failure -> SubmissionState.transient_failure + TransferStatusState.permanent_failure -> SubmissionState.permanent_failure + TransferStatusState.success -> SubmissionState.success + null -> null + } + if (state != null) { + setString(1, state.name) + 1 + } else { + 0 + } + } + ) { + TransferListStatus( + row_id = it.getLong("initiated_outgoing_transaction_id"), + status = when (it.getEnum<SubmissionState>("submitted")) { + SubmissionState.unsubmitted -> TransferStatusState.pending + SubmissionState.transient_failure -> TransferStatusState.transient_failure + SubmissionState.permanent_failure -> TransferStatusState.permanent_failure + SubmissionState.success -> TransferStatusState.success + }, + amount = it.getAmount("amount", db.bankCurrency), + credit_account = it.getString("credit_payto_uri"), + timestamp = it.getTalerTimestamp("initiation_time"), + ) + } } \ No newline at end of file diff --git a/nexus/src/test/kotlin/WireGatewayApiTest.kt b/nexus/src/test/kotlin/WireGatewayApiTest.kt @@ -24,6 +24,7 @@ import org.junit.Test import tech.libeufin.common.* import tech.libeufin.nexus.cli.ingestOutgoingPayment import kotlin.test.* +import java.time.Instant class WireGatewayApiTest { // GET /taler-wire-gateway/config @@ -107,6 +108,81 @@ class WireGatewayApiTest { } }.assertBadRequest() } + + // GET /taler-wire-gateway/transfers/{ROW_ID} + @Test + fun transferById() = serverSetup { + val wtid = ShortHashCode.rand() + val valid_req = obj { + "request_uid" to HashCode.rand() + "amount" to "CHF:55" + "exchange_base_url" to "http://exchange.example.com/" + "wtid" to wtid + "credit_account" to grothoffPayto + } + + authRoutine(HttpMethod.Get, "/taler-wire-gateway/transfers/1") + + val resp = client.postA("/taler-wire-gateway/transfer") { + json(valid_req) + }.assertOkJson<TransferResponse>() + + // Check OK + client.getA("/taler-wire-gateway/transfers/${resp.row_id}") + .assertOkJson<TransferStatus> { tx -> + assertEquals(TransferStatusState.pending, tx.status) + assertEquals(TalerAmount("CHF:55"), tx.amount) + assertEquals("http://exchange.example.com/", tx.origin_exchange_url) + assertEquals(wtid, tx.wtid) + assertEquals(resp.timestamp, tx.timestamp) + } + + // Check unknown transaction + client.getA("/taler-wire-gateway/transfers/42") + .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) + } + + // GET /accounts/{USERNAME}/taler-wire-gateway/transfers + @Test + fun transferPage() = serverSetup { db -> + authRoutine(HttpMethod.Get, "/taler-wire-gateway/transfers") + + client.getA("/taler-wire-gateway/transfers").assertNoContent() + + repeat(6) { + client.postA("/taler-wire-gateway/transfer") { + json { + "request_uid" to HashCode.rand() + "amount" to "CHF:55" + "exchange_base_url" to "http://exchange.example.com/" + "wtid" to ShortHashCode.rand() + "credit_account" to grothoffPayto + } + }.assertOkJson<TransferResponse>() + } + client.getA("/taler-wire-gateway/transfers") + .assertOkJson<TransferList> { + assertEquals(6, it.transfers.size) + assertEquals( + it, + client.getA("/taler-wire-gateway/transfers?status=pending").assertOkJson<TransferList>() + ) + } + client.getA("/taler-wire-gateway/transfers?status=success").assertNoContent() + + db.initiated.submissionSuccess(1, Instant.now(), "ORDER1") + db.initiated.submissionFailure(2, Instant.now(), "Failure") + db.initiated.submissionFailure(3, Instant.now(), "Failure") + client.getA("/taler-wire-gateway/transfers?status=success").assertOkJson<TransferList> { + assertEquals(1, it.transfers.size) + } + client.getA("/taler-wire-gateway/transfers?status=transient_failure").assertOkJson<TransferList> { + assertEquals(2, it.transfers.size) + } + client.getA("/taler-wire-gateway/transfers?status=pending").assertOkJson<TransferList> { + assertEquals(3, it.transfers.size) + } + } // GET /taler-wire-gateway/history/incoming @Test diff --git a/nexus/src/test/kotlin/bench.kt b/nexus/src/test/kotlin/bench.kt @@ -137,6 +137,15 @@ class Bench { } }.assertOk() } + measureAction("wg_transfer_get") { + client.getA("/taler-wire-gateway/transfers/42").assertOk() + } + measureAction("wg_transfer_page") { + client.getA("/taler-wire-gateway/transfers").assertOk() + } + measureAction("wg_transfer_page_filter") { + client.getA("/taler-wire-gateway/transfers?status=success").assertNoContent() + } measureAction("wg_add") { client.postA("/taler-wire-gateway/admin/add-incoming") { json {