libeufin

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

commit 94c6f605ef7ddeb9aa3453fba044a2a733e46ed4
parent 1f5e14e210d1f68ec7f73454506869e020c55d3e
Author: Antoine A <>
Date:   Thu,  5 Oct 2023 10:40:55 +0000

Fix /taler-wire-gateway/history/incoming and improve test

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt | 69+++++++++++++++++++++++++++++++++++++++++----------------------------
Mbank/src/test/kotlin/TalerApiTest.kt | 109++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
2 files changed, 105 insertions(+), 73 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt @@ -32,6 +32,7 @@ import org.slf4j.LoggerFactory import tech.libeufin.util.extractReservePubFromSubject import tech.libeufin.util.stripIbanPayto import java.time.Instant +import kotlin.math.abs private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.nexus") @@ -114,38 +115,50 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: BankApplicationContext) ?: throw internalServerError("Customer '$accountName' lacks bank account.") if (!bankAccount.isTalerExchange) throw forbidden("History is not related to a Taler exchange.") - val history: List<BankAccountTransaction> = db.bankTransactionGetHistory( - start = params.start, - delta = params.delta, - bankAccountId = bankAccount.expectRowId(), - withDirection = TransactionDirection.credit - ) - if (history.isEmpty()) { - call.respond(HttpStatusCode.NoContent) - return@get - } val resp = IncomingHistory(credit_account = bankAccount.internalPaytoUri) - history.forEach { - val reservePub = extractReservePubFromSubject(it.subject) - if (reservePub == null) { - // This should usually not happen in the first place, - // because transactions to the exchange without a valid - // reserve pub should be bounced. - logger.warn("exchange account ${c.login} contains invalid incoming transaction ${it.expectRowId()}") - } else { - resp.incoming_transactions.add( - IncomingReserveTransaction( - row_id = it.expectRowId(), - amount = it.amount, - date = TalerProtocolTimestamp(it.transactionDate), - debit_account = it.debtorPaytoUri, - reserve_pub = reservePub + var start = params.start + var delta = params.delta + + // As we may ignore rows containing incorrect subjects, we may have to run several queries. + while (delta != 0L) { + val history: List<BankAccountTransaction> = db.bankTransactionGetHistory( + start = start, + delta = delta, + bankAccountId = bankAccount.expectRowId(), + withDirection = TransactionDirection.credit + ) + if (history.isEmpty()) + break; + history.forEach { + val reservePub = extractReservePubFromSubject(it.subject) + if (reservePub == null) { + // This should usually not happen in the first place, + // because transactions to the exchange without a valid + // reserve pub should be bounced. + logger.warn("exchange account ${c.login} contains invalid incoming transaction ${it.expectRowId()}") + } else { + // Register new transacation + resp.incoming_transactions.add( + IncomingReserveTransaction( + row_id = it.expectRowId(), + amount = it.amount, + date = TalerProtocolTimestamp(it.transactionDate), + debit_account = it.debtorPaytoUri, + reserve_pub = reservePub + ) ) - ) + // Advance cursor + start = it.expectRowId() + if (delta < 0) delta++ else delta--; + } } } - call.respond(resp) - return@get + + if (resp.incoming_transactions.isEmpty()) { + call.respond(HttpStatusCode.NoContent) + } else { + call.respond(resp) + } } post("/accounts/{USERNAME}/taler-wire-gateway/admin/add-incoming") { diff --git a/bank/src/test/kotlin/TalerApiTest.kt b/bank/src/test/kotlin/TalerApiTest.kt @@ -197,67 +197,86 @@ class TalerApiTest { TalerAmount(1000000, 0, "KUDOS") ) ) - // Foo pays Bar (the exchange) twice. - val reservePubOne = "5ZFS98S1K4Y083W95GVZK638TSRE44RABVASB3AFA3R95VCW17V0" - val reservePubTwo = "TFBT5NEVT8D2GETZ4DRF7C69XZHKHJ15296HRGB1R5ARNK0SP8A0" - db.bankTransactionCreate(genTx(reservePubOne)).assertSuccess() - db.bankTransactionCreate(genTx(reservePubTwo)).assertSuccess() - // 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() // Bar expects two entries in the incoming history testApplication { application { corebankWebApp(db, ctx) } - val resp = client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=5") { + + // Check error when no transactions + client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=7") { basicAuth("bar", "secret") - }.assertOk() - val j: IncomingHistory = Json.decodeFromString(resp.bodyAsText()) - assertEquals(2, j.incoming_transactions.size) + }.assertStatus(HttpStatusCode.NoContent) + + // Foo pays Bar (the exchange) three time + repeat(3) { + db.bankTransactionCreate(genTx(randShortHashCode().encoded)).assertSuccess() + } + // 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() + // Foo pays Bar (the exchange) twice, we should see five valid transactions + repeat(2) { + db.bankTransactionCreate(genTx(randShortHashCode().encoded)).assertSuccess() + } + + // Check ignore bogus subject + client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=7") { + basicAuth("bar", "secret") + }.assertOk().run { + val j: IncomingHistory = Json.decodeFromString(this.bodyAsText()) + assertEquals(5, j.incoming_transactions.size) + } + + // Check skip bogus subject + client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=5") { + basicAuth("bar", "secret") + }.assertOk().run { + val j: IncomingHistory = Json.decodeFromString(this.bodyAsText()) + assertEquals(5, j.incoming_transactions.size) + } + // Testing ranges. - val mockReservePub = Base32Crockford.encode(ByteArray(32)) + val mockReservePub = randShortHashCode().encoded for (i in 1..400) db.bankTransactionCreate(genTx(mockReservePub)).assertSuccess() + // forward range: - val range = client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=10&start=30") { + client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=10&start=30") { basicAuth("bar", "secret") - }.assertOk() - val rangeObj = Json.decodeFromString<IncomingHistory>(range.bodyAsText()) - // testing the size is like expected. - assert(rangeObj.incoming_transactions.size == 10) { - println("incoming_transaction has wrong size: ${rangeObj.incoming_transactions.size}") - println("Response was: ${range.bodyAsText()}") + }.assertOk().run { + val txt = this.bodyAsText() + val history = Json.decodeFromString<IncomingHistory>(txt) + // testing the size is like expected. + assert(history.incoming_transactions.size == 10) { + println("incoming_transaction has wrong size: ${history.incoming_transactions.size}") + println("Response was: ${txt}") + } + // testing that the first row_id is at least the 'start' query param. + assert(history.incoming_transactions[0].row_id >= 30) + // testing that the row_id increases. + assert(history.incoming_transactions.windowed(2).all { (a, b) -> a.row_id < b.row_id }) } - // testing that the first row_id is at least the 'start' query param. - assert(rangeObj.incoming_transactions[0].row_id >= 30) - // testing that the row_id increases. - for (idx in 1..9) - assert(rangeObj.incoming_transactions[idx].row_id > rangeObj.incoming_transactions[idx - 1].row_id) + // backward range: - val rangeBackward = client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=-10&start=300") { + client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=-10&start=300") { basicAuth("bar", "secret") - }.assertOk() - val rangeBackwardObj = Json.decodeFromString<IncomingHistory>(rangeBackward.bodyAsText()) - // testing the size is like expected. - assert(rangeBackwardObj.incoming_transactions.size == 10) { - println("incoming_transaction has wrong size: ${rangeBackwardObj.incoming_transactions.size}") - println("Response was: ${rangeBackward.bodyAsText()}") - } - // testing that the first row_id is at most the 'start' query param. - assert(rangeBackwardObj.incoming_transactions[0].row_id <= 300) - // testing that the row_id decreases. - for (idx in 1..9) - assert( - rangeBackwardObj.incoming_transactions[idx].row_id < rangeBackwardObj.incoming_transactions[idx - 1].row_id - ) { - println("negative delta didn't return decreasing row_id's in idx: $idx") - println("[$idx] -> ${rangeBackwardObj.incoming_transactions[idx].row_id}") - println("[${idx - 1}] -> ${rangeBackwardObj.incoming_transactions[idx - 1].row_id}") - println(rangeBackward.bodyAsText()) + }.assertOk().run { + val txt = this.bodyAsText() + val history = Json.decodeFromString<IncomingHistory>(txt) + // testing the size is like expected. + assert(history.incoming_transactions.size == 10) { + println("incoming_transaction has wrong size: ${history.incoming_transactions.size}") + println("Response was: ${txt}") } + // testing that the first row_id is at most the 'start' query param. + assert(history.incoming_transactions[0].row_id <= 300) + // testing that the row_id decreases. + assert(history.incoming_transactions.windowed(2).all { (a, b) -> a.row_id > b.row_id }) + } + } }