gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[libeufin] branch master updated (e732b8c1 -> a46b61be)


From: gnunet
Subject: [libeufin] branch master updated (e732b8c1 -> a46b61be)
Date: Wed, 20 Mar 2024 18:34:10 +0100

This is an automated email from the git hooks/post-receive script.

antoine pushed a change to branch master
in repository libeufin.

    from e732b8c1 Add gc logic
     new 97963f2b Make soft deleted account's information and operations 
accessible to admin
     new a047fb5c Add GC command
     new a46b61be Share duration parsing with libeufin-nexus

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 API_CHANGES.md                                     |   2 +
 bank/src/main/kotlin/tech/libeufin/bank/Config.kt  |  11 +-
 .../main/kotlin/tech/libeufin/bank/Constants.kt    |   2 +-
 bank/src/main/kotlin/tech/libeufin/bank/Main.kt    |  21 +++-
 .../main/kotlin/tech/libeufin/bank/TalerMessage.kt |  11 +-
 .../kotlin/tech/libeufin/bank/api/CoreBankApi.kt   |   4 +-
 .../kotlin/tech/libeufin/bank/db/AccountDAO.kt     |  18 ++-
 .../kotlin/tech/libeufin/bank/db/CashoutDAO.kt     |   6 +-
 .../src/main/kotlin/tech/libeufin/bank/db/GcDAO.kt |  18 ++-
 .../kotlin/tech/libeufin/bank/db/TransactionDAO.kt |   2 +-
 .../kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt  |   4 +-
 bank/src/test/kotlin/CoreBankApiTest.kt            | 138 ++++++++++++++++-----
 bank/src/test/kotlin/GcTest.kt                     |  12 +-
 common/src/main/kotlin/TalerConfig.kt              |  31 ++++-
 common/src/test/kotlin/ConfigTest.kt               |  47 +++++++
 contrib/bank.conf                                  |  11 +-
 .../main/kotlin/tech/libeufin/nexus/EbicsFetch.kt  |  18 ++-
 .../main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt |  22 ++--
 nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt  |  74 -----------
 nexus/src/test/kotlin/ConfigLoading.kt             |  43 -------
 testbench/src/test/kotlin/IntegrationTest.kt       |   2 +
 21 files changed, 289 insertions(+), 208 deletions(-)
 create mode 100644 common/src/test/kotlin/ConfigTest.kt
 delete mode 100644 nexus/src/test/kotlin/ConfigLoading.kt

diff --git a/API_CHANGES.md b/API_CHANGES.md
index da085bb7..ff80790f 100644
--- a/API_CHANGES.md
+++ b/API_CHANGES.md
@@ -47,6 +47,8 @@ This files contains all the API changes for the current 
release:
 - GET /public-accounts: add row_id field
 - GET /config: new bank_name field for the bank name
 - POST /accounts/USERNAME/transactions: new request_uid field for idempotency 
and new idempotency error
+- GET /accounts: new status field
+- GET /accounts/USERNAME: new status field
 
 ## bank cli
 
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Config.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Config.kt
index 9c252b8f..54f2c44b 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Config.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Config.kt
@@ -24,6 +24,7 @@ import org.slf4j.Logger
 import org.slf4j.LoggerFactory
 import tech.libeufin.common.*
 import java.nio.file.Path
+import java.time.Duration
 
 private val logger: Logger = LoggerFactory.getLogger("libeufin-bank")
 
@@ -47,7 +48,10 @@ data class BankConfig(
     val spaPath: Path?,
     val tanChannels: Map<TanChannel, Pair<Path, Map<String, String>>>,
     val payto: BankPaytoCtx,
-    val wireMethod: WireMethod
+    val wireMethod: WireMethod,
+    val gcAbortAfter: Duration,
+    val gcCleanAfter: Duration,
+    val gcDeleteAfter: Duration
 )
 
 @Serializable
@@ -141,7 +145,10 @@ fun TalerConfig.loadBankConfig(): BankConfig {
         fiatCurrencySpec = fiatCurrencySpec,
         tanChannels = tanChannels,
         payto = payto,
-        wireMethod = method
+        wireMethod = method,
+        gcAbortAfter = requireDuration("libeufin-bank", "gc_abort_after"),
+        gcCleanAfter = requireDuration("libeufin-bank", "gc_clean_after"),
+        gcDeleteAfter = requireDuration("libeufin-bank", "gc_delete_after"),
     )
 }
 
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt
index 7a192308..15fe5be1 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt
@@ -40,7 +40,7 @@ const val IBAN_ALLOCATION_RETRY_COUNTER: Int = 5
 const val MAX_BODY_LENGTH: Long = 4 * 1024 // 4kB
 
 // API version  
-const val COREBANK_API_VERSION: String = "4:4:0"
+const val COREBANK_API_VERSION: String = "4:5:0"
 const val CONVERSION_API_VERSION: String = "0:0:0"
 const val INTEGRATION_API_VERSION: String = "2:0:2"
 const val WIRE_GATEWAY_API_VERSION: String = "0:2:0"
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
index ecdc5810..0663e49e 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -58,6 +58,7 @@ import java.net.InetAddress
 import java.sql.SQLException
 import java.util.zip.DataFormatException
 import java.util.zip.Inflater
+import java.time.Instant
 import kotlin.io.path.Path
 import kotlin.io.path.exists
 import kotlin.io.path.readText
@@ -505,10 +506,28 @@ class CreateAccount : CliktCommand(
     }
 }
 
