libeufin

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

commit 26d9e2a804f50d27d3e610b40fa77b0729a442e0
parent 384c6cc1301991e57151ff85fe08c594c660609a
Author: MS <ms@taler.net>
Date:   Mon, 22 May 2023 16:41:53 +0200

Conversion service tests.

Covering all the HTTP response paths.

Diffstat:
Mnexus/src/test/kotlin/ConversionServiceTest.kt | 204+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
1 file changed, 128 insertions(+), 76 deletions(-)

diff --git a/nexus/src/test/kotlin/ConversionServiceTest.kt b/nexus/src/test/kotlin/ConversionServiceTest.kt @@ -1,35 +1,47 @@ -import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import io.ktor.client.* +import io.ktor.client.engine.cio.* import io.ktor.client.engine.mock.* -import io.ktor.client.plugins.* import io.ktor.client.request.* -import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.server.testing.* import kotlinx.coroutines.* import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.transactions.transaction -import org.junit.Ignore import org.junit.Test -import tech.libeufin.nexus.bankaccount.getBankAccount import tech.libeufin.nexus.server.nexusApp import tech.libeufin.sandbox.* -import tech.libeufin.util.internalServerError import tech.libeufin.util.parseAmount class ConversionServiceTest { + private fun CoroutineScope.launchBuyinMonitor(httpClient: HttpClient): Job { + val job = launch { + /** + * The runInterruptible wrapper lets code without suspension + * points be cancel()'d. Without it, such code would ignore + * any call to cancel() and the test never return. + */ + runInterruptible { + buyinMonitor( + demobankName = "default", + accountToCredit = "exchange-0", + client = httpClient + ) + } + } + return job + } /** - * Testing the buy-in monitor in the normal case: Nexus - * communicates a new incoming fiat transaction and the - * monitor wires funds to the exchange. + * Testing the buy-in monitor in all the HTTP scenarios, + * successful case, client's and server's error cases. */ @Test fun buyinTest() { - // First create an incoming fiat payment _at Nexus_. - // This payment is addressed to the Nexus user whose - // (Nexus) credentials will be used by Sandbox to fetch - // new incoming fiat payments. + // 1, testing the successful case. + /* First create an incoming fiat payment _at Nexus_. + This payment is addressed to the Nexus user whose + (Nexus) credentials will be used by Sandbox to fetch + new incoming fiat payments. */ withTestDatabase { prepSandboxDb(currency = "REGIO") prepNexusDb() @@ -48,23 +60,13 @@ class ConversionServiceTest { ) // Start Nexus, to let it serve the fiat transaction. testApplication { + val client = this.createClient { + followRedirects = false + } application(nexusApp) // Start the buy-in monitor to let it download the fiat transaction. runBlocking { - val job = launch { - /** - * The runInterruptible wrapper lets code without suspension - * points be cancel()'d. Without it, such code would ignore - * any call to cancel() and the test never return. - */ - runInterruptible { - buyinMonitor( - demobankName = "default", - accountToCredit = "exchange-0", - client = client - ) - } - } + val job = launchBuyinMonitor(client) delay(1000L) // Lets the DB persist. job.cancelAndJoin() } @@ -93,6 +95,48 @@ class ConversionServiceTest { // and the regional currency. assert(boughtIn.subject == reservePub && boughtIn.currency == "REGIO") } + // 2, testing the client side error case. + assertException<BuyinClientError>( + { + runBlocking { + /** + * As soon as the buy-in monitor requests again the history + * to Nexus, it'll get 400 from the mock client. + */ + launchBuyinMonitor(getMockedClient { respondBadRequest() }) + } + } + ) + /** + * 3, testing the server side error case. Here the monitor should + * NOT throw any error and instead keep operating normally. This allows + * Sandbox to tolerate server errors and retry the requests. + */ + runBlocking { + /** + * As soon as the buy-in monitor requests again the history + * to Nexus, it'll get 500 from the mock client. + */ + val job = launchBuyinMonitor(getMockedClient { respondError(HttpStatusCode.InternalServerError) }) + delay(1000L) + // Getting here means no exceptions. Can now cancel the service. + job.cancelAndJoin() + } + /** + * 4, testing the unhandled error case. This case is treated + * as a client error, to signal the calling logic to intervene. + */ + assertException<BuyinClientError>( + { + runBlocking { + /** + * As soon as the buy-in monitor requests again the history + * to Nexus, it'll get 307 from the mock client. + */ + launchBuyinMonitor(getMockedClient { respondRedirect() }) + } + } + ) } } private fun CoroutineScope.launchCashoutMonitor(httpClient: HttpClient): Job { @@ -119,7 +163,7 @@ class ConversionServiceTest { } // This function mocks a 500 response to a cash-out request. - private fun MockRequestHandleScope.mock500Response(request: HttpRequestData): HttpResponseData { + private fun MockRequestHandleScope.mock500Response(): HttpResponseData { return respondError(HttpStatusCode.InternalServerError) } // This function implements a mock server that checks the currency in the cash-out request. @@ -137,8 +181,10 @@ class ConversionServiceTest { } } - private fun getMockClient(handler: MockRequestHandleScope.(HttpRequestData) -> HttpResponseData): HttpClient { + // Abstracts the mock handler installation. + private fun getMockedClient(handler: MockRequestHandleScope.(HttpRequestData) -> HttpResponseData): HttpClient { return HttpClient(MockEngine) { + followRedirects = false engine { addHandler { request -> handler(request) @@ -146,7 +192,6 @@ class ConversionServiceTest { } } } - /** * Checks that the cash-out monitor reacts after * a CRDT transaction arrives at the designated account. @@ -160,9 +205,13 @@ class ConversionServiceTest { ) prepNexusDb() testApplication { + val client = this.createClient { + followRedirects = false + } application(nexusApp) // Mock server to intercept and inspect the cash-out request. val checkCurrencyClient = HttpClient(MockEngine) { + followRedirects = false engine { addHandler { request -> inspectCashoutCurrency(request) @@ -202,9 +251,10 @@ class ConversionServiceTest { */ job.cancelAndJoin() val error500Client = HttpClient(MockEngine) { + followRedirects = false engine { addHandler { - request -> mock500Response(request) + request -> mock500Response() } } } @@ -224,7 +274,7 @@ class ConversionServiceTest { assert(bankaccount.lastFiatSubmission?.id?.value == 1L) } /* Removing now the mocked 500 response and checking that - * indeed the cash-out does get sent. */ + * the problematic cash-out get then sent. */ job = launchCashoutMonitor(client) // Should find the non cashed-out wire transfer and react. delay(1000L) // Lets the reaction complete. job.cancelAndJoin() @@ -234,61 +284,63 @@ class ConversionServiceTest { assert(bankaccount.lastFiatSubmission?.subject == "fiat #1") } /** - * 3, the client error case, where the conversion service is - * supposed to exit the whole process. + * 3, testing the client error case, where + * the conversion service is supposed to throw exception. */ - job = launchCashoutMonitor( - getMockClient { - /** - * This causes the cash-out request sent to Nexus to - * respond with 400. - */ - respondBadRequest() - } - ) // Should find the non cashed-out wire transfer and react. - // Triggering now a cash-out operation via a new wire transfer to admin. - wireTransfer( - debitAccount = "foo", - creditAccount = "admin", - subject = "fiat #2", - amount = "REGIO:22" - ) - delay(1000L) // Lets the reaction complete. - job.cancelAndJoin() - // Checking that the cash-out counter did NOT update. - transaction { - val bankaccount = getBankAccountFromLabel("admin") - // Checks that the once failing cash-out did go through. - assert(bankaccount.lastFiatSubmission?.subject == "fiat #1") - } + assertException<CashoutClientError>({ + runBlocking { + launchCashoutMonitor( + httpClient = getMockedClient { + tech.libeufin.sandbox.logger.debug("MOCK 400") + /** + * This causes the cash-out request sent to Nexus to + * respond with 400. + */ + respondBadRequest() + } + ) + // Triggering now a cash-out operation via a new wire transfer to admin. + wireTransfer( + debitAccount = "foo", + creditAccount = "admin", + subject = "fiat #2", + amount = "REGIO:22" + ) + }}) /** * 4, checking a redirect response. Because this is an unhandled * error case, it is treated as a client error. No need to wire a - * new cash-out to trigger a cash-out request, since the last failing + * new cash-out to trigger a cash-out request, since the last failed * one will be retried. */ - job = launchCashoutMonitor( - getMockClient { - /** - * This causes the cash-out request sent to Nexus to - * respond with 307 Temporary Redirect. - */ - respondRedirect() + assertException<CashoutClientError>({ + runBlocking { + launchCashoutMonitor( + getMockedClient { + /** + * This causes the cash-out request sent to Nexus to + * respond with 307 Temporary Redirect. + */ + respondRedirect() + } + ) } - ) - assert(job.isActive) - delay(1000L) // Lets the reaction complete. - // Checking that the service stopped because of the client-side error. - assert(!job.isActive) - // 5, Mocking a network error. The previous failed cash-out - // will again trigger the service to POST at Nexus. + }) + /* 5, Mocking a network error. The previous failed cash-out + will again trigger the service to POST to Nexus. Here the + monitor tolerates the failure, as it's not due to its state + and should be temporary. + */ + var requestMade = false job = launchCashoutMonitor( - getMockClient { + getMockedClient { + requestMade = true throw Exception("Network Issue.") } ) - delay(1000L) // Lets the reaction complete. - assert(job.isActive) // asserting that the service is still running. + delay(2000L) // Lets the reaction complete. + // asserting that the service is still running after the failed request. + assert(requestMade && job.isActive) job.cancelAndJoin() } }