summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMS <ms@taler.net>2023-03-31 14:08:04 +0200
committerMS <ms@taler.net>2023-03-31 14:08:04 +0200
commit84a5889dd8b49510f1b76fa68211070667d4d177 (patch)
tree2bb9a485be9ce13395f540ff27a487af3e67d832
parent9fa7dbec7ada36d55893a80db8bd1eb71e72d10a (diff)
downloadlibeufin-84a5889dd8b49510f1b76fa68211070667d4d177.tar.gz
libeufin-84a5889dd8b49510f1b76fa68211070667d4d177.tar.bz2
libeufin-84a5889dd8b49510f1b76fa68211070667d4d177.zip
tests
-rwxr-xr-xcli/tests/launch_services.sh13
-rw-r--r--nexus/src/test/kotlin/DownloadAndSubmit.kt8
-rw-r--r--nexus/src/test/kotlin/MakeEnv.kt140
-rw-r--r--nexus/src/test/kotlin/NexusApiTest.kt4
-rw-r--r--nexus/src/test/kotlin/SandboxCircuitApiTest.kt26
-rw-r--r--nexus/src/test/kotlin/TalerTest.kt106
-rw-r--r--nexus/src/test/kotlin/XLibeufinBankTest.kt111
7 files changed, 346 insertions, 62 deletions
diff --git a/cli/tests/launch_services.sh b/cli/tests/launch_services.sh
index b3fcc4eb..2bee7df7 100755
--- a/cli/tests/launch_services.sh
+++ b/cli/tests/launch_services.sh
@@ -4,8 +4,8 @@
# EBICS pair, in order to try CLI commands.
set -eu
-WITH_TASKS=1
-# WITH_TASKS=0
+# WITH_TASKS=1
+WITH_TASKS=0
function exit_cleanup()
{
echo "Running exit-cleanup"
@@ -25,13 +25,13 @@ curl --version &> /dev/null || (echo "'curl' command not found"; exit 77)
SQLITE_FILE_PATH=/tmp/libeufin-cli-test.sqlite3
getDbConn () {
if test withPostgres == "${1:-}"; then
- echo "jdbc:postgresql://localhost:5432/taler?user=$(whoami)"
+ echo "jdbc:postgresql://localhost:5432/libeufincheck?user=$(whoami)"
return
fi
echo "jdbc:sqlite:${SQLITE_FILE_PATH}"
}
-DB_CONN=`getDbConn`
+DB_CONN=`getDbConn withPostgres`
export LIBEUFIN_SANDBOX_DB_CONNECTION=$DB_CONN
export LIBEUFIN_NEXUS_DB_CONNECTION=$DB_CONN
@@ -139,6 +139,9 @@ if test 1 = $WITH_TASKS; then
www-nexus || true
echo OK
else
- echo NOT creating backound tasks!
+ echo NOT creating background tasks!
fi
+echo "Requesting Taler history with 90 seconds timeout..."
+curl -u test-user:x "http://localhost:5001/facades/test-facade/taler-wire-gateway/history/incoming?delta=5&long_poll_ms=90000"
+
read -p "Press Enter to terminate..."
diff --git a/nexus/src/test/kotlin/DownloadAndSubmit.kt b/nexus/src/test/kotlin/DownloadAndSubmit.kt
index 0ac5b0c7..622ff928 100644
--- a/nexus/src/test/kotlin/DownloadAndSubmit.kt
+++ b/nexus/src/test/kotlin/DownloadAndSubmit.kt
@@ -114,9 +114,9 @@ class DownloadAndSubmit {
client,
fetchSpec = FetchSpecAllJson(
level = FetchLevel.REPORT,
- "foo"
+ bankConnection = "foo"
),
- "foo"
+ accountId = "foo"
)
}
transaction {
@@ -223,7 +223,7 @@ class DownloadAndSubmit {
}
/**
- * Submit one payment instruction with a invalid Pain.001
+ * Submit one payment instruction with an invalid Pain.001
* document, and check that it was marked as invalid. Hence,
* the error is expected only by the first submission, since
* the second won't pick the invalid payment.
@@ -238,7 +238,7 @@ class DownloadAndSubmit {
addPaymentInitiation(
Pain001Data(
creditorIban = getIban(),
- creditorBic = "not-a-BIC",
+ creditorBic = "not-a-BIC", // this value causes the expected error.
creditorName = "Tester",
subject = "test payment",
sum = "1",
diff --git a/nexus/src/test/kotlin/MakeEnv.kt b/nexus/src/test/kotlin/MakeEnv.kt
index 9f8f5249..596b2c95 100644
--- a/nexus/src/test/kotlin/MakeEnv.kt
+++ b/nexus/src/test/kotlin/MakeEnv.kt
@@ -3,19 +3,16 @@ import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.statements.api.ExposedBlob
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.jetbrains.exposed.sql.transactions.transaction
-import org.jetbrains.exposed.sql.transactions.transactionManager
import tech.libeufin.nexus.*
import tech.libeufin.nexus.dbCreateTables
import tech.libeufin.nexus.dbDropTables
import tech.libeufin.nexus.iso20022.*
+import tech.libeufin.nexus.server.BankConnectionType
import tech.libeufin.nexus.server.CurrencyAmount
import tech.libeufin.nexus.server.FetchLevel
import tech.libeufin.nexus.server.FetchSpecAllJson
import tech.libeufin.sandbox.*
-import tech.libeufin.util.CryptoUtil
-import tech.libeufin.util.EbicsInitState
-import java.io.File
-import tech.libeufin.util.getIban
+import tech.libeufin.util.*
data class EbicsKeys(
val auth: CryptoUtil.RsaCrtKeyPair,
@@ -40,6 +37,15 @@ val userKeys = EbicsKeys(
sig = CryptoUtil.generateRsaKeyPair(2048)
)
+fun assertWithPrint(cond: Boolean, msg: String) {
+ try {
+ assert(cond)
+ } catch (e: AssertionError) {
+ System.err.println(msg)
+ throw e
+ }
+}
+
// New versions of JUnit provide this!
inline fun <reified ExceptionType> assertException(
block: () -> Unit,
@@ -85,11 +91,28 @@ fun prepNexusDb() {
passwordHash = CryptoUtil.hashpw("foo")
superuser = true
}
+ val b = NexusUserEntity.new {
+ username = "bar"
+ passwordHash = CryptoUtil.hashpw("bar")
+ superuser = true
+ }
val c = NexusBankConnectionEntity.new {
+ connectionId = "bar"
+ owner = b
+ type = "x-libeufin-bank"
+ }
+ val d = NexusBankConnectionEntity.new {
connectionId = "foo"
- owner = u
+ owner = b
type = "ebics"
}
+ XLibeufinBankUserEntity.new {
+ username = "bar"
+ password = "bar"
+ // Only addressing mild cases where ONE slash ends the base URL.
+ baseUrl = "http://localhost/demobanks/default/access-api"
+ nexusBankConnection = c
+ }
tech.libeufin.nexus.EbicsSubscriberEntity.new {
// ebicsURL = "http://localhost:5000/ebicsweb"
ebicsURL = "http://localhost/ebicsweb"
@@ -100,7 +123,7 @@ fun prepNexusDb() {
signaturePrivateKey = ExposedBlob(userKeys.sig.private.encoded)
encryptionPrivateKey = ExposedBlob(userKeys.enc.private.encoded)
authenticationPrivateKey = ExposedBlob(userKeys.auth.private.encoded)
- nexusBankConnection = c
+ nexusBankConnection = d
ebicsIniState = EbicsInitState.NOT_SENT
ebicsHiaState = EbicsInitState.NOT_SENT
bankEncryptionPublicKey = ExposedBlob(bankKeys.enc.public.encoded)
@@ -110,7 +133,7 @@ fun prepNexusDb() {
bankAccountName = "foo"
iban = FOO_USER_IBAN
bankCode = "SANDBOXX"
- defaultBankConnection = c
+ defaultBankConnection = d
highestSeenBankMessageSerialId = 0
accountHolder = "foo"
}
@@ -140,7 +163,7 @@ fun prepNexusDb() {
}
// Giving 'foo' a Taler facade.
val f = FacadeEntity.new {
- facadeName = "taler"
+ facadeName = "foo-facade"
type = "taler-wire-gateway"
creator = u
}
@@ -152,6 +175,20 @@ fun prepNexusDb() {
facade = f
highestSeenMessageSerialId = 0
}
+ // Giving 'bar' a Taler facade
+ val g = FacadeEntity.new {
+ facadeName = "bar-facade"
+ type = "taler-wire-gateway"
+ creator = b
+ }
+ FacadeStateEntity.new {
+ bankAccount = "bar"
+ bankConnection = "bar" // uses x-libeufin-bank connection.
+ currency = "TESTKUDOS"
+ reserveTransferLevel = "report"
+ facade = g
+ highestSeenMessageSerialId = 0
+ }
}
}
@@ -287,35 +324,82 @@ fun withSandboxTestDatabase(f: () -> Unit) {
}
}
-fun newNexusBankTransaction(currency: String, value: String, subject: String) {
+fun newNexusBankTransaction(
+ currency: String,
+ value: String,
+ subject: String,
+ creditorAcct: String = "foo",
+ connType: BankConnectionType = BankConnectionType.EBICS
+) {
+ val jDetails: String = when(connType) {
+ BankConnectionType.EBICS -> {
+ jacksonObjectMapper(
+ ).writerWithDefaultPrettyPrinter(
+ ).writeValueAsString(
+ genNexusIncomingCamt(
+ amount = CurrencyAmount(currency,value),
+ subject = subject
+ )
+ )
+ }
+ /**
+ * Note: x-libeufin-bank ALSO stores the transactions in the
+ * CaMt representation, hence this branch should be removed.
+ */
+ BankConnectionType.X_LIBEUFIN_BANK -> {
+ jacksonObjectMapper(
+ ).writerWithDefaultPrettyPrinter(
+ ).writeValueAsString(genNexusIncomingCamt(
+ amount = CurrencyAmount(currency, value),
+ subject = subject
+ ))
+ }
+ else -> throw Exception("Unsupported connection type: ${connType.typeName}")
+ }
transaction {
NexusBankTransactionEntity.new {
- bankAccount = NexusBankAccountEntity.findByName("foo")!!
+ bankAccount = NexusBankAccountEntity.findByName(creditorAcct)!!
accountTransactionId = "mock"
creditDebitIndicator = "CRDT"
this.currency = currency
this.amount = value
status = EntryStatus.BOOK
- transactionJson = jacksonObjectMapper(
- ).writerWithDefaultPrettyPrinter(
- ).writeValueAsString(
- genNexusIncomingPayment(
- amount = CurrencyAmount(currency,value),
- subject = subject
- )
- )
+ transactionJson = jDetails
}
- /*TalerIncomingPaymentEntity.new {
- payment = inc
- reservePublicKey = "mock"
- timestampMs = 0L
- debtorPaytoUri = "mock"
- }*/
}
}
-
-fun genNexusIncomingPayment(
+/**
+ * This function generates the Nexus JSON model of one transaction
+ * as if it got downloaded via one x-libeufin-bank connection. The
+ * non given values are either resorted from other sources by Nexus,
+ * or actually not useful so far.
+ */
+private fun genNexusIncomingXLibeufinBank(
+ amount: CurrencyAmount,
+ subject: String
+): XLibeufinBankTransaction =
+ XLibeufinBankTransaction(
+ creditorIban = "NOTUSED",
+ creditorBic = null,
+ creditorName = "Not Used",
+ debtorIban = "NOTUSED",
+ debtorBic = null,
+ debtorName = "Not Used",
+ amount = amount.value,
+ currency = amount.currency,
+ subject = subject,
+ date = "0",
+ uid = "not-used",
+ direction = XLibeufinBankDirection.CREDIT
+ )
+/**
+ * This function generates the Nexus JSON model of one transaction
+ * as if it got downloaded via one Ebics connection. The non given
+ * values are either resorted from other sources by Nexus, or actually
+ * not useful so far.
+ */
+private fun genNexusIncomingCamt(
amount: CurrencyAmount,
subject: String,
): CamtBankAccountEntry =
@@ -382,4 +466,4 @@ fun genNexusIncomingPayment(
)
)
)
- ) \ No newline at end of file
+ )
diff --git a/nexus/src/test/kotlin/NexusApiTest.kt b/nexus/src/test/kotlin/NexusApiTest.kt
index 30763005..e4fcc6d0 100644
--- a/nexus/src/test/kotlin/NexusApiTest.kt
+++ b/nexus/src/test/kotlin/NexusApiTest.kt
@@ -4,13 +4,13 @@ import io.ktor.http.*
import io.ktor.server.testing.*
import org.junit.Test
import tech.libeufin.nexus.server.nexusApp
+import tech.libeufin.sandbox.sandboxApp
/**
* This class tests the API offered by Nexus,
* documented here: https://docs.taler.net/libeufin/api-nexus.html
*/
class NexusApiTest {
-
// Testing basic operations on facades.
@Test
fun facades() {
@@ -19,7 +19,7 @@ class NexusApiTest {
prepNexusDb()
testApplication {
application(nexusApp)
- client.delete("/facades/taler") {
+ client.delete("/facades/foo-facade") {
basicAuth("foo", "foo")
expectSuccess = true
}
diff --git a/nexus/src/test/kotlin/SandboxCircuitApiTest.kt b/nexus/src/test/kotlin/SandboxCircuitApiTest.kt
index 8979fef9..d9ff3d51 100644
--- a/nexus/src/test/kotlin/SandboxCircuitApiTest.kt
+++ b/nexus/src/test/kotlin/SandboxCircuitApiTest.kt
@@ -38,18 +38,40 @@ class SandboxCircuitApiTest {
prepSandboxDb()
testApplication {
application(sandboxApp)
- val R = client.get(
+ var R = client.get(
"/demobanks/default/circuit-api/cashouts/estimates?amount_debit=TESTKUDOS:2"
) {
expectSuccess = true
basicAuth("foo", "foo")
}
val mapper = ObjectMapper()
- val respJson = mapper.readTree(R.bodyAsText())
+ var respJson = mapper.readTree(R.bodyAsText())
val creditAmount = respJson.get("amount_credit").asText()
// sell ratio and fee are the following constants: 0.95 and 0.
// expected credit amount = 2 * 0.95 - 0 = 1.90
assert("CHF:1.90" == creditAmount || "CHF:1.9" == creditAmount)
+ R = client.get(
+ "/demobanks/default/circuit-api/cashouts/estimates?amount_credit=CHF:1.9"
+ ) {
+ expectSuccess = true
+ basicAuth("foo", "foo")
+ }
+ respJson = mapper.readTree(R.bodyAsText())
+ val debitAmount = respJson.get("amount_debit").asText()
+ assertWithPrint(
+ "TESTKUDOS:2" == debitAmount || "TESTKUDOS:2.0" == debitAmount,
+ "'debit_amount' was $debitAmount for a 'credit_amount' of CHF:1.9"
+ )
+ R = client.get(
+ "/demobanks/default/circuit-api/cashouts/estimates?amount_credit=CHF:1&amount_debit=TESTKUDOS=1"
+ ) {
+ expectSuccess = false
+ basicAuth("foo", "foo")
+ }
+ assertWithPrint(
+ R.status.value == HttpStatusCode.BadRequest.value,
+ "Expected status code was 400, but got '${R.status.value}' instead."
+ )
}
}
}
diff --git a/nexus/src/test/kotlin/TalerTest.kt b/nexus/src/test/kotlin/TalerTest.kt
index c433284a..f877203b 100644
--- a/nexus/src/test/kotlin/TalerTest.kt
+++ b/nexus/src/test/kotlin/TalerTest.kt
@@ -30,7 +30,7 @@ class TalerTest {
withNexusAndSandboxUser {
testApplication {
application(nexusApp)
- client.post("/facades/taler/taler-wire-gateway/transfer") {
+ client.post("/facades/foo-facade/taler-wire-gateway/transfer") {
contentType(ContentType.Application.Json)
basicAuth("foo", "foo") // exchange's credentials
expectSuccess = true
@@ -73,7 +73,7 @@ class TalerTest {
*/
testApplication {
application(nexusApp)
- val r = client.get("/facades/taler/taler-wire-gateway/history/outgoing?delta=5") {
+ val r = client.get("/facades/foo-facade/taler-wire-gateway/history/outgoing?delta=5") {
expectSuccess = true
contentType(ContentType.Application.Json)
basicAuth("foo", "foo")
@@ -85,38 +85,102 @@ class TalerTest {
}
}
- // Checking that a correct wire transfer (with Taler-compatible subject)
- // is responded by the Taler facade.
+ // Tests that incoming Taler txs arrive via EBICS.
@Test
- fun historyIncomingTest() {
+ fun historyIncomingTestEbics() {
+ historyIncomingTest(
+ testedAccount = "foo",
+ connType = BankConnectionType.EBICS
+ )
+ }
+
+ // Tests that incoming Taler txs arrive via x-libeufin-bank.
+ @Test
+ fun historyIncomingTestXLibeufinBank() {
+ historyIncomingTest(
+ testedAccount = "bar",
+ connType = BankConnectionType.X_LIBEUFIN_BANK
+ )
+ }
+
+ // Tests that even if one call is long-polling, other calls
+ @Test
+ fun servingTest() {
+ withTestDatabase {
+ prepNexusDb()
+ testApplication {
+ application(nexusApp)
+ // This call blocks for 90 seconds
+ val currentTime = System.currentTimeMillis()
+ runBlocking {
+ launch {
+ val r = client.get("/facades/foo-facade/taler-wire-gateway/history/incoming?delta=5&start=0&long_poll_ms=5000") {
+ expectSuccess = true
+ contentType(ContentType.Application.Json)
+ basicAuth("foo", "foo") // user & pw always equal.
+ }
+ assert(r.status.value == HttpStatusCode.NoContent.value)
+ }
+ val R = client.get("/") {
+ expectSuccess = true
+ }
+ val latestTime = System.currentTimeMillis()
+ assert(R.status.value == HttpStatusCode.OK.value
+ && (latestTime - currentTime) < 2000
+ )
+ }
+ }
+ }
+ }
+
+ // Downloads Taler txs via the default connection of 'testedAccount'.
+ // This allows to test the Taler logic on different connection types.
+ fun historyIncomingTest(testedAccount: String, connType: BankConnectionType) {
val reservePub = "GX5H5RME193FDRCM1HZKERXXQ2K21KH7788CKQM8X6MYKYRBP8F0"
withNexusAndSandboxUser {
testApplication {
application(nexusApp)
runBlocking {
+ /**
+ * This block issues the request by long-polling and
+ * lets the execution proceed where the actions to unblock
+ * the polling are taken.
+ */
launch {
- val r = client.get("/facades/taler/taler-wire-gateway/history/incoming?delta=5&start=0&long_poll_ms=3000") {
- expectSuccess = false
+ val r = client.get("/facades/${testedAccount}-facade/taler-wire-gateway/history/incoming?delta=5&start=0&long_poll_ms=30000") {
+ expectSuccess = true
contentType(ContentType.Application.Json)
- basicAuth("foo", "foo")
+ basicAuth(testedAccount, testedAccount) // user & pw always equal.
}
- println("maybe response body: ${r.bodyAsText()}")
- assert(r.status.value == HttpStatusCode.OK.value)
+ assertWithPrint(
+ r.status.value == HttpStatusCode.OK.value,
+ "Long-polling history had status: ${r.status.value} and" +
+ " body: ${r.bodyAsText()}"
+ )
val j = mapper.readTree(r.readBytes())
val reservePubFromTwg = j.get("incoming_transactions").get(0).get("reserve_pub").asText()
assert(reservePubFromTwg == reservePub)
}
- newNexusBankTransaction(
- "KUDOS",
- "10",
- reservePub
- )
- ingestFacadeTransactions(
- "foo", // bank account local to Nexus.
- "taler-wire-gateway",
- ::talerFilter,
- ::maybeTalerRefunds
- )
+ launch {
+ delay(500)
+ /**
+ * FIXME: this test never gets the server to wait notifications from the DBMS.
+ * Somehow, the wire transfer arrives always before the blocking await on the DBMS.
+ */
+ newNexusBankTransaction(
+ currency = "KUDOS",
+ value = "10",
+ subject = reservePub,
+ creditorAcct = testedAccount,
+ connType = connType
+ )
+ ingestFacadeTransactions(
+ bankAccountId = testedAccount, // bank account local to Nexus.
+ facadeType = NexusFacadeType.TALER,
+ incomingFilterCb = ::talerFilter,
+ refundCb = ::maybeTalerRefunds
+ )
+ }
}
}
}
diff --git a/nexus/src/test/kotlin/XLibeufinBankTest.kt b/nexus/src/test/kotlin/XLibeufinBankTest.kt
new file mode 100644
index 00000000..9af9133c
--- /dev/null
+++ b/nexus/src/test/kotlin/XLibeufinBankTest.kt
@@ -0,0 +1,111 @@
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import io.ktor.server.testing.*
+import org.jetbrains.exposed.sql.transactions.transaction
+import org.junit.Test
+import tech.libeufin.nexus.BankConnectionProtocol
+import tech.libeufin.nexus.NexusBankTransactionEntity
+import tech.libeufin.nexus.NexusBankTransactionsTable
+import tech.libeufin.nexus.bankaccount.ingestBankMessagesIntoAccount
+import tech.libeufin.nexus.getNexusUser
+import tech.libeufin.nexus.iso20022.CamtBankAccountEntry
+import tech.libeufin.nexus.server.*
+import tech.libeufin.nexus.xlibeufinbank.XlibeufinBankConnectionProtocol
+import tech.libeufin.sandbox.sandboxApp
+import tech.libeufin.sandbox.wireTransfer
+import tech.libeufin.util.XLibeufinBankTransaction
+import java.net.URL
+
+// Testing the x-libeufin-bank communication
+
+class XLibeufinBankTest {
+ private val mapper = jacksonObjectMapper()
+ @Test
+ fun urlParse() {
+ val u = URL("http://localhost")
+ println(u.authority)
+ }
+
+ /**
+ * This test tries to submit a transaction to Sandbox
+ * via the x-libeufin-bank connection and later - after
+ * having downloaded its transactions - tries to reconcile
+ * it as sent.
+ */
+ @Test
+ fun submitTransaction() {
+
+ }
+
+ /**
+ * Testing that Nexus downloads one transaction from
+ * Sandbox via the x-libeufin-bank protocol supplier
+ * and stores it in the Nexus internal transactions
+ * table.
+ *
+ * NOTE: the test should be extended by checking that
+ * downloading twice the transaction doesn't lead to asset
+ * duplication locally in Nexus.
+ */
+ @Test
+ fun fetchTransaction() {
+ withTestDatabase {
+ prepSandboxDb()
+ prepNexusDb()
+ testApplication {
+ // Creating the Sandbox transaction that's expected to be ingested.
+ wireTransfer(
+ debitAccount = "bar",
+ creditAccount = "foo",
+ demobank = "default",
+ subject = "x-libeufin-bank test transaction",
+ amount = "TESTKUDOS:333"
+ )
+ val fooUser = getNexusUser("foo")
+ // Creating the x-libeufin-bank connection to interact with Sandbox.
+ val conn = XlibeufinBankConnectionProtocol()
+ val jDetails = """{
+ "username": "foo",
+ "password": "foo",
+ "baseUrl": "http://localhost/demobanks/default/access-api"
+ }""".trimIndent()
+ conn.createConnection(
+ connId = "x",
+ user = fooUser,
+ data = mapper.readTree(jDetails)
+ )
+ // Starting _Sandbox_ to check how it reacts to Nexus request.
+ application(sandboxApp)
+ /**
+ * Doing two rounds of download: the first is expected to
+ * record the payment as new, and the second is expected to
+ * ignore it because it has already it in the database.
+ */
+ repeat(2) {
+ // Invoke transaction fetcher offered by the x-libeufin-bank connection.
+ conn.fetchTransactions(
+ fetchSpec = FetchSpecAllJson(
+ FetchLevel.STATEMENT,
+ null
+ ),
+ accountId = "foo",
+ bankConnectionId = "x",
+ client = client
+ )
+ }
+ // The messages are in the database now, invoke the
+ // ingestion routine to parse them into the Nexus internal
+ // format.
+ ingestBankMessagesIntoAccount("x", "foo")
+ // Asserting that the payment made it to the database in the Nexus format.
+ transaction {
+ val maybeTx = NexusBankTransactionEntity.all()
+ // This assertion checks that the payment is not doubled in the database:
+ assert(maybeTx.count() == 1L)
+ val tx = maybeTx.first().parseDetailsIntoObject<CamtBankAccountEntry>()
+ assert(tx.getSingletonSubject() == "x-libeufin-bank test transaction")
+ }
+ }
+ }
+ }
+} \ No newline at end of file