libeufin

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

commit 902cdf300456c929c0b1f5350f5e8147d94de538
parent 605aeff268fa30e66bb15c79fdccb9c325e2bb25
Author: Antoine A <>
Date:   Mon,  8 Jul 2024 13:57:58 +0200

nexus: study EBICS transaction semantics, fix cli error format and improve EBICS code

Diffstat:
Mcommon/src/main/kotlin/Cli.kt | 2+-
Mnexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt | 1+
Mnexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 21++++++++++++++++++++-
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt | 35+++++++++++++++++++++++------------
Mnexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsConstants.kt | 1+
Anexus/src/main/kotlin/tech/libeufin/nexus/test/TxCheck.kt | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtestbench/src/main/kotlin/Main.kt | 7++-----
7 files changed, 138 insertions(+), 19 deletions(-)

diff --git a/common/src/main/kotlin/Cli.kt b/common/src/main/kotlin/Cli.kt @@ -40,7 +40,7 @@ import org.slf4j.event.Level private val logger: Logger = LoggerFactory.getLogger("libeufin-config") fun Throwable.fmt(): String = buildString { - append(message ?: this::class.simpleName) + append(message ?: this@fmt::class.simpleName) var cause = cause while (cause != null) { append(": ") diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt @@ -61,6 +61,7 @@ data class SubmissionContext( * * @param ctx [SubmissionContext] * @return true on success, false otherwise. + * TODO update doc */ private suspend fun submitInitiatedPayment( ctx: SubmissionContext, diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt @@ -232,6 +232,25 @@ class FakeIncoming: CliktCommand("Genere a fake incoming payment") { } } +class TxCheck: CliktCommand("Check transaction semantic") { + private val common by CommonOption() + + override fun run() = cliCmd(logger, common.log) { + val cfg = loadNexusConfig(common.config) + val (clientKeys, bankKeys) = expectFullKeys(cfg) + val doc = EbicsDocument.acknowledgement.doc() + val order = cfg.dialect.downloadDoc(doc, false) + val client = HttpClient { + install(HttpTimeout) { + // It can take a lot of time for the bank to generate documents + socketTimeoutMillis = 5 * 60 * 1000 + } + } + val result = tech.libeufin.nexus.test.txCheck(client, cfg, clientKeys, bankKeys, order, cfg.dialect.directDebit()) + println("$result") + } +} + enum class ListKind { incoming, outgoing, @@ -397,7 +416,7 @@ class ListCmd: CliktCommand("List nexus transactions", name = "list") { class TestingCmd : CliktCommand("Testing helper commands", name = "testing") { init { - subcommands(FakeIncoming(), ListCmd(), EbicsDownload()) + subcommands(FakeIncoming(), ListCmd(), EbicsDownload(), TxCheck()) } override fun run() = Unit diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsCommon.kt @@ -139,11 +139,13 @@ suspend fun EbicsBTS.postBTS( )) { throw EbicsError.Protocol("$phase: bank signature did not verify") } - try { - return EbicsBTS.parseResponse(doc) + val response = try { + EbicsBTS.parseResponse(doc) } catch (e: Exception) { throw EbicsError.Protocol("$phase: invalid ebics response", e) } + logger.debug("{} return codes: {} & {}", phase, response.technicalCode, response.bankCode) + return response } /** @@ -167,7 +169,7 @@ suspend fun ebicsDownload( clientKeys: ClientPrivateKeysFile, bankKeys: BankPublicKeysFile, order: EbicsOrder, - startDate: Instant?, + startDate: Instant?, endDate: Instant?, processing: suspend (InputStream) -> Unit, ) = coroutineScope { @@ -303,17 +305,18 @@ fun prepareUploadPayload( innerSignedEbicsXml.inputStream().deflate() ).encodeBase64() // Compress and encrypt payload - val segment = CryptoUtil.encryptEbicsE002( + val encrypted = CryptoUtil.encryptEbicsE002( transactionKey, payload.inputStream().deflate() - ).encodeBase64() - // TODO split 1MB segment when we have payloads that big + ) + // Chunks of 1MB and encode segments + val segments = encrypted.encodeBase64().chunked(1000000) return PreparedUploadData( encryptedTransactionKey, orderSignature, payloadDigest, - listOf(segment) + segments ) } @@ -336,9 +339,8 @@ suspend fun doEbicsUpload( bankKeys: BankPublicKeysFile, order: EbicsOrder, payload: ByteArray, -): String = withContext(NonCancellable) { +): String { val impl = EbicsBTS(cfg, bankKeys, clientKeys, order) - // TODO use a lambda and pass the order detail there for atomicity ? val preparedPayload = prepareUploadPayload(cfg, clientKeys, bankKeys, payload) // Init phase @@ -350,13 +352,13 @@ suspend fun doEbicsUpload( val orderId = requireNotNull(initResp.orderID) { "Upload init phase: missing order ID" } - + // Transfer phase for (i in 1..preparedPayload.segments.size) { val transferXml = impl.uploadTransfer(tId, preparedPayload, i) val transferResp = impl.postBTS(client, transferXml, "Upload transfer phase").okOrFail("Upload transfer phase") } - orderId + return orderId } private val SECURE_RNG = SecureRandom() @@ -385,9 +387,18 @@ class EbicsResponse<T>( val bankCode: EbicsReturnCode, private val content: T ) { + /** Checks that return codes are both EBICS_OK */ + fun ok(): T? { + return if (technicalCode.kind() != EbicsReturnCode.Kind.Error && + bankCode.kind() != EbicsReturnCode.Kind.Error) { + content + } else { + null + } + } + /** Checks that return codes are both EBICS_OK or throw an exception */ fun okOrFail(phase: String): T { - logger.debug("{} return codes: {} & {}", phase, technicalCode, bankCode) require(technicalCode.kind() != EbicsReturnCode.Kind.Error) { "$phase has technical error: $technicalCode" } diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsConstants.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsConstants.kt @@ -44,6 +44,7 @@ enum class EbicsReturnCode(val code: String) { EBICS_INVALID_XML("091010"), EBICS_TX_MESSAGE_REPLAY("091103"), EBICS_TX_SEGMENT_NUMBER_EXCEEDED("091104"), + EBICS_TX_UNKNOWN_TXID("091101"), EBICS_INVALID_REQUEST_CONTENT("091113"), EBICS_PROCESSING_ERROR("091116"), diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/test/TxCheck.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/test/TxCheck.kt @@ -0,0 +1,89 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2024 Taler Systems S.A. + * + * LibEuFin is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation; either version 3, or + * (at your option) any later version. + * + * LibEuFin is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General + * Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with LibEuFin; see the file COPYING. If not, see + * <http://www.gnu.org/licenses/> + */ + +package tech.libeufin.nexus.test + +import io.ktor.client.* +import tech.libeufin.common.* +import tech.libeufin.nexus.* +import tech.libeufin.nexus.ebics.* + +data class TxCheckResult( + var concurrentFetchAndFetch: Boolean = false, + var concurrentFetchAndSubmit: Boolean = false, + var concurrentSubmitAndSubmit: Boolean = false, + var idempotentClose: Boolean = false +) + +/** + * Test EBICS implementation's transactions semantic: + * - Can two fetch transactions run concurently ? + * - Can a fetch & submit transactions run concurently ? + * - Can two submit transactions run concurently ? + * - Is closing a submit transaction idempotent + */ +suspend fun txCheck( + client: HttpClient, + cfg: NexusConfig, + clientKeys: ClientPrivateKeysFile, + bankKeys: BankPublicKeysFile, + fetchOrder: EbicsOrder, + submitOrder: EbicsOrder +): TxCheckResult { + val result = TxCheckResult() + val fetch = EbicsBTS(cfg, bankKeys, clientKeys, fetchOrder) + val submit = EbicsBTS(cfg, bankKeys, clientKeys, submitOrder) + + suspend fun EbicsBTS.close(id: String, phase: String) { + val xml = downloadReceipt(id, false) + postBTS(client, xml, phase).okOrFail(phase) + } + + val firstTxId = fetch.postBTS(client, fetch.downloadInitialization(null, null), "Init first fetch") + .okOrFail("Init first fetch") + .transactionID!! + try { + fetch.postBTS(client, fetch.downloadInitialization(null, null), "Init second fetch").ok()?.run { + result.concurrentFetchAndFetch = true + fetch.close(transactionID!!, "Init second fetch") + } + var paylod = prepareUploadPayload(cfg, clientKeys, bankKeys, ByteArray(2000000).rand()) + val submitId = submit.postBTS(client, submit.uploadInitialization(paylod), "Init first submit").ok()?.run { + result.concurrentFetchAndSubmit = true + transactionID!! + } + if (submitId != null) { + submit.postBTS(client, submit.uploadTransfer(submitId, paylod, 1), "Submit upload").okOrFail("Submit first upload") + submit.postBTS(client, submit.uploadInitialization(paylod), "Init second submit").ok()?.run { + result.concurrentSubmitAndSubmit = true + } + } + } finally { + fetch.close(firstTxId, "Close first fetch") + } + + try { + fetch.close(firstTxId, "Close first fetch a second time") + result.idempotentClose = true + } catch (e: Exception) { + logger.debug(e.fmt()) + } + + return result +} +\ No newline at end of file diff --git a/testbench/src/main/kotlin/Main.kt b/testbench/src/main/kotlin/Main.kt @@ -29,11 +29,7 @@ import io.ktor.client.* import io.ktor.client.engine.cio.* import kotlinx.coroutines.runBlocking import kotlinx.serialization.Serializable -import tech.libeufin.nexus.LibeufinNexusCommand -import tech.libeufin.nexus.loadBankKeys -import tech.libeufin.nexus.loadClientKeys -import tech.libeufin.nexus.loadConfig -import tech.libeufin.nexus.loadJsonFile +import tech.libeufin.nexus.* import tech.libeufin.common.ANSI import kotlin.io.path.* @@ -213,6 +209,7 @@ class Cli : CliktCommand("Run integration tests on banks provider") { nexusCmd.run("ebics-submit $ebicsFlags") Unit }) + put("tx-check", "Check transaction semantic", "testing tx-check $flags") } while (true) { var clientKeys = loadClientKeys(clientKeysPath)