gnunet-svn
[Top][All Lists]
Advanced

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

[libeufin] branch master updated: Taler withdrawal: create and abort.


From: gnunet
Subject: [libeufin] branch master updated: Taler withdrawal: create and abort.
Date: Tue, 19 Sep 2023 18:30:42 +0200

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

ms pushed a commit to branch master
in repository libeufin.

The following commit(s) were added to refs/heads/master by this push:
     new a0315990 Taler withdrawal: create and abort.
a0315990 is described below

commit a03159906df6342432c238a6f7956a4872498443
Author: MS <ms@taler.net>
AuthorDate: Tue Sep 19 18:30:22 2023 +0200

    Taler withdrawal: create and abort.
---
 .../src/main/kotlin/tech/libeufin/bank/Database.kt |  23 ++++-
 .../kotlin/tech/libeufin/bank/talerWebHandlers.kt  |  86 ++++++++++++----
 bank/src/main/kotlin/tech/libeufin/bank/types.kt   |  23 +++++
 bank/src/test/kotlin/TalerApiTest.kt               | 114 +++++++++++++++++++++
 bank/src/test/kotlin/TalerTest.kt                  |  34 ------
 5 files changed, 227 insertions(+), 53 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
index 64fe9afa..4815aa61 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
@@ -144,7 +144,7 @@ class Database(private val dbConfig: String) {
         }
         res.use {
             if (!it.next())
-                throw internalServerError("SQL RETURNING gave nothing.")
+                throw internalServerError("SQL RETURNING gave no customer_id.")
             return it.getLong("customer_id")
         }
     }
@@ -615,6 +615,27 @@ class Database(private val dbConfig: String) {
         }
     }
 