+class GC : CliktCommand(
+    "Run garbage collection: abort expired operations and clean expired data",
+    name = "gc"
+) {
+    private val common by CommonOption()
+ 
+    override fun run() = cliCmd(logger, common.log) {
+        val cfg = talerConfig(common.config)
+        val ctx = cfg.loadBankConfig() 
+        val dbCfg = cfg.loadDbConfig()
+
+        Database(dbCfg.dbConnStr, ctx.regionalCurrency, ctx.fiatCurrency).use 
{ db ->
+            logger.info("Run garbage collection")
+            db.gc.collect(Instant.now(), ctx.gcAbortAfter, ctx.gcCleanAfter, 
ctx.gcDeleteAfter)
+        }
+    }
+}
+
 class LibeufinBankCommand : CliktCommand() {
     init {
         versionOption(getVersion())
-        subcommands(ServeBank(), BankDbInit(), CreateAccount(), EditAccount(), 
ChangePw(), CliConfigCmd(BANK_CONFIG_SOURCE))
+        subcommands(ServeBank(), BankDbInit(), CreateAccount(), EditAccount(), 
ChangePw(), GC(), CliConfigCmd(BANK_CONFIG_SOURCE))
     }
 
     override fun run() = Unit
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
index ce459baa..b9bfcbec 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
@@ -55,6 +55,11 @@ enum class WithdrawalStatus {
     confirmed
 }
 
+enum class AccountStatus {
+    active,
+    deleted
+}
+
 enum class RoundingMode {
     zero,
     up,
@@ -350,7 +355,8 @@ data class AccountMinimalData(
     val debit_threshold: TalerAmount,
     val is_public: Boolean,
     val is_taler_exchange: Boolean,
-    val row_id: Long
+    val row_id: Long,
+    val status: AccountStatus
 )
 
 /**
@@ -374,7 +380,8 @@ data class AccountData(
     val cashout_payto_uri: String? = null,
     val tan_channel: TanChannel? = null,
     val is_public: Boolean,
-    val is_taler_exchange: Boolean
+    val is_taler_exchange: Boolean,
+    val status: AccountStatus
 )
 
 @Serializable
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt
index a3bb8265..45689bf0 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt
@@ -422,7 +422,7 @@ private fun Routing.coreBankAccountsApi(db: Database, ctx: 
BankConfig) {
 }
 
 private fun Routing.coreBankTransactionsApi(db: Database, ctx: BankConfig) {
-    auth(db, TokenScope.readonly) {
+    auth(db, TokenScope.readonly, allowAdmin = true) {
         get("/accounts/{USERNAME}/transactions") {
             val params = HistoryParams.extract(call.request.queryParameters)
             val bankAccount = call.bankInfo(db, ctx.payto)
@@ -618,7 +618,7 @@ private fun Routing.coreBankCashoutApi(db: Database, ctx: 
BankConfig) = conditio
             }
         }
     }
-    auth(db, TokenScope.readonly) {
+    auth(db, TokenScope.readonly, allowAdmin = true) {
         get("/accounts/{USERNAME}/cashouts/{CASHOUT_ID}") {
             val id = call.longPath("CASHOUT_ID")
             val cashout = db.cashout.get(id, username) ?: throw notFound(
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt
index 7781d807..a3a5d8e5 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt
@@ -436,7 +436,7 @@ class AccountDAO(private val db: Database) {
              ,is_taler_exchange
             FROM bank_accounts
                 JOIN customers ON customer_id=owning_customer_id
-            WHERE login=? AND deleted_at IS NULL
+            WHERE login=?
         """)
         stmt.setString(1, login)
         stmt.oneOrNull {
@@ -465,10 +465,14 @@ class AccountDAO(private val db: Database) {
                 ,(max_debt).frac AS max_debt_frac
                 ,is_public
                 ,is_taler_exchange
+                ,CASE 
+                    WHEN deleted_at IS NOT NULL THEN 'deleted'
+                    ELSE 'active'
+                END as status
             FROM customers 
                 JOIN bank_accounts
                     ON customer_id=owning_customer_id
-            WHERE login=? AND deleted_at IS NULL
+            WHERE login=?
         """)
         stmt.setString(1, login)
         stmt.oneOrNull {
@@ -492,7 +496,8 @@ class AccountDAO(private val db: Database) {
                 ),
                 debit_threshold = it.getAmount("max_debt", db.bankCurrency),
                 is_public = it.getBoolean("is_public"),
-                is_taler_exchange = it.getBoolean("is_taler_exchange")
+                is_taler_exchange = it.getBoolean("is_taler_exchange"),
+                status = AccountStatus.valueOf(it.getString("status"))
             )
         }
     }
@@ -555,9 +560,13 @@ class AccountDAO(private val db: Database) {
             ,is_taler_exchange
             ,internal_payto_uri
             ,bank_account_id
+            ,CASE 
+                WHEN deleted_at IS NOT NULL THEN 'deleted'
+                ELSE 'active'
+            END as status
             FROM bank_accounts JOIN customers
               ON owning_customer_id = customer_id 
-            WHERE name LIKE ? AND deleted_at IS NULL AND
+            WHERE name LIKE ? AND
             """,
             {
                 setString(1, params.loginFilter)
@@ -580,6 +589,7 @@ class AccountDAO(private val db: Database) {
                 is_public = it.getBoolean("is_public"),
                 is_taler_exchange = it.getBoolean("is_taler_exchange"),
                 payto_uri = it.getBankPayto("internal_payto_uri", "name", ctx),
+                status = AccountStatus.valueOf(it.getString("status"))
             )
         }
 }
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt
index 45da0854..958d2b33 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt
@@ -104,7 +104,7 @@ class CashoutDAO(private val db: Database) {
                 JOIN bank_accounts ON bank_account=bank_account_id
                 JOIN customers ON owning_customer_id=customer_id
                 LEFT JOIN bank_account_transactions ON 
local_transaction=bank_transaction_id
-            WHERE cashout_id=? AND login=? AND deleted_at IS NULL
+            WHERE cashout_id=? AND login=?
         """)
         stmt.setLong(1, id)
         stmt.setString(2, login)
@@ -134,7 +134,7 @@ class CashoutDAO(private val db: Database) {
             FROM cashout_operations
                 JOIN bank_accounts ON bank_account=bank_account_id
                 JOIN customers ON owning_customer_id=customer_id
-            WHERE deleted_at IS NULL AND
+            WHERE
         """) {
             GlobalCashoutInfo(
                 cashout_id = it.getLong("cashout_id"),
@@ -150,7 +150,7 @@ class CashoutDAO(private val db: Database) {
             FROM cashout_operations
                 JOIN bank_accounts ON bank_account=bank_account_id
                 JOIN customers ON owning_customer_id=customer_id
-            WHERE login = ? AND deleted_at IS NULL AND
+            WHERE login = ? AND
         """, 
             bind = { 
                 setString(1, login)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/GcDAO.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/db/GcDAO.kt
index 9f5e9431..7e1c7a08 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/GcDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/GcDAO.kt
@@ -23,24 +23,20 @@ import tech.libeufin.bank.*
 import tech.libeufin.common.*
 import tech.libeufin.common.crypto.*
 import java.time.Instant
-import java.time.ZoneOffset
-import java.time.LocalDateTime
-import java.time.temporal.TemporalAmount
-import java.time.chrono.ChronoLocalDateTime
+import java.time.Duration
 
 /** Data access logic for garbage collection */
 class GcDAO(private val db: Database) {
     /** Run garbage collection  */
     suspend fun collect(
         now: Instant,
-        abortAfter: TemporalAmount,
-        cleanAfter: TemporalAmount,
-        deleteAfter: TemporalAmount
+        abortAfter: Duration,
+        cleanAfter: Duration,
+        deleteAfter: Duration
     ) = db.conn { conn ->
-        val dateTime = LocalDateTime.ofInstant(now, ZoneOffset.UTC)
-        val abortAfterMicro = 
dateTime.minus(abortAfter).toInstant(ZoneOffset.UTC).micros()
-        val cleanAfterMicro = 
dateTime.minus(cleanAfter).toInstant(ZoneOffset.UTC).micros()
-        val deleteAfterMicro = 
dateTime.minus(deleteAfter).toInstant(ZoneOffset.UTC).micros()
+        val abortAfterMicro = now.minus(abortAfter).micros()
+        val cleanAfterMicro = now.minus(cleanAfter).micros()
+        val deleteAfterMicro = now.minus(deleteAfter).micros()
         
         // Abort pending operations
         conn.prepareStatement(
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt
index e82f0ba2..cf5ef9bc 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt
@@ -165,7 +165,7 @@ class TransactionDAO(private val db: Database) {
             FROM bank_account_transactions
                 JOIN bank_accounts ON 
bank_account_transactions.bank_account_id=bank_accounts.bank_account_id
                 JOIN customers ON customer_id=owning_customer_id 
-               WHERE bank_transaction_id=? AND login=? AND deleted_at IS NULL
+               WHERE bank_transaction_id=? AND login=?
         """)
         stmt.setLong(1, rowId)
         stmt.setString(2, login)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt
index 3a41c0d8..4b069339 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt
@@ -189,7 +189,7 @@ class WithdrawalDAO(private val db: Database) {
             FROM taler_withdrawal_operations
                 JOIN bank_accounts ON wallet_bank_account=bank_account_id
                 JOIN customers ON customer_id=owning_customer_id
-            WHERE withdrawal_uuid=? AND deleted_at IS NULL
+            WHERE withdrawal_uuid=?
         """)
         stmt.setObject(1, uuid)
         stmt.oneOrNull { it.getString(1) }
@@ -250,7 +250,7 @@ class WithdrawalDAO(private val db: Database) {
                     FROM taler_withdrawal_operations
                         JOIN bank_accounts ON 
wallet_bank_account=bank_account_id
                         JOIN customers ON customer_id=owning_customer_id
-                    WHERE withdrawal_uuid=? AND deleted_at IS NULL
+                    WHERE withdrawal_uuid=?
                 """)
                 stmt.setObject(1, uuid)
                 stmt.oneOrNull {
diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt 
b/bank/src/test/kotlin/CoreBankApiTest.kt
index 0bd13207..275ed911 100644
--- a/bank/src/test/kotlin/CoreBankApiTest.kt
+++ b/bank/src/test/kotlin/CoreBankApiTest.kt
@@ -425,11 +425,82 @@ class CoreBankAccountsApiTest {
             .assertChallenge()
             .assertNoContent()
         // Account no longer exists
+        client.deleteA("/accounts/john").assertUnauthorized()
         client.delete("/accounts/john") {
             pwAuth("admin")
         }.assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT)
     }
 
+
+
+    @Test
+    fun softDelete() = bankSetup { db -> 
+        // Create all kind of operations
+        val token = client.postA("/accounts/customer/token") {
+            json { "scope" to "readonly" }
+        }.assertOkJson<TokenSuccessResponse>().access_token
+        val tx_id = client.postA("/accounts/customer/transactions") {
+            json {
+                "payto_uri" to "$exchangePayto?message=payout"
+                "amount" to "KUDOS:0.3"
+            }
+        }.assertOkJson<TransactionCreateResponse>().row_id
+        val withdrawal_id = client.postA("/accounts/customer/withdrawals") {
+            json { "amount" to "KUDOS:9.0" } 
+        }.assertOkJson<BankAccountCreateWithdrawalResponse>().withdrawal_id
+        fillCashoutInfo("customer")
+        val cashout_id = client.postA("/accounts/customer/cashouts") {
+            json {
+                "request_uid" to ShortHashCode.rand()
+                "amount_debit" to "KUDOS:1"
+                "amount_credit" to convert("KUDOS:1")
+            }
+        }.assertOkJson<CashoutResponse>().cashout_id
+        fillTanInfo("customer")
+        val tan_id = client.postA("/accounts/customer/transactions") {
+            json {
+                "payto_uri" to "$exchangePayto?message=payout"
+                "amount" to "KUDOS:0.3"
+            }
+        }.assertAcceptedJson<TanChallenge>().challenge_id
+
+        // Delete account
+        tx("merchant", "KUDOS:1", "customer")
+        assertBalance("customer", "+KUDOS:0")
+        client.deleteA("/accounts/customer")
+            .assertChallenge()
+            .assertNoContent()
+        
+        // Check account can no longer login
+        client.delete("/accounts/customer/token") {
+            headers["Authorization"] = "Bearer secret-token:$token"
+        }.assertUnauthorized()
+        
client.getA("/accounts/customer/transactions/$tx_id").assertUnauthorized()
+        
client.getA("/accounts/customer/cashouts/$cashout_id").assertUnauthorized()
+        
client.postA("/accounts/customer/withdrawals/$withdrawal_id/confirm").assertUnauthorized()
+
+        // But admin can still see existing operations
+        client.get("/accounts/customer/transactions/$tx_id") {
+            pwAuth("admin")
+        }.assertOkJson<BankAccountTransactionInfo>()
+        client.get("/accounts/customer/cashouts/$cashout_id") {
+            pwAuth("admin")
+        }.assertOkJson<CashoutStatusResponse>()
+        client.get("/withdrawals/$withdrawal_id")
+            .assertOkJson<WithdrawalPublicInfo>()
+
+        // GC
+        db.gc.collect(Instant.now(), Duration.ZERO, Duration.ZERO, 
Duration.ZERO)
+        client.get("/accounts/customer/transactions/$tx_id") {
+            pwAuth("admin")
+        }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
+        client.get("/accounts/customer/cashouts/$cashout_id") {
+            pwAuth("admin")
+        }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
+        client.get("/withdrawals/$withdrawal_id")
+            .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
+    }
+
     // Test admin-only account deletion
     @Test
     fun deleteRestricted() = bankSetup(conf = "test_restrict.conf") { _ -> 
@@ -686,19 +757,32 @@ class CoreBankAccountsApiTest {
 
     // GET /public-accounts and GET /accounts
     @Test
-    fun list() = bankSetup(conf = "test_no_conversion.conf") { _ -> 
+    fun list() = bankSetup(conf = "test_no_conversion.conf") { db -> 
         authRoutine(HttpMethod.Get, "/accounts", requireAdmin = true)
         // Remove default accounts
-        listOf("merchant", "exchange", "customer").forEach {
+        val defaultAccounts = listOf("merchant", "exchange", "customer")
+        defaultAccounts.forEach {
             client.delete("/accounts/$it") {
                 pwAuth("admin")
             }.assertNoContent()
         }
+        client.get("/accounts") {
+            pwAuth("admin")
+        }.assertOkJson<ListBankAccountsResponse> {
+            for (account in it.accounts) {
+                if (defaultAccounts.contains(account.username)) {
+                    assertEquals(AccountStatus.deleted, account.status)
+                } else {
+                    assertEquals(AccountStatus.active, account.status)
+                }
+            }
+        }
+        db.gc.collect(Instant.now(), Duration.ZERO, Duration.ZERO, 
Duration.ZERO)
         // Check error when no public accounts
         client.get("/public-accounts").assertNoContent()
         client.get("/accounts") {
             pwAuth("admin")
-        }.assertOk()
+        }.assertOkJson<ListBankAccountsResponse>()
         
         // Gen some public and private accounts
         repeat(5) {
@@ -712,22 +796,18 @@ class CoreBankAccountsApiTest {
             }.assertOk()
         }
         // All public
-        client.get("/public-accounts").run {
-            assertOk()
-            val obj = json<PublicAccountsResponse>()
-            assertEquals(3, obj.public_accounts.size)
-            obj.public_accounts.forEach {
+        client.get("/public-accounts").assertOkJson<PublicAccountsResponse> {
+            assertEquals(3, it.public_accounts.size)
+            it.public_accounts.forEach {
                 assertEquals(0, it.username.toInt() % 2)
             }
         }
         // All accounts
         client.get("/accounts?delta=10"){
             pwAuth("admin")
-        }.run {
-            assertOk()
-            val obj = json<ListBankAccountsResponse>()
-            assertEquals(6, obj.accounts.size)
-            obj.accounts.forEachIndexed { idx, it ->
+        }.assertOkJson<ListBankAccountsResponse> {
+            assertEquals(6, it.accounts.size)
+            it.accounts.forEachIndexed { idx, it ->
                 if (idx == 0) {
                     assertEquals("admin", it.username)
                 } else {
@@ -738,11 +818,9 @@ class CoreBankAccountsApiTest {
         // Filtering
         client.get("/accounts?filter_name=3"){
             pwAuth("admin")
-        }.run {
-            assertOk()
-            val obj = json<ListBankAccountsResponse>()
-            assertEquals(1, obj.accounts.size)
-            assertEquals("3", obj.accounts[0].username)
+        }.assertOkJson<ListBankAccountsResponse> {
+            assertEquals(1, it.accounts.size)
+            assertEquals("3", it.accounts[0].username)
         }
     }
 
@@ -761,7 +839,7 @@ class CoreBankTransactionsApiTest {
     // GET /transactions
     @Test
     fun history() = bankSetup { _ -> 
-        authRoutine(HttpMethod.Get, "/accounts/merchant/transactions")
+        authRoutine(HttpMethod.Get, "/accounts/merchant/transactions", 
allowAdmin = true)
         historyRoutine<BankAccountTransactionsResponse>(
             url = "/accounts/customer/transactions",
             ids = { it.transactions.map { it.row_id } },
@@ -799,7 +877,7 @@ class CoreBankTransactionsApiTest {
     // GET /transactions/T_ID
     @Test
     fun testById() = bankSetup { _ -> 
-        authRoutine(HttpMethod.Get, "/accounts/merchant/transactions/42")
+        authRoutine(HttpMethod.Get, "/accounts/merchant/transactions/42", 
allowAdmin = true)
 
         // Create transaction
         tx("merchant", "KUDOS:0.3", "exchange", "tx")
@@ -1043,9 +1121,7 @@ class CoreBankWithdrawalApiTest {
         client.postA("/accounts/merchant/withdrawals") {
             json { "amount" to amount}
         }.assertOkJson<BankAccountCreateWithdrawalResponse> {
-            client.get("/withdrawals/${it.withdrawal_id}") {
-                pwAuth("merchant")
-            }.assertOkJson<WithdrawalPublicInfo> {
+            
client.get("/withdrawals/${it.withdrawal_id}").assertOkJson<WithdrawalPublicInfo>
 {
                 assertEquals(amount, it.amount)
             }
         }
@@ -1069,7 +1145,7 @@ class CoreBankWithdrawalApiTest {
         client.postA("/accounts/merchant/withdrawals") {
             json { "amount" to "KUDOS:1" } 
         }.assertOkJson<BankAccountCreateWithdrawalResponse> {
-            val uuid = it.taler_withdraw_uri.split("/").last()
+            val uuid = it.withdrawal_id
 
             // Check err
             client.postA("/accounts/merchant/withdrawals/$uuid/confirm")
@@ -1080,7 +1156,7 @@ class CoreBankWithdrawalApiTest {
         client.postA("/accounts/merchant/withdrawals") {
             json { "amount" to "KUDOS:1" } 
         }.assertOkJson<BankAccountCreateWithdrawalResponse> {
-            val uuid = it.taler_withdraw_uri.split("/").last()
+            val uuid = it.withdrawal_id
             withdrawalSelect(uuid)
 
             // Check OK
@@ -1093,7 +1169,7 @@ class CoreBankWithdrawalApiTest {
         client.postA("/accounts/merchant/withdrawals") {
             json { "amount" to "KUDOS:1" } 
         }.assertOkJson<BankAccountCreateWithdrawalResponse> {
-            val uuid = it.taler_withdraw_uri.split("/").last()
+            val uuid = it.withdrawal_id
             withdrawalSelect(uuid)
             
client.postA("/accounts/merchant/withdrawals/$uuid/abort").assertNoContent()
 
@@ -1106,7 +1182,7 @@ class CoreBankWithdrawalApiTest {
         client.postA("/accounts/merchant/withdrawals") {
             json { "amount" to "KUDOS:5" } 
         }.assertOkJson<BankAccountCreateWithdrawalResponse> {
-            val uuid = it.taler_withdraw_uri.split("/").last()
+            val uuid = it.withdrawal_id
             withdrawalSelect(uuid)
 
             // Send too much money
@@ -1122,7 +1198,7 @@ class CoreBankWithdrawalApiTest {
         client.postA("/accounts/customer/withdrawals") {
             json { "amount" to "KUDOS:1" } 
         }.assertOkJson<BankAccountCreateWithdrawalResponse> {
-            val uuid = it.taler_withdraw_uri.split("/").last()
+            val uuid = it.withdrawal_id
             withdrawalSelect(uuid)
 
             // Check error
@@ -1144,7 +1220,7 @@ class CoreBankWithdrawalApiTest {
         client.postA("/accounts/merchant/withdrawals") {
             json { "amount" to "KUDOS:1" } 
         }.assertOkJson<BankAccountCreateWithdrawalResponse> {
-            val uuid = it.taler_withdraw_uri.split("/").last()
+            val uuid = it.withdrawal_id
             withdrawalSelect(uuid)
 
             client.postA("/accounts/merchant/withdrawals/$uuid/confirm")
@@ -1244,7 +1320,7 @@ class CoreBankCashoutApiTest {
     // GET /accounts/{USERNAME}/cashouts/{CASHOUT_ID}
     @Test
     fun get() = bankSetup { _ ->
-        authRoutine(HttpMethod.Get, "/accounts/merchant/cashouts/42")
+        authRoutine(HttpMethod.Get, "/accounts/merchant/cashouts/42", 
allowAdmin = true)
         fillCashoutInfo("customer")
 
         val amountDebit = TalerAmount("KUDOS:1.5")
@@ -1292,7 +1368,7 @@ class CoreBankCashoutApiTest {
     // GET /accounts/{USERNAME}/cashouts
     @Test
     fun history() = bankSetup { _ ->
-        authRoutine(HttpMethod.Get, "/accounts/merchant/cashouts")
+        authRoutine(HttpMethod.Get, "/accounts/merchant/cashouts", allowAdmin 
= true)
         historyRoutine<Cashouts>(
             url = "/accounts/customer/cashouts",
             ids = { it.cashouts.map { it.cashout_id } },
diff --git a/bank/src/test/kotlin/GcTest.kt b/bank/src/test/kotlin/GcTest.kt
index 550178a4..92c4bd41 100644
--- a/bank/src/test/kotlin/GcTest.kt
+++ b/bank/src/test/kotlin/GcTest.kt
@@ -51,14 +51,12 @@ class GcTest {
 
         // Time calculation
         val abortAfter = Duration.ofMinutes(15)
-        val cleanAfter = Period.ofDays(14)
-        val deleteAfter = Period.ofYears(10)
+        val cleanAfter = Duration.ofDays(14)
+        val deleteAfter = Duration.ofDays(350)
         val now = Instant.now()
-        val dateTime = LocalDateTime.ofInstant(now, ZoneOffset.UTC)
-        val abort = dateTime.minus(abortAfter).toInstant(ZoneOffset.UTC)
-        val clean = dateTime.minus(cleanAfter).toInstant(ZoneOffset.UTC)
-        val delete = dateTime.minus(deleteAfter).toInstant(ZoneOffset.UTC)
-   
+        val abort = now.minus(abortAfter)
+        val clean = now.minus(cleanAfter)
+        val delete = now.minus(deleteAfter)
 
         // Create test accounts
         val payto = IbanPayto.rand()
diff --git a/common/src/main/kotlin/TalerConfig.kt 
b/common/src/main/kotlin/TalerConfig.kt
index ed11ee94..c0148278 100644
--- a/common/src/main/kotlin/TalerConfig.kt
+++ b/common/src/main/kotlin/TalerConfig.kt
@@ -24,6 +24,8 @@ import org.slf4j.LoggerFactory
 import java.nio.file.AccessDeniedException
 import java.nio.file.NoSuchFileException
 import java.nio.file.Path
+import java.time.temporal.ChronoUnit
+import java.time.Duration
 import kotlin.io.path.*
 
 private val logger: Logger = LoggerFactory.getLogger("libeufin-config")
@@ -98,7 +100,7 @@ fun ConfigSource.fromFile(file: Path?): TalerConfig {
  * @param configSource information about where to load configuration defaults 
from
  */
 class TalerConfig internal constructor(
-    val configSource: ConfigSource,
+    val configSource: ConfigSource
 ) {
     private val sectionMap: MutableMap<String, Section> = mutableMapOf()
 
@@ -465,4 +467,31 @@ class TalerConfig internal constructor(
 
     fun requirePath(section: String, option: String): Path =
         lookupPath(section, option) ?: throw TalerConfigError.missing("path", 
section, option)
+
+    fun lookupDuration(section: String, option: String): Duration? {
+        val entry = lookupString(section, option) ?: return null
+        return TIME_AMOUNT_PATTERN.findAll(entry).map { match ->
+            val (rawAmount, unit) = match.destructured
+            val amount = rawAmount.toLongOrNull() ?: throw 
TalerConfigError.invalid("temporal", section, option, "'$rawAmount' not a valid 
temporal amount")
+            val value = when (unit) {
+                "us" -> 1
+                "ms" -> 1000
+                "s", "second", "seconds", "\"" -> 1000 * 1000L
+                "m", "min", "minute", "minutes", "'" -> 60 * 1000 * 1000L
+                "h", "hour", "hours" -> 60 * 60 * 1000 * 1000L
+                "d", "day", "days" -> 24 * 60 * 60 * 1000L * 1000L
+                "week", "weeks" ->  7 * 24 * 60 * 60 * 1000L * 1000L
+                "year", "years", "a" -> 31536000000000L
+                else -> throw TalerConfigError.invalid("temporal", section, 
option, "'$unit' not a valid temporal unit")
+            }
+            Duration.of(amount * value, ChronoUnit.MICROS)
+        }.fold(Duration.ZERO) { a, b -> a.plus(b) }
+    }
+
+    fun requireDuration(section: String, option: String): Duration =
+        lookupDuration(section, option) ?: throw 
TalerConfigError.missing("temporal", section, option)
+
+    companion object {
+        private val TIME_AMOUNT_PATTERN = Regex("([0-9]+) ?([a-z'\"]+)")
+    }
 }
diff --git a/common/src/test/kotlin/ConfigTest.kt 
b/common/src/test/kotlin/ConfigTest.kt
new file mode 100644
index 00000000..cb573501
--- /dev/null
+++ b/common/src/test/kotlin/ConfigTest.kt
@@ -0,0 +1,47 @@
+/*
+ * 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/>
+ */
+
+import org.junit.Test
+import java.time.Duration
+import tech.libeufin.common.*
+import kotlin.test.*
+
+class ConfigTest {
+    @Test
+    fun timeParsing() {
+        fun parseTime(raw: String): Duration {
+            val cfg = TalerConfig(ConfigSource("test", "test", "test"))
+            cfg.loadFromMem("""
+                [test]
+                time = "$raw"
+            """, null)
+            return cfg.requireDuration("test", "time")
+        }
+        assertEquals(Duration.ofSeconds(1), parseTime("1s"))
+        assertEquals(parseTime("1 s"), parseTime("1s"))
+        assertEquals(Duration.ofMinutes(10), parseTime("10m"))
+        assertEquals(parseTime("10 m"), parseTime("10m"))
+        assertEquals(Duration.ofHours(1), parseTime("01h"))
+        assertEquals(
+            
Duration.ofHours(1).plus(Duration.ofMinutes(10)).plus(Duration.ofSeconds(12)),
+            parseTime("1h10m12s")
+        )
+        assertEquals(parseTime("1h10m12s"), parseTime("1h10'12\""))
+    }
+}
\ No newline at end of file
diff --git a/contrib/bank.conf b/contrib/bank.conf
index 29239e1e..372143e6 100644
--- a/contrib/bank.conf
+++ b/contrib/bank.conf
@@ -70,7 +70,16 @@ BIND_TO = 0.0.0.0
 SPA = $DATADIR/spa/
 
 # Exchange that is suggested to wallets when withdrawing.
-#SUGGESTED_WITHDRAWAL_EXCHANGE = https://exchange.demo.taler.net/
+# SUGGESTED_WITHDRAWAL_EXCHANGE = https://exchange.demo.taler.net/
+
+# Time after which pending operations are aborted
+GC_ABORT_AFTER = 15m
+
+# Time after which aborted operations and expired items are deleted
+GC_CLEAN_AFTER = 14d
+
+# Time after which all bank transactions, operations and deleted accounts are 
deleted
+GC_DELETE_AFTER = 10year
 
 [libeufin-bankdb-postgres]
 # Where are the SQL files to setup our tables?
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
index 47c021cd..298d84b4 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
@@ -32,10 +32,12 @@ import tech.libeufin.nexus.db.*
 import java.io.IOException
 import java.io.InputStream
 import java.time.Instant
+import java.time.Duration
 import java.time.LocalDate
 import java.time.ZoneId
 import kotlin.io.*
 import kotlin.io.path.*
+import kotlin.time.toKotlinDuration
 
 /**
  * Necessary data to perform a download.
@@ -389,20 +391,16 @@ class EbicsFetch: CliktCommand("Fetches EBICS files") {
                     throw Exception("Failed to fetch documents")
                 }
             } else {
-                val configValue = cfg.config.requireString("nexus-fetch", 
"frequency")
-                val frequencySeconds = checkFrequency(configValue)
-                val cfgFrequency: NexusFrequency = 
NexusFrequency(frequencySeconds, configValue)
-                logger.debug("Running with a frequency of 
${cfgFrequency.fromConfig}")
-                val frequency: NexusFrequency? = if (cfgFrequency.inSeconds == 
0) {
+                var frequency: Duration = 
cfg.config.requireDuration("nexus-fetch", "frequency")
+                val raw = cfg.config.requireString("nexus-fetch", "frequency")
+                logger.debug("Running with a frequency of $raw")
+                if (frequency == Duration.ZERO) {
                     logger.warn("Long-polling not implemented, running 
therefore in transient mode")
-                    null
-                } else {
-                    cfgFrequency
                 }
                 do {
                     fetchDocuments(db, ctx, docs)
-                    delay(((frequency?.inSeconds ?: 0) * 1000).toLong())
-                } while (frequency != null)
+                    delay(frequency.toKotlinDuration())
+                } while (frequency != Duration.ZERO)
             }
         }
     }
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt
index 8366f064..cf206380 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSubmit.kt
@@ -29,6 +29,7 @@ import tech.libeufin.nexus.ebics.*
 import tech.libeufin.nexus.db.*
 import java.time.*
 import java.util.*
+import kotlin.time.toKotlinDuration
 
 /**
  * Groups useful parameters to submit pain.001 via EBICS.
@@ -156,20 +157,17 @@ class EbicsSubmit : CliktCommand("Submits any initiated 
payment found in the dat
             fileLogger = FileLogger(ebicsLog)
         )
         Database(dbCfg.dbConnStr).use { db -> 
-            val frequency = if (transient) {
+            val frequency: Duration = if (transient) {
                 logger.info("Transient mode: submitting what found and 
returning.")
-                null
+                Duration.ZERO
             } else {
-                val configValue = cfg.config.requireString("nexus-submit", 
"frequency")
-                val frequencySeconds = checkFrequency(configValue)
-                val frequency: NexusFrequency =  
NexusFrequency(frequencySeconds, configValue)
-                logger.debug("Running with a frequency of 
${frequency.fromConfig}")
-                if (frequency.inSeconds == 0) {
+                var frequency = cfg.config.requireDuration("nexus-submit", 
"frequency")
+                val raw = cfg.config.requireString("nexus-submit", "frequency")
+                logger.debug("Running with a frequency of $raw")
+                if (frequency == Duration.ZERO) {
                     logger.warn("Long-polling not implemented, running 
therefore in transient mode")
-                    null
-                } else {
-                    frequency
                 }
+                frequency
             }
             do {
                 try {
@@ -178,8 +176,8 @@ class EbicsSubmit : CliktCommand("Submits any initiated 
payment found in the dat
                     throw Exception("Failed to submit payments")
                 }
                 // TODO take submitBatch taken time in the delay
-                delay(((frequency?.inSeconds ?: 0) * 1000).toLong())
-            } while (frequency != null)
+                delay(frequency.toKotlinDuration())
+            } while (frequency != Duration.ZERO)
         }
     }
 }
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
index 8a51c766..b9582976 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
@@ -55,80 +55,6 @@ data class IbanAccountMetadata(
     val name: String
 )
 
-/**
- * Contains the frequency of submit or fetch iterations.
- */
-data class NexusFrequency(
-    /**
-     * Value in seconds of the FREQUENCY configuration
-     * value, found either under [nexus-fetch] or [nexus-submit]
-     */
-    val inSeconds: Int,
-    /**
-     * Copy of the value found in the configuration.  Used
-     * for logging.
-     */
-    val fromConfig: String
-)
-
-/**
- * Converts human-readable duration in how many seconds.  Supports
- * the suffixes 's' (seconds), 'm' (minute), 'h' (hours).  A valid
- * duration is therefore, for example, Nm, where N is the number of
- * minutes.
- *
- * @param trimmed duration
- * @return how many seconds is the duration input, or null if the input
- *         is not valid.
- */
-fun getFrequencyInSeconds(humanFormat: String): Int? {
-    val trimmed = humanFormat.trim()
-    if (trimmed.isEmpty()) {
-        logger.error("Input was empty")
-        return null
-    }
-    val howManySeconds: Int = when (val lastChar = trimmed.last()) {
-        's' -> {1}
-        'm' -> {60}
-        'h' -> {60 * 60}
-        else -> {
-            logger.error("Duration symbol not one of s, m, h.  '$lastChar' was 
found instead")
-            return null
-        }
-    }
-    val maybeNumber = trimmed.dropLast(1)
-    val howMany = try {
-        maybeNumber.trimEnd().toInt()
-    } catch (e: Exception) {
-        logger.error("Prefix was not a valid input: '$maybeNumber'")
-        return null
-    }
-    if (howMany == 0) return 0
-    val ret = howMany * howManySeconds
-    if (howMany != ret / howManySeconds) {
-        logger.error("Result overflew")
-        return null
-    }
-    return ret
-}
-
-/**
- * Sanity-checks the frequency found in the configuration and
- * either returns it or fails the process.  Note: the returned
- * value is also guaranteed to be non-negative.
- *
- * @param foundInConfig frequency value as found in the configuration.
- * @return the duration in seconds of the value found in the configuration.
- */
-fun checkFrequency(foundInConfig: String): Int {
-    val frequencySeconds = getFrequencyInSeconds(foundInConfig)
-        ?: throw Exception("Invalid frequency value in config section 
nexus-submit: $foundInConfig")
-    if (frequencySeconds < 0) {
-        throw Exception("Configuration error: cannot operate with a negative 
submit frequency ($foundInConfig)")
-    }
-    return frequencySeconds
-}
-
 fun Instant.fmtDate(): String = 
     DateTimeFormatter.ISO_LOCAL_DATE.withZone(ZoneId.of("UTC")).format(this)
 
diff --git a/nexus/src/test/kotlin/ConfigLoading.kt 
b/nexus/src/test/kotlin/ConfigLoading.kt
deleted file mode 100644
index ebdfa381..00000000
--- a/nexus/src/test/kotlin/ConfigLoading.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * 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/>
- */
-
-import org.junit.Test
-import tech.libeufin.nexus.getFrequencyInSeconds
-import kotlin.test.assertEquals
-import kotlin.test.assertNull
-
-class ConfigLoading {
-    // Checks converting human-readable durations to seconds.
-    @Test
-    fun timeParsing() {
-        assertEquals(1, getFrequencyInSeconds("1s"))
-        assertEquals(1, getFrequencyInSeconds("       1           s       "))
-        assertEquals(10*60, getFrequencyInSeconds("10m"))
-        assertEquals(10*60, getFrequencyInSeconds("10 m"))
-        assertEquals(24*60*60, getFrequencyInSeconds("24h"))
-        assertEquals(24*60*60, getFrequencyInSeconds(" 24h"))
-        assertEquals(60*60, getFrequencyInSeconds("      1h      "))
-        assertEquals(60*60, getFrequencyInSeconds("01h"))
-        assertNull(getFrequencyInSeconds("1.1s"))
-        assertNull(getFrequencyInSeconds("         "))
-        assertNull(getFrequencyInSeconds("m"))
-        assertNull(getFrequencyInSeconds(""))
-        assertNull(getFrequencyInSeconds("0"))
-    }
-}
\ No newline at end of file
diff --git a/testbench/src/test/kotlin/IntegrationTest.kt 
b/testbench/src/test/kotlin/IntegrationTest.kt
index b6ede6c0..441ec143 100644
--- a/testbench/src/test/kotlin/IntegrationTest.kt
+++ b/testbench/src/test/kotlin/IntegrationTest.kt
@@ -108,6 +108,8 @@ class IntegrationTest {
             // Check bank is running
             client.get("http://0.0.0.0:8080/public-accounts";).assertNoContent()
         }
+
+        bankCmd.run("gc $flags")
     }
 
     @Test

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]