libeufin

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

commit 1b6289f33d5c5a84f269af86fe338190c193926b
parent ae0341f0663684e0828c35a9ce8429f562d6be15
Author: Antoine A <>
Date:   Wed, 25 Jun 2025 12:24:18 +0200

nexus: ebics fetch pinned start test with mocked bank server

Diffstat:
Mnexus/src/test/kotlin/EbicsTest.kt | 276++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
1 file changed, 209 insertions(+), 67 deletions(-)

diff --git a/nexus/src/test/kotlin/EbicsTest.kt b/nexus/src/test/kotlin/EbicsTest.kt @@ -17,6 +17,7 @@ * <http://www.gnu.org/licenses/> */ +import org.w3c.dom.Document import com.github.ajalt.clikt.testing.test import io.ktor.client.engine.mock.* import io.ktor.http.* @@ -31,8 +32,7 @@ import kotlin.io.path.* import kotlin.test.* import java.security.interfaces.RSAPrivateCrtKey import java.security.interfaces.RSAPublicKey - - +import java.time.LocalDate class EbicsState { private val bankSignKey: RSAPrivateCrtKey = CryptoUtil.genRSAPrivate(2048) @@ -85,6 +85,80 @@ class EbicsState { } } + private fun signedResponse(doc: Document): ByteArray { + XMLUtil.signEbicsDocument(doc, bankAuthKey) + return XMLUtil.convertDomToBytes(doc) + } + + private fun ebicsResponsePayload(payload: ByteArray): ByteArray { + transactionId = randEbicsId() + val deflated = payload.inputStream().deflate() + val (transactionKey, encryptedTransactionKey) = CryptoUtil.genEbicsE002Key(clientEncrPub!!) + val encrypted = CryptoUtil.encryptEbicsE002(transactionKey, deflated) + val doc = XmlBuilder.toDom("ebicsResponse", "http://www.ebics.org/H005") { + attr("http://www.w3.org/2000/xmlns/", "xmlns", "http://www.ebics.org/H005") + el("header") { + attr("authenticate", "true") + el("static") { + el("TransactionID", transactionId!!) + el("NumSegments", "1") + } + el("mutable") { + el("TransactionPhase", "Initialisation") + el("SegmentNumber") { + attr("lastSegment", "true") + text("1") + } + el("ReturnCode", "000000") + el("ReportText", "[EBICS_OK] OK") + } + } + el("AuthSignature") + el("body") { + el("DataTransfer") { + el("DataEncryptionInfo") { + attr("authenticate", "true") + el("EncryptionPubKeyDigest") { + attr("Version", "E002") + attr("Algorithm", "http://www.w3.org/2001/04/xmlenc#sha256") + text(CryptoUtil.getEbicsPublicKeyHash(clientEncrPub!!).encodeBase64()) + } + el("TransactionKey", encryptedTransactionKey.encodeBase64()) + } + el("OrderData", encrypted.encodeBase64()) + } + el("ReturnCode") { + attr("authenticate", "true") + text("000000") + } + } + } + return signedResponse(doc) + } + + private fun ebicsResponseNoData(): ByteArray { + val doc = XmlBuilder.toDom("ebicsResponse", "http://www.ebics.org/H005") { + attr("http://www.w3.org/2000/xmlns/", "xmlns", "http://www.ebics.org/H005") + el("header") { + attr("authenticate", "true") + el("static") + el("mutable") { + el("TransactionPhase", "Initialisation") + el("ReturnCode", "000000") + el("ReportText", "[EBICS_OK] OK") + } + } + el("AuthSignature") + el("body") { + el("ReturnCode") { + attr("authenticate", "true") + text("090005") + } + } + } + return signedResponse(doc) + } + fun hev(body: String): ByteArray { // Parse HEV request val hostId = XmlDestructor.parse(body, "ebicsHEVRequest") { @@ -193,7 +267,7 @@ class EbicsState { val code = one("body").one("TransferReceipt").one("ReceiptCode").text() assertEquals(code, "0") } - val doc = XmlBuilder.toDom("ebicsResponse", "http://www.ebics.org/H005") { + val response = signedResponse(XmlBuilder.toDom("ebicsResponse", "http://www.ebics.org/H005") { attr("http://www.w3.org/2000/xmlns/", "xmlns", "http://www.ebics.org/H005") el("header") { attr("authenticate", "true") @@ -213,10 +287,9 @@ class EbicsState { text("000000") } } - } + }) transactionId = null - XMLUtil.signEbicsDocument(doc, bankAuthKey) - return XMLUtil.convertDomToBytes(doc) + return response } fun hkd(body: String): ByteArray { @@ -228,90 +301,82 @@ class EbicsState { assertEquals(phase, "Initialisation") } } - transactionId = randEbicsId() - val payload = XmlBuilder.toBytes("HKDResponseOrderData") { - el("PartnerInfo") { - el("AddressInfo") - el("OrderInfo") { - el("AdminOrderType", "BTD") - el("Service") { - el("ServiceName", "STM") - el("Scope", "CH") - el("Container") { - attr("containerType", "ZIP") - } - el("MsgName") { - attr("version", "04") - text("camt.052") + return ebicsResponsePayload( + XmlBuilder.toBytes("HKDResponseOrderData") { + el("PartnerInfo") { + el("AddressInfo") + el("OrderInfo") { + el("AdminOrderType", "BTD") + el("Service") { + el("ServiceName", "STM") + el("Scope", "CH") + el("Container") { + attr("containerType", "ZIP") + } + el("MsgName") { + attr("version", "08") + text("camt.052") + } } + el("Description") } - el("Description") } } - - }.inputStream().deflate() + ) + } - val (transactionKey, encryptedTransactionKey) = CryptoUtil.genEbicsE002Key(clientEncrPub!!) - val encrypted = CryptoUtil.encryptEbicsE002(transactionKey, payload) - val doc = XmlBuilder.toDom("ebicsResponse", "http://www.ebics.org/H005") { - attr("http://www.w3.org/2000/xmlns/", "xmlns", "http://www.ebics.org/H005") - el("header") { - attr("authenticate", "true") - el("static") { - el("TransactionID", transactionId!!) - el("NumSegments", "1") - } - el("mutable") { - el("TransactionPhase", "Initialisation") - el("SegmentNumber") { - attr("lastSegment", "true") - text("1") - } - el("ReturnCode", "000000") - el("ReportText", "[EBICS_OK] OK") - } + fun haa(body: String): ByteArray { + XmlDestructor.parse(body, "ebicsRequest") { + one("header") { + val adminOrder = one("static").one("OrderDetails").one("AdminOrderType").text() + assertEquals(adminOrder, "HAA") + val phase = one("mutable").one("TransactionPhase").text() + assertEquals(phase, "Initialisation") } - el("AuthSignature") - el("body") { - el("DataTransfer") { - el("DataEncryptionInfo") { - attr("authenticate", "true") - el("EncryptionPubKeyDigest") { - attr("Version", "E002") - attr("Algorithm", "http://www.w3.org/2001/04/xmlenc#sha256") - text(CryptoUtil.getEbicsPublicKeyHash(clientEncrPub!!).encodeBase64()) - } - el("TransactionKey", encryptedTransactionKey.encodeBase64()) + } + return ebicsResponsePayload( + XmlBuilder.toBytes("HAAResponseOrderData") { + el("Service") { + el("ServiceName", "STM") + el("Scope", "CH") + el("Container") { + attr("containerType", "ZIP") + } + el("MsgName") { + attr("version", "08") + text("camt.052") } - el("OrderData", encrypted.encodeBase64()) - } - el("ReturnCode") { - attr("authenticate", "true") - text("000000") } } - } - XMLUtil.signEbicsDocument(doc, bankAuthKey) - return XMLUtil.convertDomToBytes(doc) + ) } - fun haa(body: String): ByteArray { + private fun btdDateCheck(body: String, pinned: LocalDate?): ByteArray { XmlDestructor.parse(body, "ebicsRequest") { one("header") { - val adminOrder = one("static").one("OrderDetails").one("AdminOrderType").text() - assertEquals(adminOrder, "HAA") + one("static").one("OrderDetails") { + val adminOrder = one("AdminOrderType").text() + assertEquals(adminOrder, "BTD") + val start = one("BTDOrderParams").opt("DateRange")?.opt("Start")?.date() + assertEquals(start, pinned) + } val phase = one("mutable").one("TransactionPhase").text() assertEquals(phase, "Initialisation") } } - throw Exception("tmp") + return ebicsResponseNoData() } + + fun btdNoData(body: String): ByteArray = btdDateCheck(body, null) + fun btdNoDataNow(body: String): ByteArray = btdDateCheck(body, LocalDate.now()) + fun btdNoDataPinned(body: String): ByteArray = btdDateCheck(body, LocalDate.parse("2024-06-05")) } @OptIn(kotlin.io.path.ExperimentalPathApi::class) class EbicsTest { private val nexusCmd = LibeufinNexus() private val bank = EbicsState() + private val args = "-L TRACE -c conf/fetch.conf" private fun setMock(sequences: Sequence<(String) -> ByteArray>) { val steps = sequences.iterator() @@ -341,7 +406,7 @@ class EbicsTest { }) // Run setup - val res = nexusCmd.test("ebics-setup -L TRACE -c conf/fetch.conf --auto-accept-keys") + val res = nexusCmd.test("ebics-setup $args --auto-accept-keys") assertEquals(res.statusCode, 0) } @@ -391,4 +456,81 @@ class EbicsTest { fun setup() = setup { _, _ -> ebicsSetup() } + + @Test + fun fetchPinnedDate() = setup { db, _ -> + ebicsSetup() + + suspend fun resetCheckpoint() { + db.serializable("DELETE FROM kv WHERE key=?") { + bind(CHECKPOINT_KEY) + executeUpdate() + } + } + + // Default transient + setMock(sequence { + yield(bank::haa) + yield(bank::receipt) + yield(bank::btdNoData) + }) + val transient = nexusCmd.test("ebics-fetch $args --transient") + assertEquals(transient.statusCode, 0) + + // Pinned transient + setMock(sequence { + yield(bank::haa) + yield(bank::receipt) + yield(bank::btdNoDataPinned) + }) + val transientPinned = nexusCmd.test("ebics-fetch $args --transient --pinned-start 2024-06-05") + assertEquals(transientPinned.statusCode, 0) + + // Init checkpoint + setMock(sequence { + yield(bank::hkd) + yield(bank::receipt) + yield(bank::btdNoData) + }) + val initCheckpoint = nexusCmd.test("ebics-fetch $args --transient --checkpoint") + assertEquals(initCheckpoint.statusCode, 0) + + // Default checkpoint + setMock(sequence { + yield(bank::hkd) + yield(bank::receipt) + yield(bank::btdNoDataNow) + }) + val checkpoint = nexusCmd.test("ebics-fetch $args --transient --checkpoint") + assertEquals(checkpoint.statusCode, 0) + + // Pinned checkpoint + setMock(sequence { + yield(bank::hkd) + yield(bank::receipt) + yield(bank::btdNoDataPinned) + }) + val transientCheckpoint = nexusCmd.test("ebics-fetch $args --transient --checkpoint --pinned-start 2024-06-05") + assertEquals(transientCheckpoint.statusCode, 0) + + // Reset checkpoint + resetCheckpoint() + setMock(sequence { + yield(bank::hkd) + yield(bank::receipt) + yield(bank::btdNoData) + }) + val resetCheckpoint = nexusCmd.test("ebics-fetch $args --transient --checkpoint") + assertEquals(resetCheckpoint.statusCode, 0) + + // Reset checkpoint pinned + resetCheckpoint() + setMock(sequence { + yield(bank::hkd) + yield(bank::receipt) + yield(bank::btdNoDataPinned) + }) + val resetCheckpointPinned = nexusCmd.test("ebics-fetch $args --transient --checkpoint --pinned-start 2024-06-05") + assertEquals(resetCheckpointPinned.statusCode, 0) + } } \ No newline at end of file