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:
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