summaryrefslogtreecommitdiff
path: root/testbench
diff options
context:
space:
mode:
authorAntoine A <>2024-01-23 18:06:55 +0100
committerAntoine A <>2024-01-23 18:10:59 +0100
commitbb7e455b0f71ba1870f4233f58bcb4bd4fbf05ed (patch)
treeecb0f851e8781dd8c8100224d079131ec56efea9 /testbench
parent8aeffb3f9d4fa5323d896a46902ed2384a953cbd (diff)
downloadlibeufin-bb7e455b0f71ba1870f4233f58bcb4bd4fbf05ed.tar.gz
libeufin-bb7e455b0f71ba1870f4233f58bcb4bd4fbf05ed.tar.bz2
libeufin-bb7e455b0f71ba1870f4233f58bcb4bd4fbf05ed.zip
Split utils into common and ebics and ename integration to testbench
Diffstat (limited to 'testbench')
-rw-r--r--testbench/build.gradle39
-rw-r--r--testbench/conf/integration.conf17
-rw-r--r--testbench/conf/mini.conf2
-rw-r--r--testbench/conf/netzbon.conf28
-rw-r--r--testbench/conf/postfinance.conf27
-rw-r--r--testbench/src/main/kotlin/Main.kt229
-rw-r--r--testbench/src/test/kotlin/IntegrationTest.kt325
7 files changed, 667 insertions, 0 deletions
diff --git a/testbench/build.gradle b/testbench/build.gradle
new file mode 100644
index 00000000..93fb6a46
--- /dev/null
+++ b/testbench/build.gradle
@@ -0,0 +1,39 @@
+plugins {
+ id("kotlin")
+ id("application")
+}
+
+java {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+}
+
+compileKotlin.kotlinOptions.jvmTarget = "17"
+compileTestKotlin.kotlinOptions.jvmTarget = "17"
+
+sourceSets.main.java.srcDirs = ["src/main/kotlin"]
+
+dependencies {
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version")
+
+ implementation(project(":common"))
+ implementation(project(":bank"))
+ implementation(project(":nexus"))
+
+ implementation("com.github.ajalt.clikt:clikt:$clikt_version")
+
+ implementation("org.postgresql:postgresql:$postgres_version")
+
+ implementation("io.ktor:ktor-server-test-host:$ktor_version")
+ implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")
+ implementation("org.jetbrains.kotlin:kotlin-test:$kotlin_version")
+}
+
+application {
+ mainClass = "tech.libeufin.testbench.MainKt"
+ applicationName = "libeufin-testbench-test"
+}
+
+run {
+ standardInput = System.in
+} \ No newline at end of file
diff --git a/testbench/conf/integration.conf b/testbench/conf/integration.conf
new file mode 100644
index 00000000..39056996
--- /dev/null
+++ b/testbench/conf/integration.conf
@@ -0,0 +1,17 @@
+[libeufin-bank]
+SUGGESTED_WITHDRAWAL_EXCHANGE = https://exchange.example.com
+ALLOW_REGISTRATION = yes
+ALLOW_ACCOUNT_DELETION = yes
+allow_conversion = YES
+FIAT_CURRENCY = EUR
+tan_sms = libeufin-tan-file.sh
+tan_email = libeufin-tan-fail.sh
+
+[libeufin-bankdb-postgres]
+CONFIG = postgresql:///libeufincheck
+
+[nexus-ebics]
+currency = EUR
+
+[nexus-postgres]
+CONFIG = postgres:///libeufincheck
diff --git a/testbench/conf/mini.conf b/testbench/conf/mini.conf
new file mode 100644
index 00000000..5244dae2
--- /dev/null
+++ b/testbench/conf/mini.conf
@@ -0,0 +1,2 @@
+[libeufin-bankdb-postgres]
+CONFIG = postgresql:///libeufincheck \ No newline at end of file
diff --git a/testbench/conf/netzbon.conf b/testbench/conf/netzbon.conf
new file mode 100644
index 00000000..00b140e6
--- /dev/null
+++ b/testbench/conf/netzbon.conf
@@ -0,0 +1,28 @@
+[nexus-ebics]
+CURRENCY = CHF
+
+# Bank
+HOST_BASE_URL = https://ebics.postfinance.ch/ebics/ebics.aspx
+BANK_DIALECT = postfinance
+
+# EBICS IDs
+HOST_ID = PFEBICS
+USER_ID = 5183101
+PARTNER_ID = 51831
+
+
+BANK_PUBLIC_KEYS_FILE = test/netzbon/bank-keys.json
+CLIENT_PRIVATE_KEYS_FILE = test/netzbon/client-keys.json
+
+IBAN = CH4009000000160948810
+BIC = POFICHBEXXX
+NAME = Genossenschaft Netz Soziale Oekonomie
+
+[nexus-fetch]
+FREQUENCY = 5s
+
+[nexus-submit]
+FREQUENCY = 5s
+
+[nexus-postgres]
+CONFIG = postgres:///libeufincheck
diff --git a/testbench/conf/postfinance.conf b/testbench/conf/postfinance.conf
new file mode 100644
index 00000000..a5cb15ad
--- /dev/null
+++ b/testbench/conf/postfinance.conf
@@ -0,0 +1,27 @@
+[nexus-ebics]
+currency = CHF
+
+# Bank
+HOST_BASE_URL = https://isotest.postfinance.ch/ebicsweb/ebicsweb
+BANK_DIALECT = postfinance
+
+# EBICS IDs
+HOST_ID = PFEBICS
+USER_ID = PFC00563
+PARTNER_ID = PFC00563
+
+# Key files
+BANK_PUBLIC_KEYS_FILE = test/postfinance/bank-keys.json
+CLIENT_PRIVATE_KEYS_FILE = test/postfinance/client-keys.json
+
+#IBAN = CH2989144971918294289
+IBAN = CH7789144474425692816
+
+[nexus-fetch]
+FREQUENCY = 5s
+
+[nexus-submit]
+FREQUENCY = 5s
+
+[nexus-postgres]
+CONFIG = postgres:///libeufincheck
diff --git a/testbench/src/main/kotlin/Main.kt b/testbench/src/main/kotlin/Main.kt
new file mode 100644
index 00000000..b1c549c2
--- /dev/null
+++ b/testbench/src/main/kotlin/Main.kt
@@ -0,0 +1,229 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2023 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.testbench
+
+import tech.libeufin.nexus.Database as NexusDb
+import tech.libeufin.nexus.*
+import tech.libeufin.bank.*
+import tech.libeufin.common.*
+import com.github.ajalt.clikt.core.*
+import com.github.ajalt.clikt.parameters.arguments.*
+import com.github.ajalt.clikt.parameters.types.*
+import com.github.ajalt.clikt.testing.*
+import io.ktor.client.*
+import io.ktor.client.engine.cio.*
+import kotlin.test.*
+import java.io.File
+import java.nio.file.*
+import java.time.Instant
+import kotlinx.coroutines.runBlocking
+import io.ktor.client.request.*
+import kotlin.io.path.*
+
+fun randBytes(lenght: Int): ByteArray {
+ val bytes = ByteArray(lenght)
+ kotlin.random.Random.nextBytes(bytes)
+ return bytes
+}
+
+val nexusCmd = LibeufinNexusCommand()
+val client = HttpClient(CIO)
+
+fun step(name: String) {
+ println("\u001b[35m$name\u001b[0m")
+}
+
+fun ask(question: String): String? {
+ print("\u001b[;1m$question\u001b[0m")
+ System.out.flush()
+ return readlnOrNull()
+}
+
+fun CliktCommandTestResult.assertOk(msg: String? = null) {
+ println("$output")
+ assertEquals(0, statusCode, msg)
+}
+
+fun CliktCommandTestResult.assertErr(msg: String? = null) {
+ println("$output")
+ assertEquals(1, statusCode, msg)
+}
+
+enum class Kind {
+ postfinance,
+ netzbon
+}
+
+class Cli : CliktCommand("Run integration tests on banks provider") {
+ val kind: Kind by argument().enum<Kind>()
+ override fun run() {
+ val name = kind.name
+ step("Test init $name")
+
+ runBlocking {
+ Path("test/$name").createDirectories()
+ val conf = "conf/$name.conf"
+ val log = "DEBUG"
+ val flags = " -c $conf -L $log"
+ val ebicsFlags = "$flags --transient --debug-ebics test/$name"
+ val cfg = loadConfig(conf)
+
+ val clientKeysPath = Path(cfg.requireString("nexus-ebics", "client_private_keys_file"))
+ val bankKeysPath = Path(cfg.requireString("nexus-ebics", "bank_public_keys_file"))
+
+ var hasClientKeys = clientKeysPath.exists()
+ var hasBankKeys = bankKeysPath.exists()
+
+ if (ask("Reset DB ? y/n>") == "y") nexusCmd.test("dbinit -r $flags").assertOk()
+ else nexusCmd.test("dbinit $flags").assertOk()
+ val nexusDb = NexusDb("postgresql:///libeufincheck")
+
+ when (kind) {
+ Kind.postfinance -> {
+ if (hasClientKeys || hasBankKeys) {
+ if (ask("Reset keys ? y/n>") == "y") {
+ if (hasClientKeys) clientKeysPath.deleteIfExists()
+ if (hasBankKeys) bankKeysPath.deleteIfExists()
+ hasClientKeys = false
+ hasBankKeys = false
+ }
+ }
+
+ if (!hasClientKeys) {
+ step("Test INI order")
+ ask("Got to https://isotest.postfinance.ch/corporates/user/settings/ebics and click on 'Reset EBICS user'.\nPress Enter when done>")
+ nexusCmd.test("ebics-setup $flags")
+ .assertErr("ebics-setup should failed the first time")
+ }
+
+ if (!hasBankKeys) {
+ step("Test HIA order")
+ ask("Got to https://isotest.postfinance.ch/corporates/user/settings/ebics and click on 'Activate EBICS user'.\nPress Enter when done>")
+ nexusCmd.test("ebics-setup --auto-accept-keys $flags")
+ .assertOk("ebics-setup should succeed the second time")
+ }
+
+ val payto = "payto://iban/CH2989144971918294289?receiver-name=Test"
+
+ step("Test fetch transactions")
+ nexusCmd.test("ebics-fetch $ebicsFlags --pinned-start 2022-01-01").assertOk()
+
+ while (true) {
+ when (ask("Run 'fetch', 'submit', 'tx', 'txs', 'logs', 'ack' or 'exit'>")) {
+ "fetch" -> {
+ step("Fetch new transactions")
+ nexusCmd.test("ebics-fetch $ebicsFlags").assertOk()
+ }
+ "tx" -> {
+ step("Test submit one transaction")
+ nexusDb.initiatedPaymentCreate(InitiatedPayment(
+ amount = TalerAmount("CFH:42"),
+ creditPaytoUri = payto,
+ wireTransferSubject = "single transaction test",
+ initiationTime = Instant.now(),
+ requestUid = Base32Crockford.encode(randBytes(16))
+ ))
+ nexusCmd.test("ebics-submit $ebicsFlags").assertOk()
+ }
+ "txs" -> {
+ step("Test submit many transaction")
+ repeat(4) {
+ nexusDb.initiatedPaymentCreate(InitiatedPayment(
+ amount = TalerAmount("CFH:${100L+it}"),
+ creditPaytoUri = payto,
+ wireTransferSubject = "multi transaction test $it",
+ initiationTime = Instant.now(),
+ requestUid = Base32Crockford.encode(randBytes(16))
+ ))
+ }
+ nexusCmd.test("ebics-submit $ebicsFlags").assertOk()
+ }
+ "submit" -> {
+ step("Submit pending transactions")
+ nexusCmd.test("ebics-submit $ebicsFlags").assertOk()
+ }
+ "logs" -> {
+ step("Fetch logs")
+ nexusCmd.test("ebics-fetch $ebicsFlags --only-logs").assertOk()
+ }
+ "ack" -> {
+ step("Fetch ack")
+ nexusCmd.test("ebics-fetch $ebicsFlags --only-ack").assertOk()
+ }
+ "exit" -> break
+ }
+ }
+ }
+ Kind.netzbon -> {
+ if (!hasClientKeys)
+ throw Exception("Clients keys are required to run netzbon tests")
+
+ if (!hasBankKeys) {
+ step("Test HIA order")
+ nexusCmd.test("ebics-setup --auto-accept-keys $flags").assertOk("ebics-setup should succeed the second time")
+ }
+
+ step("Test fetch transactions")
+ nexusCmd.test("ebics-fetch $ebicsFlags --pinned-start 2022-01-01").assertOk()
+
+ while (true) {
+ when (ask("Run 'fetch', 'submit', 'logs', 'ack' or 'exit'>")) {
+ "fetch" -> {
+ step("Fetch new transactions")
+ nexusCmd.test("ebics-fetch $ebicsFlags").assertOk()
+ }
+ "submit" -> {
+ step("Submit pending transactions")
+ nexusCmd.test("ebics-submit $ebicsFlags").assertOk()
+ }
+ "tx" -> {
+ step("Submit new transaction")
+ // TODO interactive payment editor
+ nexusDb.initiatedPaymentCreate(InitiatedPayment(
+ amount = TalerAmount("CFH:1.1"),
+ creditPaytoUri = "payto://iban/CH6208704048981247126?receiver-name=Grothoff%20Hans",
+ wireTransferSubject = "single transaction test",
+ initiationTime = Instant.now(),
+ requestUid = Base32Crockford.encode(randBytes(16))
+ ))
+ nexusCmd.test("ebics-submit $ebicsFlags").assertOk()
+ }
+ "logs" -> {
+ step("Fetch logs")
+ nexusCmd.test("ebics-fetch $ebicsFlags --only-logs").assertOk()
+ }
+ "ack" -> {
+ step("Fetch ack")
+ nexusCmd.test("ebics-fetch $ebicsFlags --only-ack").assertOk()
+ }
+ "exit" -> break
+ }
+ }
+ }
+ }
+ }
+
+ step("Test succeed")
+ }
+}
+
+fun main(args: Array<String>) {
+ Cli().main(args)
+}
diff --git a/testbench/src/test/kotlin/IntegrationTest.kt b/testbench/src/test/kotlin/IntegrationTest.kt
new file mode 100644
index 00000000..87f75baa
--- /dev/null
+++ b/testbench/src/test/kotlin/IntegrationTest.kt
@@ -0,0 +1,325 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2023 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/>
+ */
+
+import org.junit.Test
+import tech.libeufin.bank.*
+import tech.libeufin.nexus.*
+import tech.libeufin.nexus.Database as NexusDb
+import tech.libeufin.bank.db.AccountDAO.*
+import tech.libeufin.common.*
+import java.io.File
+import java.time.Instant
+import java.util.Arrays
+import java.sql.SQLException
+import kotlinx.coroutines.runBlocking
+import com.github.ajalt.clikt.testing.test
+import com.github.ajalt.clikt.core.CliktCommand
+import org.postgresql.jdbc.PgConnection
+import kotlin.test.*
+import io.ktor.client.*
+import io.ktor.client.engine.cio.*
+import io.ktor.client.plugins.*
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
+import io.ktor.http.HttpStatusCode
+
+fun CliktCommand.run(cmd: String) {
+ val result = test(cmd)
+ if (result.statusCode != 0)
+ throw Exception(result.output)
+ println(result.output)
+}
+
+fun HttpResponse.assertNoContent() {
+ assertEquals(HttpStatusCode.NoContent, this.status)
+}
+
+fun randBytes(lenght: Int): ByteArray {
+ val bytes = ByteArray(lenght)
+ kotlin.random.Random.nextBytes(bytes)
+ return bytes
+}
+
+fun server(lambda: () -> Unit) {
+ // Start the HTTP server in another thread
+ kotlin.concurrent.thread(isDaemon = true) {
+ lambda()
+ }
+ // Wait for the HTTP server to be up
+ runBlocking {
+ HttpClient(CIO) {
+ install(HttpRequestRetry) {
+ maxRetries = 10
+ constantDelay(200, 100)
+ }
+ }.get("http://0.0.0.0:8080/config")
+ }
+
+}
+
+fun setup(lambda: suspend (NexusDb) -> Unit) {
+ try {
+ runBlocking {
+ NexusDb("postgresql:///libeufincheck").use {
+ lambda(it)
+ }
+ }
+ } finally {
+ engine?.stop(0, 0) // Stop http server if started
+ }
+}
+
+inline fun assertException(msg: String, lambda: () -> Unit) {
+ try {
+ lambda()
+ throw Exception("Expected failure: $msg")
+ } catch (e: Exception) {
+ assert(e.message!!.startsWith(msg)) { "${e.message}" }
+ }
+}
+
+class IntegrationTest {
+ val nexusCmd = LibeufinNexusCommand()
+ val bankCmd = LibeufinBankCommand();
+ val client = HttpClient(CIO)
+
+ @Test
+ fun mini() {
+ bankCmd.run("dbinit -c conf/mini.conf -r")
+ bankCmd.run("passwd admin password -c conf/mini.conf")
+ bankCmd.run("dbinit -c conf/mini.conf") // Indempotent
+
+ server {
+ bankCmd.run("serve -c conf/mini.conf")
+ }
+
+ setup { _ ->
+ // Check bank is running
+ client.get("http://0.0.0.0:8080/public-accounts").assertNoContent()
+ }
+ }
+
+ @Test
+ fun errors() {
+ nexusCmd.run("dbinit -c conf/integration.conf -r")
+ bankCmd.run("dbinit -c conf/integration.conf -r")
+ bankCmd.run("passwd admin password -c conf/integration.conf")
+
+ suspend fun checkCount(db: NexusDb, nbIncoming: Int, nbBounce: Int, nbTalerable: Int) {
+ db.conn { conn ->
+ conn.prepareStatement("SELECT count(*) FROM incoming_transactions").oneOrNull {
+ assertEquals(nbIncoming, it.getInt(1))
+ }
+ conn.prepareStatement("SELECT count(*) FROM bounced_transactions").oneOrNull {
+ assertEquals(nbBounce, it.getInt(1))
+ }
+ conn.prepareStatement("SELECT count(*) FROM talerable_incoming_transactions").oneOrNull {
+ assertEquals(nbTalerable, it.getInt(1))
+ }
+ }
+ }
+
+ setup { db ->
+ val userPayTo = IbanPayTo(genIbanPaytoUri())
+ val fiatPayTo = IbanPayTo(genIbanPaytoUri())
+
+ // Load conversion setup manually as the server would refuse to start without an exchange account
+ val sqlProcedures = File("../database-versioning/libeufin-conversion-setup.sql")
+ db.conn {
+ it.execSQLUpdate(sqlProcedures.readText())
+ it.execSQLUpdate("SET search_path TO libeufin_nexus;")
+ }
+
+ val reservePub = randBytes(32)
+ val payment = IncomingPayment(
+ amount = TalerAmount("EUR:10"),
+ debitPaytoUri = userPayTo.canonical,
+ wireTransferSubject = "Error test ${Base32Crockford.encode(reservePub)}",
+ executionTime = Instant.now(),
+ bankId = "error"
+ )
+
+ assertException("ERROR: cashin failed: missing exchange account") {
+ ingestIncomingPayment(db, payment)
+ }
+
+ // Create exchange account
+ bankCmd.run("create-account -c conf/integration.conf -u exchange -p password --name 'Mr Money' --exchange")
+
+ assertException("ERROR: cashin currency conversion failed: missing conversion rates") {
+ ingestIncomingPayment(db, payment)
+ }
+
+ // Start server
+ server {
+ bankCmd.run("serve -c conf/integration.conf")
+ }
+
+ // Set conversion rates
+ client.post("http://0.0.0.0:8080/conversion-info/conversion-rate") {
+ basicAuth("admin", "password")
+ json {
+ "cashin_ratio" to "0.8"
+ "cashin_fee" to "KUDOS:0.02"
+ "cashin_tiny_amount" to "KUDOS:0.01"
+ "cashin_rounding_mode" to "nearest"
+ "cashin_min_amount" to "EUR:0"
+ "cashout_ratio" to "1.25"
+ "cashout_fee" to "EUR:0.003"
+ "cashout_tiny_amount" to "EUR:0.00000001"
+ "cashout_rounding_mode" to "zero"
+ "cashout_min_amount" to "KUDOS:0.1"
+ }
+ }.assertNoContent()
+
+ assertException("ERROR: cashin failed: admin balance insufficient") {
+ db.registerTalerableIncoming(payment, reservePub)
+ }
+
+ // Allow admin debt
+ bankCmd.run("edit-account admin --debit_threshold KUDOS:100 -c conf/integration.conf")
+
+ // Too small amount
+ checkCount(db, 0, 0, 0)
+ ingestIncomingPayment(db, payment.copy(
+ amount = TalerAmount("EUR:0.01"),
+ ))
+ checkCount(db, 1, 1, 0)
+ client.get("http://0.0.0.0:8080/accounts/exchange/transactions") {
+ basicAuth("exchange", "password")
+ }.assertNoContent()
+
+ // Check success
+ ingestIncomingPayment(db, IncomingPayment(
+ amount = TalerAmount("EUR:10"),
+ debitPaytoUri = userPayTo.canonical,
+ wireTransferSubject = "Success ${Base32Crockford.encode(randBytes(32))}",
+ executionTime = Instant.now(),
+ bankId = "success"
+ ))
+ checkCount(db, 2, 1, 1)
+ client.get("http://0.0.0.0:8080/accounts/exchange/transactions") {
+ basicAuth("exchange", "password")
+ }.assertOkJson<BankAccountTransactionsResponse>()
+
+ // TODO check double insert cashin with different subject
+ }
+ }
+
+ @Test
+ fun conversion() {
+ nexusCmd.run("dbinit -c conf/integration.conf -r")
+ bankCmd.run("dbinit -c conf/integration.conf -r")
+ bankCmd.run("passwd admin password -c conf/integration.conf")
+ bankCmd.run("edit-account admin --debit_threshold KUDOS:1000 -c conf/integration.conf")
+ bankCmd.run("create-account -c conf/integration.conf -u exchange -p password --name 'Mr Money' --exchange")
+ nexusCmd.run("dbinit -c conf/integration.conf") // Idempotent
+ bankCmd.run("dbinit -c conf/integration.conf") // Idempotent
+
+ server {
+ bankCmd.run("serve -c conf/integration.conf")
+ }
+
+ setup { db ->
+ val userPayTo = IbanPayTo(genIbanPaytoUri())
+ val fiatPayTo = IbanPayTo(genIbanPaytoUri())
+
+ // Create user
+ client.post("http://0.0.0.0:8080/accounts") {
+ basicAuth("admin", "password")
+ json {
+ "username" to "customer"
+ "password" to "password"
+ "name" to "JohnSmith"
+ "internal_payto_uri" to userPayTo
+ "cashout_payto_uri" to fiatPayTo
+ "debit_threshold" to "KUDOS:100"
+ "contact_data" to obj {
+ "phone" to "+99"
+ }
+ }
+ }.assertOkJson<RegisterAccountResponse>()
+
+ // Set conversion rates
+ client.post("http://0.0.0.0:8080/conversion-info/conversion-rate") {
+ basicAuth("admin", "password")
+ json {
+ "cashin_ratio" to "0.8"
+ "cashin_fee" to "KUDOS:0.02"
+ "cashin_tiny_amount" to "KUDOS:0.01"
+ "cashin_rounding_mode" to "nearest"
+ "cashin_min_amount" to "EUR:0"
+ "cashout_ratio" to "1.25"
+ "cashout_fee" to "EUR:0.003"
+ "cashout_tiny_amount" to "EUR:0.00000001"
+ "cashout_rounding_mode" to "zero"
+ "cashout_min_amount" to "KUDOS:0.1"
+ }
+ }.assertNoContent()
+
+ // Cashin
+ repeat(3) { i ->
+ val reservePub = randBytes(32);
+ val amount = TalerAmount("EUR:${20+i}")
+ val subject = "cashin test $i: ${Base32Crockford.encode(reservePub)}"
+ ingestIncomingPayment(db,
+ IncomingPayment(
+ amount = amount,
+ debitPaytoUri = userPayTo.canonical,
+ wireTransferSubject = subject,
+ executionTime = Instant.now(),
+ bankId = Base32Crockford.encode(reservePub)
+ )
+ )
+ val converted = client.get("http://0.0.0.0:8080/conversion-info/cashin-rate?amount_debit=EUR:${20 + i}")
+ .assertOkJson<ConversionResponse>().amount_credit
+ client.get("http://0.0.0.0:8080/accounts/exchange/transactions") {
+ basicAuth("exchange", "password")
+ }.assertOkJson<BankAccountTransactionsResponse> {
+ val tx = it.transactions.first()
+ assertEquals(subject, tx.subject)
+ assertEquals(converted, tx.amount)
+ }
+ client.get("http://0.0.0.0:8080/accounts/exchange/taler-wire-gateway/history/incoming") {
+ basicAuth("exchange", "password")
+ }.assertOkJson<IncomingHistory> {
+ val tx = it.incoming_transactions.first()
+ assertEquals(converted, tx.amount)
+ assert(Arrays.equals(reservePub, tx.reserve_pub.raw))
+ }
+ }
+
+ // Cashout
+ repeat(3) { i ->
+ val requestUid = randBytes(32);
+ val amount = TalerAmount("KUDOS:${10+i}")
+ val convert = client.get("http://0.0.0.0:8080/conversion-info/cashout-rate?amount_debit=$amount")
+ .assertOkJson<ConversionResponse>().amount_credit;
+ client.post("http://0.0.0.0:8080/accounts/customer/cashouts") {
+ basicAuth("customer", "password")
+ json {
+ "request_uid" to ShortHashCode(requestUid)
+ "amount_debit" to amount
+ "amount_credit" to convert
+ }
+ }.assertOkJson<CashoutResponse>()
+ }
+ }
+ }
+}