libeufin

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

commit e8a79ced5fb2bb303990070c1b1e0f0fda0da040
parent 96bbb55757be2dcc7b0c7341ffc52d14fdacec36
Author: MS <ms@taler.net>
Date:   Wed, 30 Nov 2022 22:33:54 +0100

Activate camt.052 and pain.001 integration tests.

Diffstat:
Mnexus/src/main/kotlin/tech/libeufin/nexus/Errors.kt | 7++++++-
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt | 11+++++++++--
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt | 5++++-
Mnexus/src/test/kotlin/DownloadAndSubmit.kt | 128++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msandbox/src/test/kotlin/BalanceTest.kt | 1-
Msandbox/src/test/kotlin/DBTest.kt | 1-
6 files changed, 140 insertions(+), 13 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Errors.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Errors.kt @@ -20,8 +20,13 @@ package tech.libeufin.nexus import io.ktor.http.HttpStatusCode +import tech.libeufin.util.LibeufinErrorCode -data class NexusError(val statusCode: HttpStatusCode, val reason: String) : +data class NexusError( + val statusCode: HttpStatusCode, + val reason: String, + val code: LibeufinErrorCode? = null + ) : Exception("$reason (HTTP status $statusCode)") fun NexusAssert(condition: Boolean, errorMsg: String): Boolean { diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt @@ -243,8 +243,15 @@ suspend fun doEbicsUploadTransaction( EbicsReturnCode.EBICS_OK -> { } else -> { - throw NexusError(HttpStatusCode.InternalServerError, - "Unexpected EBICS technical return code: ${txResp.technicalReturnCode}" + throw EbicsProtocolError( + /** + * 500 because Nexus walked until having the + * bank rejecting the operation instead of it + * detecting the problem. + */ + HttpStatusCode.InternalServerError, + "Unexpected EBICS technical return code: ${txResp.technicalReturnCode}", + txResp.technicalReturnCode ) } } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt @@ -542,7 +542,10 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol { // to lose the database transaction data. TransactionManager.current().commit() throw NexusError( - HttpStatusCode.InternalServerError, "Pain.001 message is invalid." + HttpStatusCode.InternalServerError, + "Attempted Pain.001 (${paymentInitiation.paymentInformationId})" + + " message is invalid. Not sent to the bank.", + LibeufinErrorCode.LIBEUFIN_EC_INVALID_STATE ) } object { diff --git a/nexus/src/test/kotlin/DownloadAndSubmit.kt b/nexus/src/test/kotlin/DownloadAndSubmit.kt @@ -1,3 +1,4 @@ +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import io.ktor.application.* import io.ktor.client.* import io.ktor.client.request.* @@ -15,7 +16,12 @@ import org.w3c.dom.Document import tech.libeufin.nexus.* import tech.libeufin.nexus.bankaccount.addPaymentInitiation import tech.libeufin.nexus.bankaccount.fetchBankAccountTransactions +import tech.libeufin.nexus.bankaccount.submitAllPaymentInitiations import tech.libeufin.nexus.ebics.EbicsBankConnectionProtocol +import tech.libeufin.nexus.ebics.doEbicsUploadTransaction +import tech.libeufin.nexus.ebics.getEbicsSubscriberDetails +import tech.libeufin.nexus.iso20022.NexusPaymentInitiationData +import tech.libeufin.nexus.iso20022.createPain001document import tech.libeufin.nexus.server.FetchLevel import tech.libeufin.nexus.server.FetchSpecAllJson import tech.libeufin.nexus.server.FetchSpecJson @@ -44,9 +50,9 @@ data class EbicsResponses( /** * Minimal server responding always the 'init' field of a EbicsResponses - * object along a download EBICS message. Suitable to set arbitrary data + * object to a download EBICS message. Suitable to set arbitrary data * in said response. Signs the response assuming the client is the one - * created a MakeEnv.kt. + * created in MakeEnv.kt. */ fun getCustomEbicsServer(r: EbicsResponses, endpoint: String = "/ebicsweb"): Application.() -> Unit { val ret: Application.() -> Unit = { @@ -91,10 +97,9 @@ fun getCustomEbicsServer(r: EbicsResponses, endpoint: String = "/ebicsweb"): App * and having had access to runTask and TaskSchedule, that * are now 'private'. */ -// @Ignore class DownloadAndSubmit { /** - * Instruct the server to return invalid CAMT content. + * Download a C52 report from the bank. */ @Test fun download() { @@ -104,7 +109,7 @@ class DownloadAndSubmit { "foo", "default", "Show up in logging!", - "TESTKUDOS:5" + "TESTKUDOS:1" ) wireTransfer( "bank", @@ -114,7 +119,6 @@ class DownloadAndSubmit { "TESTKUDOS:5" ) withTestApplication(sandboxApp) { - val conn = EbicsBankConnectionProtocol() runBlocking { fetchBankAccountTransactions( client, @@ -125,10 +129,19 @@ class DownloadAndSubmit { "foo" ) } + transaction { + // FIXME: assert on the subject. + assert( + NexusBankTransactionEntity[1].amount == "1" && + NexusBankTransactionEntity[2].amount == "5" + ) + } } } } - + /** + * Upload one payment instruction to the bank. + */ @Test fun upload() { withNexusAndSandboxUser { @@ -156,6 +169,107 @@ class DownloadAndSubmit { 1L ) } + transaction { + val payment = BankAccountTransactionEntity[1] + assert(payment.debtorIban == FOO_USER_IBAN && + payment.subject == "test payment" && + payment.direction == "DBIT" + ) + } + } + } + } + + /** + * Upload one payment instruction charging one IBAN + * that does not belong to the requesting EBICS subscriber. + */ + @Test + fun unallowedDebtorIban() { + withNexusAndSandboxUser { + withTestApplication(sandboxApp) { + runBlocking { + val bar = transaction { NexusBankAccountEntity.findByName("bar") } + val painMessage = createPain001document( + NexusPaymentInitiationData( + debtorIban = bar!!.iban, + debtorBic = bar!!.bankCode, + debtorName = bar!!.accountHolder, + currency = "TESTKUDOS", + amount = "1", + creditorIban = getIban(), + creditorName = "Get", + creditorBic = "SANDBOXX", + paymentInformationId = "entropy-0", + preparationTimestamp = 1970L, + subject = "Unallowed", + messageId = "entropy-1", + endToEndId = null, + instructionId = null + ) + ) + val unallowedSubscriber = transaction { getEbicsSubscriberDetails("foo") } + var thrown = false + try { + doEbicsUploadTransaction( + client, + unallowedSubscriber, + "CCT", + painMessage.toByteArray(Charsets.UTF_8), + EbicsStandardOrderParams() + ) + } catch (e: EbicsProtocolError) { + if (e.ebicsTechnicalCode == + EbicsReturnCode.EBICS_ACCOUNT_AUTHORISATION_FAILED + ) + thrown = true + } + assert(thrown) + } + } + } + } + + /** + * Submit one payment instruction with a invalid Pain.001 + * document, and check that it was marked as invalid. Hence, + * the error is expected only by the first submission, since + * the second won't pick the invalid payment. + */ + @Test + fun invalidPain001() { + withNexusAndSandboxUser { + withTestApplication(sandboxApp) { + val conn = EbicsBankConnectionProtocol() + runBlocking { + // Create Pain.001 to be submitted. + addPaymentInitiation( + Pain001Data( + creditorIban = getIban(), + creditorBic = "not-a-BIC", + creditorName = "Tester", + subject = "test payment", + sum = Amount(1), + currency = "TESTKUDOS" + ), + transaction { + NexusBankAccountEntity.findByName( + "foo" + ) ?: throw Exception("Test failed") + } + ) + // Encounters errors. + var thrown = false + try { + submitAllPaymentInitiations(client, "foo") + } catch (e: NexusError) { + assert((e.code == LibeufinErrorCode.LIBEUFIN_EC_INVALID_STATE)) + thrown = true + } + assert(thrown) + // No errors, since it should not retry. + submitAllPaymentInitiations(client, "foo") + } } } } diff --git a/sandbox/src/test/kotlin/BalanceTest.kt b/sandbox/src/test/kotlin/BalanceTest.kt @@ -25,7 +25,6 @@ class BalanceTest { allowRegistrations = true name = "default" withSignupBonus = false - uiTitle = "test" } val one = BankAccountEntity.new { iban = "IBAN 1" diff --git a/sandbox/src/test/kotlin/DBTest.kt b/sandbox/src/test/kotlin/DBTest.kt @@ -71,7 +71,6 @@ class DBTest { allowRegistrations = true name = "default" withSignupBonus = false - uiTitle = "test" } val bankAccount = BankAccountEntity.new { iban = "iban"