libeufin

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

commit 1541b50689decb3c74abfb58c7d47e9dc9d05bc8
parent 348a6945996eed2c180ff0d115ef00dbb32a659a
Author: Antoine A <>
Date:   Mon, 20 Nov 2023 17:11:28 +0000

Common history test routine

Diffstat:
Mbank/src/test/kotlin/CoreBankApiTest.kt | 164+++++++++++++++++++++----------------------------------------------------------
Mbank/src/test/kotlin/RevenueApiTest.kt | 127++++++++++++++-----------------------------------------------------------------
Mbank/src/test/kotlin/WireGatewayApiTest.kt | 258+++++++++++++++----------------------------------------------------------------
Abank/src/test/kotlin/routines.kt | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 271 insertions(+), 435 deletions(-)

diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt @@ -548,70 +548,39 @@ class CoreBankTransactionsApiTest { // GET /transactions @Test fun testHistory() = bankSetup { _ -> - suspend fun HttpResponse.assertHistory(size: Int) { - assertHistoryIds<BankAccountTransactionsResponse>(size) { - it.transactions.map { it.row_id } - } - } - - authRoutine("/accounts/merchant/transactions", method = HttpMethod.Get) - - // Check error when no transactions - client.get("/accounts/merchant/transactions") { - pwAuth("merchant") - }.assertNoContent() - - // Gen three transactions from merchant to exchange - repeat(3) { - tx("merchant", "KUDOS:0.$it", "customer") - } - // Gen two transactions from exchange to merchant - repeat(2) { - tx("customer", "KUDOS:0.$it", "merchant") - } - - // Check no useless polling - assertTime(0, 100) { - client.get("/accounts/merchant/transactions?delta=-6&start=11&long_poll_ms=1000") { - pwAuth("merchant") - }.assertHistory(5) - } - - // Check no polling when find transaction - assertTime(0, 100) { - client.getA("/accounts/merchant/transactions?delta=6&long_poll_ms=1000") - .assertHistory(5) - } - - coroutineScope { - launch { // Check polling succeed - assertTime(100, 200) { - client.getA("/accounts/merchant/transactions?delta=2&start=10&long_poll_ms=1000") - .assertHistory(1) + authRoutine("/accounts/customer/transactions", method = HttpMethod.Get) + historyRoutine<BankAccountTransactionsResponse>( + url = "/accounts/customer/transactions", + ids = { it.transactions.map { it.row_id } }, + registered = listOf( + { + // Transactions from merchant to exchange + tx("merchant", "KUDOS:0.1", "customer") + }, + { + // Transactions from exchange to merchant + tx("customer", "KUDOS:0.1", "merchant") + }, + { + // Transactions from merchant to exchange + tx("merchant", "KUDOS:0.1", "customer") + }, + { + // Cashout from merchant + cashout("KUDOS:0.1") } - } - launch { // Check polling timeout - assertTime(200, 300) { - client.getA("/accounts/merchant/transactions?delta=1&start=11&long_poll_ms=200") - .assertNoContent() + ), + ignored = listOf( + { + // Ignore transactions of other accounts + tx("merchant", "KUDOS:0.1", "exchange") + }, + { + // Ignore transactions of other accounts + tx("exchange", "KUDOS:0.1", "merchant",) } - } - delay(100) - tx("merchant", "KUDOS:4.2", "customer") - } - - // Testing ranges. - repeat(30) { - tx("merchant", "KUDOS:0.001", "customer") - } - - // forward range: - client.getA("/accounts/merchant/transactions?delta=10&start=20") - .assertHistory(10) - - // backward range: - client.getA("/accounts/merchant/transactions?delta=-10&start=25") - .assertHistory(10) + ) + ) } // GET /transactions/T_ID @@ -1281,70 +1250,25 @@ class CoreBankCashoutApiTest { @Test fun history() = bankSetup { _ -> // TODO auth routine - - suspend fun HttpResponse.assertHistory(size: Int) { - assertHistoryIds<Cashouts>(size) { - it.cashouts.map { it.cashout_id } - } - } - - // Empty - client.getA("/accounts/customer/cashouts") - .assertNoContent() - - // Testing ranges. - repeat(30) { - cashout("KUDOS:0.${it+1}") - } - - // Default - client.getA("/accounts/customer/cashouts") - .assertHistory(20) - - // Forward range: - client.getA("/accounts/customer/cashouts?delta=10&start=20") - .assertHistory(10) - - // Fackward range: - client.getA("/accounts/customer/cashouts?delta=-10&start=25") - .assertHistory(10) + historyRoutine<Cashouts>( + url = "/accounts/customer/cashouts", + ids = { it.cashouts.map { it.cashout_id } }, + registered = listOf({ cashout("KUDOS:0.1") }), + polling = false + ) } // GET /cashouts @Test fun globalHistory() = bankSetup { _ -> // TODO admin auth routine - - suspend fun HttpResponse.assertHistory(size: Int) { - assertHistoryIds<GlobalCashouts>(size) { - it.cashouts.map { it.cashout_id } - } - } - - // Empty - client.get("/cashouts") { - pwAuth("admin") - }.assertNoContent() - - // Testing ranges. - repeat(30) { - cashout("KUDOS:0.${it+1}") - } - - // Default - client.get("/cashouts") { - pwAuth("admin") - }.assertHistory(20) - - // Forward range: - client.get("/cashouts?delta=10&start=20") { - pwAuth("admin") - }.assertHistory(10) - - // Fackward range: - client.get("/cashouts?delta=-10&start=25") { - pwAuth("admin") - }.assertHistory(10) + historyRoutine<GlobalCashouts>( + url = "/cashouts", + ids = { it.cashouts.map { it.cashout_id } }, + registered = listOf({ cashout("KUDOS:0.1") }), + polling = false, + auth = "admin" + ) } @Test diff --git a/bank/src/test/kotlin/RevenueApiTest.kt b/bank/src/test/kotlin/RevenueApiTest.kt @@ -33,113 +33,30 @@ class RevenueApiTest { @Test fun history() = bankSetup { setMaxDebt("exchange", TalerAmount("KUDOS:1000000")) - - suspend fun HttpResponse.assertHistory(size: Int) { - assertHistoryIds<MerchantIncomingHistory>(size) { - it.incoming_transactions.map { it.row_id } - } - } - - suspend fun latestId(): Long { - return client.getA("/accounts/merchant/taler-revenue/history?delta=-1") - .assertOkJson<MerchantIncomingHistory>().incoming_transactions[0].row_id - } - - suspend fun testTrigger(trigger: suspend () -> Unit) { - coroutineScope { - val id = latestId() - launch { - assertTime(100, 200) { - client.getA("/accounts/merchant/taler-revenue/history?delta=7&start=$id&long_poll_ms=1000") - .assertHistory(1) - } - } - delay(100) - trigger() - } - } - // TODO auth routine - - // Check error when no transactions - client.getA("/accounts/merchant/taler-revenue/history?delta=7") - .assertNoContent() - - // Gen three transactions using clean transfer logic - repeat(3) { - transfer("KUDOS:10") - } - // Should not show up in the revenue API history - tx("exchange", "KUDOS:10", "merchant", "bogus") - // Merchant pays customer once, but that should not appear in the result - addIncoming("KUDOS:10") - // Gen two transactions using raw bank transaction logic - repeat(2) { - tx("exchange", "KUDOS:10", "merchant", OutgoingTxMetadata(randShortHashCode(), ExchangeUrl("http://exchange.example.com/")).encode()) - } - - // Check ignore bogus subject - client.getA("/accounts/merchant/taler-revenue/history?delta=7") - .assertHistory(5) - - // Check skip bogus subject - client.getA("/accounts/merchant/taler-revenue/history?delta=5") - .assertHistory(5) - - // Check no useless polling - assertTime(0, 100) { - client.getA("/accounts/merchant/taler-revenue/history?delta=-6&long_poll_ms=1000") - .assertHistory(5) - } - - // Check no polling when find transaction - assertTime(0, 100) { - client.getA("/accounts/merchant/taler-revenue/history?delta=6&long_poll_ms=1000") - .assertHistory(5) - } - - coroutineScope { - val id = latestId() - launch { // Check polling succeed forward - assertTime(100, 200) { - client.getA("/accounts/merchant/taler-revenue/history?delta=2&start=$id&long_poll_ms=1000") - .assertHistory(1) + historyRoutine<MerchantIncomingHistory>( + url = "/accounts/merchant/taler-revenue/history", + ids = { it.incoming_transactions.map { it.row_id } }, + registered = listOf( + { + // Transactions using clean add incoming logic + transfer("KUDOS:10") + }, + { + // Transactions using raw bank transaction logic + tx("exchange", "KUDOS:10", "merchant", OutgoingTxMetadata(randShortHashCode(), ExchangeUrl("http://exchange.example.com/")).encode()) } - } - launch { // Check polling timeout forward - assertTime(200, 300) { - client.getA("/accounts/merchant/taler-revenue/history?delta=1&start=${id+3}&long_poll_ms=200") - .assertNoContent() + ), + ignored = listOf( + { + // Ignore malformed incoming transaction + tx("merchant", "KUDOS:10", "exchange", "ignored") + }, + { + // Ignore malformed outgoing transaction + tx("exchange", "KUDOS:10", "merchant", "ignored") } - } - delay(100) - transfer("KUDOS:10") - } - - // Test trigger by raw transaction - testTrigger { - tx("exchange", "KUDOS:10", "merchant", OutgoingTxMetadata(randShortHashCode(), ExchangeUrl("http://exchange.example.com/")).encode()) - } - // Test trigger by outgoing - testTrigger { transfer("KUDOS:9") } - - // Testing ranges. - repeat(5) { - transfer("KUDOS:10") - } - - val id = latestId() - - // forward range: - client.getA("/accounts/merchant/taler-revenue/history?delta=10") - .assertHistory(10) - client.getA("/accounts/merchant/taler-revenue/history?delta=10&start=4") - .assertHistory(10) - - // backward range: - client.getA("/accounts/merchant/taler-revenue/history?delta=-10") - .assertHistory(10) - client.getA("/accounts/merchant/taler-revenue/history?delta=-10&start=${id-4}") - .assertHistory(10) + ) + ) } } \ No newline at end of file diff --git a/bank/src/test/kotlin/WireGatewayApiTest.kt b/bank/src/test/kotlin/WireGatewayApiTest.kt @@ -169,115 +169,35 @@ class WireGatewayApiTest { fun historyIncoming() = bankSetup { // Give Foo reasonable debt allowance: setMaxDebt("merchant", TalerAmount("KUDOS:1000")) - - suspend fun HttpResponse.assertHistory(size: Int) { - assertHistoryIds<IncomingHistory>(size) { - it.incoming_transactions.map { it.row_id } - } - } - - suspend fun latestId(): Long { - return client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?delta=-1") - .assertOkJson<IncomingHistory>().incoming_transactions[0].row_id - } - - suspend fun testTrigger(trigger: suspend () -> Unit) { - coroutineScope { - val id = latestId() - launch { - assertTime(100, 200) { - client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?delta=7&start=$id&long_poll_ms=1000") - .assertHistory(1) - } - } - delay(100) - trigger() - } - } - authRoutine("/accounts/merchant/taler-wire-gateway/history/incoming?delta=7", method = HttpMethod.Get) - - // Check error when no transactions - client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?delta=7") - .assertNoContent() - - // Gen three transactions using clean add incoming logic - repeat(3) { - addIncoming("KUDOS:10") - } - // Should not show up in the taler wire gateway API history - tx("merchant", "KUDOS:10", "exchange", "bogus") - // Exchange pays merchant once, but that should not appear in the result - tx("exchange", "KUDOS:10", "merchant", "ignored") - // Gen one transaction using raw bank transaction logic - tx("merchant", "KUDOS:10", "exchange", IncomingTxMetadata(randShortHashCode()).encode()) - // Gen one transaction using withdraw logic - withdrawal("KUDOS:9") - - // Check ignore bogus subject - client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?delta=7") - .assertHistory(5) - - // Check skip bogus subject - client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?delta=5") - .assertHistory(5) - - // Check no useless polling - assertTime(0, 100) { - client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?delta=-6&long_poll_ms=1000") - .assertHistory(5) - } - - // Check no polling when find transaction - assertTime(0, 100) { - client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?delta=6&long_poll_ms=1000") - .assertHistory(5) - } - - coroutineScope { - val id = latestId() - launch { // Check polling succeed - assertTime(100, 200) { - client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?delta=2&start=$id&long_poll_ms=1000") - .assertHistory(1) + historyRoutine<IncomingHistory>( + url = "/accounts/exchange/taler-wire-gateway/history/incoming", + ids = { it.incoming_transactions.map { it.row_id } }, + registered = listOf( + { + // Transactions using clean add incoming logic + addIncoming("KUDOS:10") + }, + { + // Transactions using raw bank transaction logic + tx("merchant", "KUDOS:10", "exchange", IncomingTxMetadata(randShortHashCode()).encode()) + }, + { + // Transaction using withdraw logic + withdrawal("KUDOS:9") } - } - launch { // Check polling timeout - assertTime(200, 300) { - client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?delta=1&start=${id+2}&long_poll_ms=200") - .assertNoContent() + ), + ignored = listOf( + { + // Ignore malformed incoming transaction + tx("merchant", "KUDOS:10", "exchange", "ignored") + }, + { + // Ignore malformed outgoing transaction + tx("exchange", "KUDOS:10", "merchant", "ignored") } - } - delay(100) - addIncoming("KUDOS:10") - } - - // Test trigger by raw transaction - testTrigger { - tx("merchant", "KUDOS:10", "exchange", IncomingTxMetadata(randShortHashCode()).encode()) - } - // Test trigger by withdraw operation - testTrigger { withdrawal("KUDOS:9") } - // Test trigger by incoming - testTrigger { addIncoming("KUDOS:9") } - - // Testing ranges. - repeat(5) { - addIncoming("KUDOS:10") - } - val id = latestId() - - // forward range: - client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?delta=10") - .assertHistory(10) - client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?delta=10&start=4") - .assertHistory(10) - - // backward range: - client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?delta=-10") - .assertHistory(10) - client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?delta=-10&start=${id-4}") - .assertHistory(10) + ) + ) } @@ -287,113 +207,31 @@ class WireGatewayApiTest { @Test fun historyOutgoing() = bankSetup { setMaxDebt("exchange", TalerAmount("KUDOS:1000000")) - - suspend fun HttpResponse.assertHistory(size: Int) { - assertHistoryIds<OutgoingHistory>(size) { - it.outgoing_transactions.map { it.row_id } - } - } - - suspend fun latestId(): Long { - return client.getA("/accounts/exchange/taler-wire-gateway/history/outgoing?delta=-1") - .assertOkJson<OutgoingHistory>().outgoing_transactions[0].row_id - } - - suspend fun testTrigger(trigger: suspend () -> Unit) { - coroutineScope { - val id = latestId() - launch { - assertTime(100, 200) { - client.getA("/accounts/exchange/taler-wire-gateway/history/outgoing?delta=7&start=$id&long_poll_ms=1000") - .assertHistory(1) - } - } - delay(100) - trigger() - } - } - authRoutine("/accounts/merchant/taler-wire-gateway/history/outgoing?delta=7", method = HttpMethod.Get) - - // Check error when no transactions - client.getA("/accounts/exchange/taler-wire-gateway/history/outgoing?delta=7") - .assertNoContent() - - // Gen three transactions using clean transfer logic - repeat(3) { - transfer("KUDOS:10") - } - // Should not show up in the taler wire gateway API history - tx("exchange", "KUDOS:10", "merchant", "bogus") - // Merchant pays exchange once, but that should not appear in the result - tx("merchant", "KUDOS:10", "exchange", "ignored") - // Gen two transactions using raw bank transaction logic - repeat(2) { - tx("exchange", "KUDOS:10", "merchant", OutgoingTxMetadata(randShortHashCode(), ExchangeUrl("http://exchange.example.com/")).encode()) - } - - // Check ignore bogus subject - client.getA("/accounts/exchange/taler-wire-gateway/history/outgoing?delta=7") - .assertHistory(5) - - // Check skip bogus subject - client.getA("/accounts/exchange/taler-wire-gateway/history/outgoing?delta=5") - .assertHistory(5) - - // Check no useless polling - assertTime(0, 100) { - client.getA("/accounts/exchange/taler-wire-gateway/history/outgoing?delta=-6&long_poll_ms=1000") - .assertHistory(5) - } - - // Check no polling when find transaction - assertTime(0, 100) { - client.getA("/accounts/exchange/taler-wire-gateway/history/outgoing?delta=6&long_poll_ms=1000") - .assertHistory(5) - } - - coroutineScope { - val id = latestId() - launch { // Check polling succeed forward - assertTime(100, 200) { - client.getA("/accounts/exchange/taler-wire-gateway/history/outgoing?delta=2&start=$id&long_poll_ms=1000") - .assertHistory(1) + historyRoutine<OutgoingHistory>( + url = "/accounts/exchange/taler-wire-gateway/history/outgoing", + ids = { it.outgoing_transactions.map { it.row_id } }, + registered = listOf( + { + // Transactions using clean add incoming logic + transfer("KUDOS:10") + }, + { + // Transactions using raw bank transaction logic + tx("exchange", "KUDOS:10", "merchant", OutgoingTxMetadata(randShortHashCode(), ExchangeUrl("http://exchange.example.com/")).encode()) } - } - launch { // Check polling timeout forward - assertTime(200, 300) { - client.getA("/accounts/exchange/taler-wire-gateway/history/outgoing?delta=1&start=${id+2}&long_poll_ms=200") - .assertNoContent() + ), + ignored = listOf( + { + // Ignore malformed incoming transaction + tx("merchant", "KUDOS:10", "exchange", "ignored") + }, + { + // Ignore malformed outgoing transaction + tx("exchange", "KUDOS:10", "merchant", "ignored") } - } - delay(100) - transfer("KUDOS:10") - } - - // Test trigger by raw transaction - testTrigger { - tx("exchange", "KUDOS:10", "merchant", OutgoingTxMetadata(randShortHashCode(), ExchangeUrl("http://exchange.example.com/")).encode()) - } - // Test trigger by outgoing - testTrigger { transfer("KUDOS:9") } - - // Testing ranges - repeat(5) { - transfer("KUDOS:10") - } - val id = latestId() - - // forward range: - client.getA("/accounts/exchange/taler-wire-gateway/history/outgoing?delta=10") - .assertHistory(10) - client.getA("/accounts/exchange/taler-wire-gateway/history/outgoing?delta=10&start=4") - .assertHistory(10) - - // backward range: - client.getA("/accounts/exchange/taler-wire-gateway/history/outgoing?delta=-10") - .assertHistory(10) - client.getA("/accounts/exchange/taler-wire-gateway/history/outgoing?delta=-10&start=${id-4}") - .assertHistory(10) + ) + ) } // Testing the /admin/add-incoming call from the TWG API. diff --git a/bank/src/test/kotlin/routines.kt b/bank/src/test/kotlin/routines.kt @@ -0,0 +1,156 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2023 Taler Systems S.A. + + * LibEuFin is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation; either version 3, or + * (at your option) any later version. + + * LibEuFin is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General + * Public License for more details. + + * You should have received a copy of the GNU Affero General Public + * License along with LibEuFin; see the file COPYING. If not, see + * <http://www.gnu.org/licenses/> + */ + +import tech.libeufin.bank.* +import io.ktor.client.statement.HttpResponse +import io.ktor.server.testing.ApplicationTestBuilder +import io.ktor.client.request.* +import kotlinx.coroutines.* + +inline suspend fun <reified B> ApplicationTestBuilder.historyRoutine( + url: String, + crossinline ids: (B) -> List<Long>, + registered: List<suspend () -> Unit>, + ignored: List<suspend () -> Unit> = listOf(), + polling: Boolean = true, + auth: String? = null +) { + // Get history + val history: suspend (String) -> HttpResponse = { params: String -> + client.get("$url?$params") { + pwAuth(auth) + } + } + // Check history is following specs + val assertHistory: suspend HttpResponse.(Int) -> Unit = { size: Int -> + assertHistoryIds<B>(size, ids) + } + // Get latest registered id + val latestId: suspend () -> Long = { + history("delta=-1").assertOkJson<B>().run { ids(this)[0] } + } + + // Check error when no transactions + history("delta=7").assertNoContent() + + // Run interleaved registered and ignore transactions + val registered_iter = registered.iterator() + val ignored_iter = ignored.iterator() + while (registered_iter.hasNext() || ignored_iter.hasNext()) { + if (registered_iter.hasNext()) registered_iter.next()() + if (ignored_iter.hasNext()) ignored_iter.next()() + } + + + val nbRegistered = registered.size + val nbIgnored = ignored.size + val nbTotal = nbRegistered + nbIgnored + + println(nbRegistered) + + println("simple") + + // Check ignored + history("delta=$nbTotal").assertHistory(nbRegistered) + // Check skip ignored + history("delta=$nbRegistered").assertHistory(nbRegistered) + + if (polling) { + // Check no polling when we cannot have more transactions + assertTime(0, 100) { + history("delta=-${nbRegistered+1}&long_poll_ms=1000") + .assertHistory(nbRegistered) + } + // Check no polling when already find transactions even if less than delta + assertTime(0, 100) { + history("delta=${nbRegistered+1}&long_poll_ms=1000") + .assertHistory(nbRegistered) + } + + println("polling") + + // Check polling + coroutineScope { + val id = latestId() + launch { // Check polling succeed + assertTime(100, 200) { + history("delta=2&start=$id&long_poll_ms=1000") + .assertHistory(1) + } + } + launch { // Check polling timeout + assertTime(200, 300) { + history("delta=1&start=${id+10}&long_poll_ms=200") + .assertNoContent() + } + } + delay(100) + println(registered.size) + registered[0]() + } + + println("triggers") + + // Test triggers + for (register in registered) { + coroutineScope { + val id = latestId() + launch { + assertTime(100, 200) { + history("delta=7&start=$id&long_poll_ms=1000") + .assertHistory(1) + } + } + delay(100) + register() + } + } + + // Test doesn't trigger + coroutineScope { + val id = latestId() + launch { + assertTime(200, 300) { + history("delta=7&start=$id&long_poll_ms=200") + .assertNoContent() + } + } + delay(100) + for (ignore in ignored) { + ignore() + } + } + } + + println("range") + + // Testing ranges. + repeat(20) { + registered[0]() + } + val id = latestId() + // Default + history("").assertHistory(20) + // forward range: + history("delta=10").assertHistory(10) + history("delta=10&start=4").assertHistory(10) + // backward range: + history("delta=-10").assertHistory(10) + history("delta=-10&start=${id-4}").assertHistory(10) +} +\ No newline at end of file