libeufin

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

commit 3c295e0d1d6bbee577caf2b4d9ce4e71a03fc82c
parent cb276fc4aeb2d18f7d8a5023491edb410acb4f80
Author: MS <ms@taler.net>
Date:   Wed, 22 Jul 2020 21:00:29 +0200

fix pain parsing

Diffstat:
Aintegration-tests/test-ebics-double-payment-submission.py | 221+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mintegration-tests/util.py | 4++--
Msandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt | 35+++++++++++++++++++----------------
Mutil/src/main/kotlin/Ebics.kt | 2+-
4 files changed, 243 insertions(+), 19 deletions(-)

diff --git a/integration-tests/test-ebics-double-payment-submission.py b/integration-tests/test-ebics-double-payment-submission.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 + +from subprocess import check_call +from requests import post, get +from time import sleep +import os +import hashlib +import base64 + +from util import startNexus, startSandbox + +# Steps implemented in this test. +# +# 0 Prepare sandbox. +# -> (a) Make a EBICS host, (b) make a EBICS subscriber +# for the test runner, and (c) assign a IBAN to such +# subscriber. +# +# 1 Prepare nexus. +# -> (a) Make a Nexus user, (b) make a EBICS subscriber +# associated to that user +# +# 2 Prepare the Ebics bank connection for the nexus user. +# -> (a) Upload keys from Nexus to the Bank (INI & HIA), +# (b) Download key from the Bank (HPB) to the Nexus, +# and (c) Fetch the bank account owned by that subscriber +# at the bank. + +# 3 Request history from the Nexus to the Bank (C53). +# 4 Verify that history is empty. +# 5 Issue a payment from Nexus +# -> (a) Prepare & (b) trigger CCT. +# 6 Request history after submitting the payment, +# from Nexus to Bank. +# 7 Verify that previous payment shows up. + +# Nexus user details +USERNAME = "person" +PASSWORD = "y" +USER_AUTHORIZATION_HEADER = "basic {}".format( + base64.b64encode(b"person:y").decode("utf-8") +) + +# Admin authentication +ADMIN_AUTHORIZATION_HEADER = "basic {}".format( + base64.b64encode(b"admin:x").decode("utf-8") +) + +# EBICS details +EBICS_URL = "http://localhost:5000/ebicsweb" +HOST_ID = "HOST01" +PARTNER_ID = "PARTNER1" +USER_ID = "USER1" +EBICS_VERSION = "H004" + +# Subscriber's bank account +SUBSCRIBER_IBAN = "GB33BUKB20201555555555" +SUBSCRIBER_BIC = "BUKBGB22" +SUBSCRIBER_NAME = "Oliver Smith" +BANK_ACCOUNT_LABEL = "savings" + +# Databases +NEXUS_DB = "test-nexus.sqlite3" + + +def fail(msg): + print(msg) + exit(1) + + +def assertResponse(response): + if response.status_code != 200: + print("Test failed on URL: {}".format(response.url)) + # stdout/stderr from both services is A LOT of text. + # Confusing to dump all that to console. + print("Check nexus.log and sandbox.log, probably under /tmp") + exit(1) + # Allows for finer grained checks. + return response + + +startNexus(NEXUS_DB) +startSandbox() + +# 0.a +assertResponse( + post( + "http://localhost:5000/admin/ebics/host", + json=dict(hostID=HOST_ID, ebicsVersion=EBICS_VERSION), + ) +) + +# 0.b +assertResponse( + post( + "http://localhost:5000/admin/ebics/subscribers", + json=dict(hostID=HOST_ID, partnerID=PARTNER_ID, userID=USER_ID), + ) +) + +# 0.c +assertResponse( + post( + "http://localhost:5000/admin/ebics/bank-accounts", + json=dict( + subscriber=dict(hostID=HOST_ID, partnerID=PARTNER_ID, userID=USER_ID), + iban=SUBSCRIBER_IBAN, + bic=SUBSCRIBER_BIC, + name=SUBSCRIBER_NAME, + label=BANK_ACCOUNT_LABEL, + ), + ) +) + +# 1.a, make a new nexus user. +assertResponse( + post( + "http://localhost:5001/users", + headers=dict(Authorization=ADMIN_AUTHORIZATION_HEADER), + json=dict(username=USERNAME, password=PASSWORD), + ) +) + +print("creating bank connection") + +# 1.b, make a ebics bank connection for the new user. +assertResponse( + post( + "http://localhost:5001/bank-connections", + json=dict( + name="my-ebics", + source="new", + type="ebics", + data=dict( + ebicsURL=EBICS_URL, hostID=HOST_ID, partnerID=PARTNER_ID, userID=USER_ID + ), + ), + headers=dict(Authorization=USER_AUTHORIZATION_HEADER), + ) +) + +print("connecting") + +assertResponse( + post( + "http://localhost:5001/bank-connections/my-ebics/connect", + json=dict(), + headers=dict(Authorization=USER_AUTHORIZATION_HEADER), + ) +) + + +# 2.c, fetch bank account information +assertResponse( + post( + "http://localhost:5001/bank-connections/my-ebics/ebics/import-accounts", + json=dict(), + headers=dict(Authorization=USER_AUTHORIZATION_HEADER), + ) +) + +# 3, ask nexus to download history +assertResponse( + post( + f"http://localhost:5001/bank-accounts/{BANK_ACCOUNT_LABEL}/fetch-transactions", + headers=dict(Authorization=USER_AUTHORIZATION_HEADER), + ) +) + +# 4, make sure history is empty +resp = assertResponse( + get( + f"http://localhost:5001/bank-accounts/{BANK_ACCOUNT_LABEL}/transactions", + headers=dict(Authorization=USER_AUTHORIZATION_HEADER), + ) +) +if len(resp.json().get("transactions")) != 0: + fail("unexpected number of transactions") + +# 5.a, prepare a payment +resp = assertResponse( + post( + "http://localhost:5001/bank-accounts/{}/payment-initiations".format( + BANK_ACCOUNT_LABEL + ), + json=dict( + iban="FR7630006000011234567890189", + bic="AGRIFRPP", + name="Jacques La Fayette", + subject="integration test", + amount="EUR:1", + ), + headers=dict(Authorization=USER_AUTHORIZATION_HEADER), + ) +) +PREPARED_PAYMENT_UUID = resp.json().get("uuid") +if PREPARED_PAYMENT_UUID == None: + fail("Payment UUID not received") + +# 5.b, submit payment initiation +assertResponse( + post( + f"http://localhost:5001/bank-accounts/{BANK_ACCOUNT_LABEL}/payment-initiations/{PREPARED_PAYMENT_UUID}/submit", + json=dict(), + headers=dict(Authorization=USER_AUTHORIZATION_HEADER), + ) +) + +# hack the database +check_call(["sqlite3", NEXUS_DB, f"UPDATE PaymentInitiations SET submitted = false WHERE id = '{PREPARED_PAYMENT_UUID}'"]) + +# 5.b, submit payment initiation AGAIN +assertResponse( + post( + f"http://localhost:5001/bank-accounts/{BANK_ACCOUNT_LABEL}/payment-initiations/{PREPARED_PAYMENT_UUID}/submit", + json=dict(), + headers=dict(Authorization=USER_AUTHORIZATION_HEADER), + ) +) + +print("Test passed!") diff --git a/integration-tests/util.py b/integration-tests/util.py @@ -84,11 +84,11 @@ def startNexus(dbname="nexus-test.sqlite3"): stderr=open("nexus-stderr.log", "w"), ) atexit.register(lambda: kill("nexus", nexus)) - for i in range(10): + for i in range(80): try: get("http://localhost:5001/") except: - if i == 9: + if i == 79: nexus.terminate() print("Nexus timed out") exit(77) diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt @@ -509,41 +509,44 @@ private fun parsePain001(paymentRequest: String, initiatorName: String): PainPar return destructXml(painDoc) { requireRootElement("Document") { requireUniqueChildNamed("CstmrCdtTrfInitn") { - val msgId = requireOnlyChild { + val msgId = requireUniqueChildNamed("GrpHdr") { requireUniqueChildNamed("MsgId") { focusElement.textContent } } requireUniqueChildNamed("PmtInf") { val pmtInfId = requireUniqueChildNamed("PmtInfId") { focusElement.textContent } val creditorIban = requireUniqueChildNamed("CdtTrfTxInf") { requireUniqueChildNamed("CdtrAcct") { - requireUniqueChildNamed("id") { + requireUniqueChildNamed("Id") { requireUniqueChildNamed("IBAN") { focusElement.textContent } } } } - val creditorName = requireUniqueChildNamed("Cdt") { - requireUniqueChildNamed("Nm") { focusElement.textContent } + val txInf = requireUniqueChildNamed("CdtTrfTxInf") { + val amt = requireUniqueChildNamed("Amt") { + requireOnlyChild { + focusElement + } + } + val creditorName = requireUniqueChildNamed("Cdtr") { + requireUniqueChildNamed("Nm") { focusElement.textContent } + } + val subject = requireUniqueChildNamed("RmtInf") { + requireUniqueChildNamed("Ustrd") { focusElement.textContent } + } + object {val amt = amt; val subject = subject; val creditorName = creditorName} } val debitorIban = requireUniqueChildNamed("DbtrAcct") { requireOnlyChild { requireOnlyChild { focusElement.textContent } } } - val subject = requireUniqueChildNamed("RmtInf") { - requireUniqueChildNamed("Ustrd") { focusElement.textContent } - } - val amt = requireUniqueChildNamed("Amt") { - requireOnlyChild { - focusElement - } - } PainParseResult( - currency = amt.getAttribute("Ccy"), - amount = Amount(amt.textContent), - subject = subject, + currency = txInf.amt.getAttribute("Ccy"), + amount = Amount(txInf.amt.textContent), + subject = txInf.subject, debitorIban = debitorIban, debitorName = initiatorName, - creditorName = creditorName, + creditorName = txInf.creditorName, creditorIban = creditorIban, pmtInfId = pmtInfId, msgId = msgId diff --git a/util/src/main/kotlin/Ebics.kt b/util/src/main/kotlin/Ebics.kt @@ -435,7 +435,7 @@ fun parseAndValidateEbicsResponse( } catch (e: Exception) { throw EbicsProtocolError( HttpStatusCode.InternalServerError, - "Invalid XML (as EbicsResponse) received from bank" + "Invalid XML (as EbicsResponse) received from bank: $responseStr" ) }