libeufin

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

commit c8bb455941b274526cf46b8f5a9e91b0bd1012fd
parent e7fc0fe36c2f019d93c34aa9dd5a54c81e42abb5
Author: Florian Dold <florian.dold@gmail.com>
Date:   Tue,  9 Jun 2020 02:56:15 +0530

support multiple EBICS download segments

Diffstat:
Mintegration-tests/start-testenv.py | 8+++++---
Mintegration-tests/test-ebics.py | 9+++++++++
Mintegration-tests/util.py | 4++--
Mnexus/src/main/kotlin/tech/libeufin/nexus/EbicsClient.kt | 38++++++++++++++++++++++++++++++++++++++
Mutil/src/main/kotlin/Ebics.kt | 36++++++++++++++++++++++++++++++++----
Mutil/src/main/kotlin/ebics_h004/EbicsRequest.kt | 38++++++++++++++++++++++++++++++++++----
6 files changed, 120 insertions(+), 13 deletions(-)

diff --git a/integration-tests/start-testenv.py b/integration-tests/start-testenv.py @@ -53,8 +53,6 @@ def assertResponse(response): return response -os.chdir("..") - startNexus("nexus-testenv.sqlite3") startSandbox() @@ -204,4 +202,8 @@ if len(resp.json().get("transactions")) != 1: fail("Unexpected number of transactions; should be 1") -input("press enter to stop LibEuFin test environment ...") +try: + input("press enter to stop LibEuFin test environment ...") +except: + pass +print("exiting!") diff --git a/integration-tests/test-ebics.py b/integration-tests/test-ebics.py @@ -167,6 +167,15 @@ assertResponse( ) ) +# Test download transaction (TSD, LibEuFin-specific test order type) +assertResponse( + post( + "http://localhost:5001/bank-connections/my-ebics/ebics/download/tsd", + json=dict(), + headers=dict(Authorization=USER_AUTHORIZATION_HEADER), + ) +) + # 2.c, fetch bank account information assertResponse( post( diff --git a/integration-tests/util.py b/integration-tests/util.py @@ -18,7 +18,7 @@ def checkPort(port): exit(77) -def startSandbox(dbname): +def startSandbox(dbname="sandbox-test.sqlite3"): db_full_path = str(Path.cwd() / dbname) check_call(["rm", "-f", db_full_path]) check_call(["../gradlew", "-p", "..", "sandbox:assemble"]) @@ -43,7 +43,7 @@ def startSandbox(dbname): break -def startNexus(dbname): +def startNexus(dbname="nexus-test.sqlite3"): db_full_path = str(Path.cwd() / dbname) check_call(["rm", "-f", "--", db_full_path]) check_call( diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsClient.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsClient.kt @@ -95,6 +95,44 @@ suspend fun doEbicsDownloadTransaction( payloadChunks.add(initOrderDataEncChunk) + val numSegments = initResponse.numSegments + ?: throw NexusError(HttpStatusCode.FailedDependency, "missing segment number in EBICS download init response") + + // Transfer phase + + for (x in 2..numSegments) { + val transferReqStr = + createEbicsRequestForDownloadTransferPhase(subscriberDetails, transactionID, x, numSegments) + val transferResponseStr = client.postToBank(subscriberDetails.ebicsUrl, transferReqStr) + val transferResponse = parseAndValidateEbicsResponse(subscriberDetails, transferResponseStr) + when (transferResponse.technicalReturnCode) { + EbicsReturnCode.EBICS_OK -> { + // Success, nothing to do! + } + else -> { + throw NexusError( + HttpStatusCode.FailedDependency, + "unexpected technical return code ${transferResponse.technicalReturnCode}" + ) + } + } + when (transferResponse.bankReturnCode) { + EbicsReturnCode.EBICS_OK -> { + // Success, nothing to do! + } + else -> { + logger.warn("Bank return code was: ${transferResponse.bankReturnCode}") + return EbicsDownloadBankErrorResult(transferResponse.bankReturnCode) + } + } + val transferOrderDataEncChunk = transferResponse.orderDataEncChunk + ?: throw NexusError( + HttpStatusCode.InternalServerError, + "transfer response for download transaction does not contain data transfer" + ) + payloadChunks.add(transferOrderDataEncChunk) + } + val respPayload = decryptAndDecompressResponse(subscriberDetails, encryptionInfo, payloadChunks) // Acknowledgement phase diff --git a/util/src/main/kotlin/Ebics.kt b/util/src/main/kotlin/Ebics.kt @@ -260,6 +260,23 @@ fun createEbicsRequestForDownloadInitialization( return XMLUtil.convertDomToString(doc) } +fun createEbicsRequestForDownloadTransferPhase( + subscriberDetails: EbicsClientSubscriberDetails, + transactionID: String, + segmentNumber: Int, + numSegments: Int +): String { + val req = EbicsRequest.createForDownloadTransferPhase( + subscriberDetails.hostId, + transactionID, + segmentNumber, + numSegments + ) + val doc = XMLUtil.convertJaxbToDocument(req) + XMLUtil.signEbicsDocument(doc, subscriberDetails.customerAuthPriv) + return XMLUtil.convertDomToString(doc) +} + fun createEbicsRequestForUploadTransferPhase( subscriberDetails: EbicsClientSubscriberDetails, @@ -329,7 +346,10 @@ data class EbicsResponseContent( val dataEncryptionInfo: DataEncryptionInfo?, val orderDataEncChunk: String?, val technicalReturnCode: EbicsReturnCode, - val bankReturnCode: EbicsReturnCode + val bankReturnCode: EbicsReturnCode, + val segmentNumber: Int?, + // Only present in init phase + val numSegments: Int? ) data class EbicsKeyManagementResponseContent( @@ -404,7 +424,10 @@ fun parseAndValidateEbicsResponse( val responseDocument = try { XMLUtil.parseStringIntoDom(responseStr) } catch (e: Exception) { - throw EbicsProtocolError(HttpStatusCode.InternalServerError, "Invalid XML (as EbicsResponse) received from bank") + throw EbicsProtocolError( + HttpStatusCode.InternalServerError, + "Invalid XML (as EbicsResponse) received from bank" + ) } if (!XMLUtil.verifyEbicsDocument( @@ -420,7 +443,10 @@ fun parseAndValidateEbicsResponse( val resp = try { XMLUtil.convertStringToJaxb<EbicsResponse>(responseStr) } catch (e: Exception) { - throw EbicsProtocolError(HttpStatusCode.InternalServerError, "Could not transform string-response from bank into JAXB") + throw EbicsProtocolError( + HttpStatusCode.InternalServerError, + "Could not transform string-response from bank into JAXB" + ) } val bankReturnCodeStr = resp.value.body.returnCode.value @@ -441,7 +467,9 @@ fun parseAndValidateEbicsResponse( bankReturnCode = bankReturnCode, technicalReturnCode = techReturnCode, orderDataEncChunk = resp.value.body.dataTransfer?.orderData?.value, - dataEncryptionInfo = dataEncryptionInfo + dataEncryptionInfo = dataEncryptionInfo, + numSegments = resp.value.header._static.numSegments?.toInt(), + segmentNumber = resp.value.header.mutable.segmentNumber?.value?.toInt() ) } diff --git a/util/src/main/kotlin/ebics_h004/EbicsRequest.kt b/util/src/main/kotlin/ebics_h004/EbicsRequest.kt @@ -5,6 +5,7 @@ import tech.libeufin.util.CryptoUtil import java.math.BigInteger import java.security.interfaces.RSAPublicKey import java.util.* +import javax.swing.text.Segment import javax.xml.bind.annotation.* import javax.xml.bind.annotation.adapters.CollapsedStringAdapter import javax.xml.bind.annotation.adapters.HexBinaryAdapter @@ -360,10 +361,10 @@ class EbicsRequest { } securityMedium = "0000" } - mutable = MutableHeader().apply { - transactionPhase = - EbicsTypes.TransactionPhaseType.INITIALISATION - } + } + mutable = MutableHeader().apply { + transactionPhase = + EbicsTypes.TransactionPhaseType.INITIALISATION } } } @@ -473,5 +474,34 @@ class EbicsRequest { } } } + + fun createForDownloadTransferPhase( + hostID: String, + transactionID: String, + segmentNumber: Int, + numSegments: Int + ): EbicsRequest { + return EbicsRequest().apply { + version = "H004" + revision = 1 + authSignature = SignatureType() + body = Body() + header = Header().apply { + authenticate = true + static = StaticHeaderType().apply { + this.hostID = hostID + this.transactionID = transactionID + } + mutable = MutableHeader().apply { + transactionPhase = + EbicsTypes.TransactionPhaseType.TRANSFER + this.segmentNumber = EbicsTypes.SegmentNumber().apply { + this.value = BigInteger.valueOf(segmentNumber.toLong()) + this.lastSegment = segmentNumber == numSegments + } + } + } + } + } } } \ No newline at end of file