+    /**
+     * Aborts one Taler withdrawal, only if it wasn't previously
+     * confirmed.  It returns false if the UPDATE didn't succeed.
+     */
+    fun talerWithdrawalAbort(opUUID: UUID): Boolean {
+        reconnect()
+        val stmt = prepare("""
+            UPDATE taler_withdrawal_operations
+            SET aborted = true
+            WHERE withdrawal_uuid=? AND selection_done = false
+            RETURNING taler_withdrawal_id
+        """
+        )
+        stmt.setObject(1, opUUID)
+        val res = stmt.executeQuery()
+        res.use {
+            if (!it.next()) return false
+        }
+        return true
+    }
+
     // Values coming from the wallet.
     fun talerWithdrawalSetDetails(
         opUUID: UUID,
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt
index d3bc8e71..e680d7df 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt
@@ -24,6 +24,7 @@
 
 package tech.libeufin.bank
 
+import io.ktor.http.*
 import io.ktor.server.application.*
 import io.ktor.server.plugins.*
 import io.ktor.server.request.*
@@ -34,6 +35,27 @@ import net.taler.wallet.crypto.Base32Crockford
 import tech.libeufin.util.getBaseUrl
 import java.util.*
 
+/**
+ * This handler factors out the checking of the query param
+ * and the retrieval of the related withdrawal database row.
+ * It throws 404 if the operation is not found, and throws 400
+ * if the query param doesn't parse into an UUID.
+ */
+private fun getWithdrawal(opIdParam: String): TalerWithdrawalOperation {
+    val opId = try {
+        UUID.fromString(opIdParam)
+    } catch (e: Exception) {
+        logger.error(e.message)
+        throw badRequest("withdrawal_id query parameter was malformed")
+    }
+    val op = db.talerWithdrawalGet(opId)
+        ?: throw notFound(
+            hint = "Withdrawal operation ${opIdParam} not found",
+            talerEc = TalerErrorCode.TALER_EC_END
+        )
+    return op
+}
+
 fun Routing.talerWebHandlers() {
     post("/accounts/{USERNAME}/withdrawals") {
         val c = call.myAuth(TokenScope.readwrite) ?: throw unauthorized()
@@ -76,25 +98,13 @@ fun Routing.talerWebHandlers() {
         ))
         return@post
     }
-    get("/accounts/{USERNAME}/withdrawals/{W_ID}") {
+    get("/accounts/{USERNAME}/withdrawals/{withdrawal_id}") {
         val c = call.myAuth(TokenScope.readonly) ?: throw unauthorized()
         val accountName = call.expectUriComponent("USERNAME")
         // Admin allowed to see the details
         if (c.login != accountName && c.login != "admin") throw forbidden()
         // Permissions passed, get the information.
-        val opIdParam: String = call.request.queryParameters.get("W_ID") ?: 
throw
-                MissingRequestParameterException("withdrawal_id")
-        val opId = try {
-            UUID.fromString(opIdParam)
-        } catch (e: Exception) {
-            logger.error(e.message)
-            throw badRequest("withdrawal_id query parameter was malformed")
-        }
-        val op = db.talerWithdrawalGet(opId)
-            ?: throw notFound(
-                hint = "Withdrawal operation ${opIdParam} not found",
-                talerEc = TalerErrorCode.TALER_EC_END
-            )
+        val op = getWithdrawal(call.expectUriComponent("withdrawal_id"))
         call.respond(BankAccountGetWithdrawalResponse(
             amount = op.amount.toString(),
             aborted = op.aborted,
@@ -107,11 +117,51 @@ fun Routing.talerWebHandlers() {
         ))
         return@get
     }
-    post("/accounts/{USERNAME}/withdrawals/abort") {
-        throw NotImplementedError()
+    post("/accounts/{USERNAME}/withdrawals/{withdrawal_id}/abort") {
+        val c = call.myAuth(TokenScope.readonly) ?: throw unauthorized()
+        // Admin allowed to abort.
+        if (!call.getResourceName("USERNAME").canI(c)) throw forbidden()
+        val op = getWithdrawal(call.expectUriComponent("withdrawal_id"))
+        // Idempotency:
+        if (op.aborted) {
+            call.respondText("{}", ContentType.Application.Json)
+            return@post
+        }
+        // Op is found, it'll now fail only if previously confirmed (DB 
checks).
+        if (!db.talerWithdrawalAbort(op.withdrawalUuid)) throw conflict(
+            hint = "Cannot abort confirmed withdrawal",
+            talerEc = TalerErrorCode.TALER_EC_END
+        )
+        call.respondText("{}", ContentType.Application.Json)
+        return@post
     }
-    post("/accounts/{USERNAME}/withdrawals/confirm") {
-        throw NotImplementedError()
+    post("/accounts/{USERNAME}/withdrawals/{withdrawal_id}/confirm") {
+        val c = call.myAuth(TokenScope.readwrite) ?: throw unauthorized()
+        // No admin allowed.
+        if(!call.getResourceName("USERNAME").canI(c, withAdmin = false)) throw 
forbidden()
+        val op = getWithdrawal(call.expectUriComponent("withdrawal_id"))
+        // Checking idempotency:
+        if (op.confirmationDone) {
+            call.respondText("{}", ContentType.Application.Json)
+            return@post
+        }
+        if (op.aborted)
+            throw conflict(
+                hint = "Cannot confirm an aborted withdrawal",
+                talerEc = TalerErrorCode.TALER_EC_BANK_CONFIRM_ABORT_CONFLICT
+            )
+        // Checking that reserve GOT indeed selected.
+        if (!op.selectionDone)
+            throw LibeufinBankException(
+                httpStatus = HttpStatusCode.UnprocessableEntity,
+                talerError = TalerError(
+                    hint = "Cannot confirm an unselected withdrawal",
+                    code = TalerErrorCode.TALER_EC_END.code
+            ))
+        /* Confirmation conditions are all met, now put the operation
+         * to the selected state _and_ wire the funds to the exchange.
+         */
+        throw NotImplementedError("Need a database transaction now?")
     }
 }
 
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/types.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/types.kt
index cfe58476..41064863 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/types.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/types.kt
@@ -20,6 +20,7 @@
 package tech.libeufin.bank
 
 import io.ktor.http.*
+import io.ktor.server.application.*
 import kotlinx.serialization.Contextual
 import kotlinx.serialization.Serializable
 import java.util.*
@@ -406,3 +407,25 @@ data class BankAccountGetWithdrawalResponse(
     val selected_reserve_pub: String? = null,
     val selected_exchange_account: String? = null
 )
+
+typealias ResourceName = String
+
+
+// Checks if the input Customer has the rights over ResourceName
+fun ResourceName.canI(c: Customer, withAdmin: Boolean = true): Boolean {
+    if (c.login == this) return true
+    if (c.login == "admin" && withAdmin) return true
+    return false
+}
+
+/**
+ * Factors out the retrieval of the resource name from
+ * the URI.  The resource looked for defaults to "USERNAME"
+ * as this is frequently mentioned resource along the endpoints.
+ *
+ * This helper is recommended because it returns a ResourceName
+ * type that then offers the ".canI()" helper to check if the user
+ * has the rights on the resource.
+ */
+fun ApplicationCall.getResourceName(param: String): ResourceName =
+    this.expectUriComponent(param)
\ No newline at end of file
diff --git a/bank/src/test/kotlin/TalerApiTest.kt 
b/bank/src/test/kotlin/TalerApiTest.kt
new file mode 100644
index 00000000..d5334c98
--- /dev/null
+++ b/bank/src/test/kotlin/TalerApiTest.kt
@@ -0,0 +1,114 @@
+import io.ktor.client.plugins.*
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
+import io.ktor.http.*
+import io.ktor.server.testing.*
+import kotlinx.serialization.json.Json
+import org.junit.Ignore
+import org.junit.Test
+import tech.libeufin.bank.*
+import tech.libeufin.util.CryptoUtil
+import java.util.*
+
+class TalerApiTest {
+    private val customerFoo = Customer(
+        login = "foo",
+        passwordHash = CryptoUtil.hashpw("pw"),
+        name = "Foo",
+        phone = "+00",
+        email = "foo@b.ar",
+        cashoutPayto = "payto://external-IBAN",
+        cashoutCurrency = "KUDOS"
+    )
+    private val bankAccountFoo = BankAccount(
+        internalPaytoUri = "FOO-IBAN-XYZ",
+        lastNexusFetchRowId = 1L,
+        owningCustomerId = 1L,
+        hasDebt = false,
+        maxDebt = TalerAmount(10, 1, "KUDOS")
+    )
+    // Testing withdrawal abort
+    @Test
+    fun withdrawalAbort() {
+        val db = initDb()
+        val uuid = UUID.randomUUID()
+        assert(db.customerCreate(customerFoo) != null)
+        assert(db.bankAccountCreate(bankAccountFoo))
+        // insert new.
+        assert(db.talerWithdrawalCreate(
+            opUUID = uuid,
+            walletBankAccount = 1L,
+            amount = TalerAmount(1, 0)
+        ))
+        val op = db.talerWithdrawalGet(uuid)
+        assert(op?.aborted == false)
+        testApplication {
+            application(webApp)
+            client.post("/accounts/foo/withdrawals/${uuid}/abort") {
+                expectSuccess = true
+                basicAuth("foo", "pw")
+            }
+        }
+        val opAbo = db.talerWithdrawalGet(uuid)
+        assert(opAbo?.aborted == true)
+    }
+    // Testing withdrawal creation
+    @Test
+    fun withdrawalCreation() {
+        val db = initDb()
+        assert(db.customerCreate(customerFoo) != null)
+        assert(db.bankAccountCreate(bankAccountFoo))
+        testApplication {
+            application(webApp)
+            // Creating the withdrawal as if the SPA did it.
+            val r = client.post("/accounts/foo/withdrawals") {
+                basicAuth("foo", "pw")
+                contentType(ContentType.Application.Json)
+                expectSuccess = true
+                setBody("""
+                    {"amount": "KUDOS:9"}
+                """.trimIndent())
+            }
+            val opId = 
Json.decodeFromString<BankAccountCreateWithdrawalResponse>(r.bodyAsText())
+            // Getting the withdrawal from the bank.  Throws (failing the 
test) if not found.
+            client.get("/accounts/foo/withdrawals/${opId.withdrawal_id}") {
+                expectSuccess = true
+                basicAuth("foo", "pw")
+            }
+        }
+    }
+    // Testing withdrawal confirmation
+    @Ignore
+    fun withdrawalConfirmation() {
+        assert(false)
+    }
+    // Testing the generation of taler://withdraw-URIs.
+    @Test
+    fun testWithdrawUri() {
+        // Checking the taler+http://-style.
+        val withHttp = getTalerWithdrawUri(
+            "http://example.com";,
+            "my-id"
+        )
+        assert(withHttp == 
"taler+http://withdraw/example.com/taler-integration/my-id";)
+        // Checking the taler://-style
+        val onlyTaler = getTalerWithdrawUri(
+            "https://example.com/";,
+            "my-id"
+        )
+        // Note: this tests as well that no double slashes belong to the result
+        assert(onlyTaler == 
"taler://withdraw/example.com/taler-integration/my-id")
+        // Checking the removal of subsequent slashes
+        val manySlashes = getTalerWithdrawUri(
+            "https://www.example.com//////";,
+            "my-id"
+        )
+        assert(manySlashes == 
"taler://withdraw/www.example.com/taler-integration/my-id")
+        // Checking with specified port number
+        val withPort = getTalerWithdrawUri(
+            "https://www.example.com:9876";,
+            "my-id"
+        )
+        assert(withPort == 
"taler://withdraw/www.example.com:9876/taler-integration/my-id")
+    }
+}
\ No newline at end of file
diff --git a/bank/src/test/kotlin/TalerTest.kt 
b/bank/src/test/kotlin/TalerTest.kt
deleted file mode 100644
index 2aa90e2d..00000000
--- a/bank/src/test/kotlin/TalerTest.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-import org.junit.Test
-import tech.libeufin.bank.getTalerWithdrawUri
-
-class TalerTest {
-    // Testing the generation of taler://withdraw-URIs.
-    @Test
-    fun testWithdrawUri() {
-        // Checking the taler+http://-style.
-        val withHttp = getTalerWithdrawUri(
-            "http://example.com";,
-            "my-id"
-        )
-        assert(withHttp == 
"taler+http://withdraw/example.com/taler-integration/my-id";)
-        // Checking the taler://-style
-        val onlyTaler = getTalerWithdrawUri(
-            "https://example.com/";,
-            "my-id"
-        )
-        // Note: this tests as well that no double slashes belong to the result
-        assert(onlyTaler == 
"taler://withdraw/example.com/taler-integration/my-id")
-        // Checking the removal of subsequent slashes
-        val manySlashes = getTalerWithdrawUri(
-            "https://www.example.com//////";,
-            "my-id"
-        )
-        assert(manySlashes == 
"taler://withdraw/www.example.com/taler-integration/my-id")
-        // Checking with specified port number
-        val withPort = getTalerWithdrawUri(
-            "https://www.example.com:9876";,
-            "my-id"
-        )
-        assert(withPort == 
"taler://withdraw/www.example.com:9876/taler-integration/my-id")
-    }
-}
\ No newline at end of file

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