gnunet-svn
[Top][All Lists]
Advanced

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

[libeufin] 02/02: Get bank testcases up to 'credit-1' to pass.


From: gnunet
Subject: [libeufin] 02/02: Get bank testcases up to 'credit-1' to pass.
Date: Mon, 20 Apr 2020 21:24:44 +0200

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

marcello pushed a commit to branch master
in repository libeufin.

commit 69f7cd208c41ba0436984b8455f8652a4f537f76
Author: Marcello Stanisci <address@hidden>
AuthorDate: Mon Apr 20 21:18:15 2020 +0200

    Get bank testcases up to 'credit-1' to pass.
    
    The following changes were needed:
    
    * Implement decompression of upload data.
    * Wrap timestamps within 'ms_t'.
    * Allow x-taler-bank as Payto type.
    * Introduce NEXUS_PRODUCTION env variable to allow
      EBICS-free tests.
    * Avoid Gson serializer upon respond, as it includes
      the "charset" token into the Content-Type response
      header, and this latter makes the exchange parser unhappy.
    * Store booking date in Long format (Raw payment table).
    * Avoid the "204 No Content" HTTP status code, as it makes
      the exchange unhappy; return "200 OK" on empty histories
      instead.
---
 nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt    |   2 +-
 .../src/main/kotlin/tech/libeufin/nexus/Helpers.kt |  11 +-
 nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt  |  82 ++++++++++----
 nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt | 120 +++++++++++++++------
 4 files changed, 158 insertions(+), 57 deletions(-)

diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
index a7b57f4..af94e1d 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
@@ -96,7 +96,7 @@ object EbicsRawBankTransactionsTable : LongIdTable() {
     val debitorIban = text("debitorIban")
     val debitorName = text("debitorName")
     val counterpartBic = text("counterpartBic")
-    val bookingDate = text("bookingDate")
+    val bookingDate = long("bookingDate")
     val status = text("status") // BOOK, ..
 }
 
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt
index e5b38bf..3e04eed 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Helpers.kt
@@ -4,6 +4,8 @@ import io.ktor.application.ApplicationCall
 import io.ktor.http.HttpStatusCode
 import org.jetbrains.exposed.sql.and
 import org.jetbrains.exposed.sql.transactions.transaction
+import org.joda.time.DateTime
+import org.joda.time.format.DateTimeFormat
 import tech.libeufin.util.CryptoUtil
 import tech.libeufin.util.base64ToBytes
 import javax.sql.rowset.serial.SerialBlob
@@ -103,10 +105,9 @@ fun extractUserAndHashedPassword(authorizationHeader: 
String): Pair<String, Byte
  * @return subscriber id
  */
 fun authenticateRequest(authorization: String?): String {
-    val headerLine = authorization ?: throw NexusError(
+    val headerLine = if (authorization == null) throw NexusError(
         HttpStatusCode.BadRequest, "Authentication:-header line not found"
-    )
-    logger.debug("Checking for authorization: $headerLine")
+    ) else authorization
     val subscriber = transaction {
         val (user, pass) = extractUserAndHashedPassword(headerLine)
         EbicsSubscriberEntity.find {
@@ -115,3 +116,7 @@ fun authenticateRequest(authorization: String?): String {
     } ?: throw NexusError(HttpStatusCode.Forbidden, "Wrong password")
     return subscriber.id.value
 }
+
+fun parseDate(date: String): DateTime {
+    return DateTime.parse(date, DateTimeFormat.forPattern("YYYY-MM-DD"))
+}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
index 3f86387..e751c0d 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
@@ -22,15 +22,13 @@ package tech.libeufin.nexus
 import io.ktor.application.ApplicationCallPipeline
 import io.ktor.application.call
 import io.ktor.application.install
-import io.ktor.auth.Authentication
-import io.ktor.auth.basic
 import io.ktor.client.HttpClient
-import io.ktor.features.CallLogging
-import io.ktor.features.ContentNegotiation
-import io.ktor.features.StatusPages
+import io.ktor.features.*
 import io.ktor.gson.gson
 import io.ktor.http.ContentType
 import io.ktor.http.HttpStatusCode
+import io.ktor.request.ApplicationReceivePipeline
+import io.ktor.request.ApplicationReceiveRequest
 import io.ktor.request.receive
 import io.ktor.request.uri
 import io.ktor.response.respond
@@ -40,6 +38,11 @@ import io.ktor.routing.post
 import io.ktor.routing.routing
 import io.ktor.server.engine.embeddedServer
 import io.ktor.server.netty.Netty
+import io.ktor.util.KtorExperimentalAPI
+import kotlinx.coroutines.io.ByteReadChannel
+import kotlinx.coroutines.io.jvm.javaio.toByteReadChannel
+import kotlinx.coroutines.io.jvm.javaio.toInputStream
+import kotlinx.io.core.ExperimentalIoApi
 import org.jetbrains.exposed.sql.SizedIterable
 import org.jetbrains.exposed.sql.StdOutSqlLogger
 import org.jetbrains.exposed.sql.addLogger
@@ -60,6 +63,8 @@ import java.time.ZoneId
 import java.time.ZonedDateTime
 import java.time.format.DateTimeFormatter
 import java.util.*
+import java.util.zip.Inflater
+import java.util.zip.InflaterInputStream
 import javax.crypto.EncryptedPrivateKeyInfo
 import javax.sql.rowset.serial.SerialBlob
 
@@ -86,6 +91,10 @@ data class NexusError(val statusCode: HttpStatusCode, val 
reason: String) : Exce
 
 val logger: Logger = LoggerFactory.getLogger("tech.libeufin.nexus")
 
+fun isProduction(): Boolean {
+    return System.getenv("NEXUS_PRODUCTION") != null
+}
+
 fun getSubscriberEntityFromId(id: String): EbicsSubscriberEntity {
     return transaction {
         EbicsSubscriberEntity.findById(id) ?: throw NexusError(
@@ -146,14 +155,33 @@ fun getSubscriberDetailsFromBankAccount(bankAccountId: 
String): EbicsClientSubsc
  * is guaranteed to be non empty.
  */
 fun getBankAccountsInfoFromId(id: String): 
SizedIterable<EbicsAccountInfoEntity> {
+    logger.debug("Looking up bank account of user '$id'")
     val list = transaction {
         EbicsAccountInfoEntity.find {
             EbicsAccountsInfoTable.subscriber eq id
         }
     }
-    if (list.empty()) throw NexusError(
-        HttpStatusCode.NotFound, "This subscriber '$id' did never fetch its 
own bank accounts, request HTD first."
-    )
+    if (list.empty()) {
+        if (!isProduction()) {
+            /* make up a bank account info object */
+            transaction {
+                EbicsAccountInfoEntity.new("mocked-bank-account") {
+                    subscriber = EbicsSubscriberEntity.findById(id) ?: throw 
NexusError(
+                        HttpStatusCode.NotFound, "Please create subscriber 
'${id}' first."
+                    )
+                    accountHolder = "Tests runner"
+                    iban = "IBAN-FOR-TESTS"
+                    bankCode = "BIC-FOR-TESTS"
+                }
+            }
+            logger.debug("Faked bank account info object for user '$id'")
+        } else throw NexusError(
+            HttpStatusCode.NotFound,
+            "This subscriber '$id' did never fetch its own bank accounts, 
request HTD first."
+        )
+        // call this function again now that the database is augmented with 
the mocked information.
+        return getBankAccountsInfoFromId(id)
+    }
     return list
 }
 
@@ -336,6 +364,8 @@ fun createPain001entity(entry: Pain001Data, 
debtorAccountId: String): Pain001Ent
     }
 }
 
+@ExperimentalIoApi
+@KtorExperimentalAPI
 fun main() {
     dbCreateTables()
     testData()
@@ -343,6 +373,7 @@ fun main() {
         expectSuccess = false // this way, it does not throw exceptions on != 
200 responses.
     }
     val server = embeddedServer(Netty, port = 5001) {
+
         install(CallLogging) {
             this.level = Level.DEBUG
             this.logger = tech.libeufin.nexus.logger
@@ -370,12 +401,13 @@ fun main() {
                     cause.statusCode
                 )
             }
-            exception<javax.xml.bind.UnmarshalException> { cause ->
-                logger.error("Exception while handling '${call.request.uri}'", 
cause)
+            exception<Exception> { cause ->
+                logger.error("Uncaught exception while handling 
'${call.request.uri}'", cause)
+                logger.error(cause.toString())
                 call.respondText(
-                    "Could not convert string into JAXB\n",
+                    "Internal server error",
                     ContentType.Text.Plain,
-                    HttpStatusCode.NotFound
+                    HttpStatusCode.InternalServerError
                 )
             }
         }
@@ -387,6 +419,18 @@ fun main() {
             }
         }
 
+        receivePipeline.intercept(ApplicationReceivePipeline.Before) {
+            if (this.context.request.headers["Content-Encoding"] == "deflate") 
{
+                logger.debug("About to inflate received data")
+                val deflated = this.subject.value as ByteReadChannel
+                val inflated = InflaterInputStream(deflated.toInputStream())
+                proceedWith(ApplicationReceiveRequest(this.subject.typeInfo, 
inflated.toByteReadChannel()))
+                return@intercept
+            }
+            proceed()
+            return@intercept
+        }
+
         routing {
             get("/") {
                 call.respondText("Hello by Nexus!\n")
@@ -677,12 +721,10 @@ fun main() {
                                     currency = 
camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Amt']/@Ccy")
                                     amount = 
camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Amt']")
                                     status = 
camt53doc.pickString("//*[local-name()='Ntry']//*[local-name()='Sts']")
-                                    bookingDate = 
camt53doc.pickString("//*[local-name()='BookgDt']//*[local-name()='Dt']")
+                                    bookingDate = 
parseDate(camt53doc.pickString("//*[local-name()='BookgDt']//*[local-name()='Dt']")).millis
                                     nexusSubscriber = 
getSubscriberEntityFromId(id)
-                                    creditorName =
-                                        
camt53doc.pickString("//*[local-name()='RltdPties']//*[local-name()='Dbtr']//*[local-name()='Nm']")
-                                    creditorIban =
-                                        
camt53doc.pickString("//*[local-name()='CdtrAcct']//*[local-name()='IBAN']")
+                                    creditorName = 
camt53doc.pickString("//*[local-name()='RltdPties']//*[local-name()='Dbtr']//*[local-name()='Nm']")
+                                    creditorIban = 
camt53doc.pickString("//*[local-name()='CdtrAcct']//*[local-name()='IBAN']")
                                     debitorName = 
camt53doc.pickString("//*[local-name()='RltdPties']//*[local-name()='Dbtr']//*[local-name()='Nm']")
                                     debitorIban = 
camt53doc.pickString("//*[local-name()='DbtrAcct']//*[local-name()='IBAN']")
                                     counterpartBic = 
camt53doc.pickString("//*[local-name()='RltdAgts']//*[local-name()='BIC']")
@@ -1415,8 +1457,12 @@ fun main() {
                 call.respondText("Bank keys stored in database\n", 
ContentType.Text.Plain, HttpStatusCode.OK)
                 return@post
             }
+            post("/test/intercept") {
+                call.respondText(call.receive<String>() + "\n")
+                return@post
+            }
         }
     }
     logger.info("Up and running")
     server.start(wait = true)
-}
+}
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt
index b82e516..25de532 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/taler.kt
@@ -1,6 +1,8 @@
 package tech.libeufin.nexus
 
+import com.google.gson.Gson
 import io.ktor.application.call
+import io.ktor.content.TextContent
 import io.ktor.http.ContentType
 import io.ktor.http.HttpStatusCode
 import io.ktor.request.receive
@@ -18,6 +20,7 @@ import tech.libeufin.util.Amount
 import tech.libeufin.util.CryptoUtil
 import tech.libeufin.util.toZonedString
 import kotlin.math.abs
+import kotlin.math.min
 
 class Taler(app: Route) {
 
@@ -73,8 +76,11 @@ class Taler(app: Route) {
         val debit_account: String
     )
 
+    private data class GnunetTimestamp(
+        val t_ms: Long
+    )
     private data class TalerAddIncomingResponse(
-        val timestamp: Long,
+        val timestamp: GnunetTimestamp,
         val row_id: Long
     )
 
@@ -94,13 +100,36 @@ class Taler(app: Route) {
     /**
      * Helper functions
      */
-
     fun parsePayto(paytoUri: String): Payto {
-        // payto://iban/BIC?/IBAN?name=<name>
-        val match = 
Regex("payto://iban/([A-Z0-9]+/)?([A-Z0-9]+)\\?name=(\\w+)").find(paytoUri) ?: 
throw
-                NexusError(HttpStatusCode.BadRequest, "invalid payto URI 
($paytoUri)")
-        val (bic, iban, name) = match.destructured
-        return Payto(name, iban, bic.replace("/", ""))
+        /**
+         * First try to parse a "iban"-type payto URI.  If that fails,
+         * then assume a test is being run under the "x-taler-bank" type.
+         * If that one fails too, throw exception.
+         *
+         * Note: since the Nexus doesn't have the notion of "x-taler-bank",
+         * such URIs must yield a iban-compatible tuple of values.  Therefore,
+         * the plain bank account number maps to a "iban", and the <bank 
hostname>
+         * maps to a "bic".
+         */
+
+
+        /**
+         * payto://iban/BIC?/IBAN?name=<name>
+         * payto://x-taler-bank/<bank hostname>/<plain account number>
+         */
+
+        val ibanMatch = 
Regex("payto://iban/([A-Z0-9]+/)?([A-Z0-9]+)\\?name=(\\w+)").find(paytoUri)
+        if (ibanMatch != null) {
+            val (bic, iban, name) = ibanMatch.destructured
+            return Payto(name, iban, bic.replace("/", ""))
+        }
+        val xTalerBankMatch = 
Regex("payto://x-taler-bank/localhost/([0-9])?").find(paytoUri)
+        if (xTalerBankMatch != null) {
+            val xTalerBankAcctNo = xTalerBankMatch.destructured.component1()
+            return Payto("Taler Exchange", xTalerBankAcctNo, "localhost")
+        }
+
+        throw NexusError(HttpStatusCode.BadRequest, "invalid payto URI 
($paytoUri)")
     }
 
     fun parseAmount(amount: String): AmountWithCurrency {
@@ -123,9 +152,6 @@ class Taler(app: Route) {
     private fun getPaytoUri(iban: String, bic: String): String {
         return "payto://iban/$iban/$bic"
     }
-    private fun parseDate(date: String): DateTime {
-        return DateTime.parse(date, DateTimeFormat.forPattern("YYYY-MM-DD"))
-    }
 
     /** Builds the comparison operator for history entries based on the sign 
of 'delta'  */
     private fun getComparisonOperator(delta: Int, start: Long): Op<Boolean> {
@@ -158,6 +184,18 @@ class Taler(app: Route) {
         }
     }
 
+    /**
+     * The Taler layer cannot rely on the ktor-internal 
JSON-converter/responder,
+     * because this one adds a "charset" extra information in the Content-Type 
header
+     * that makes the GNUnet JSON parser unhappy.
+     *
+     * The workaround is to explicitly convert the 'data class'-object into a 
JSON
+     * string (what this function does), and use the simpler respondText 
method.
+     */
+    private fun customConverter(body: Any): String {
+        return Gson().toJson(body)
+    }
+
     /** Attach Taler endpoints to the main Web server */
 
     init {
@@ -201,7 +239,8 @@ class Taler(app: Route) {
                     ),
                     exchangeBankAccount.id.value
                 )
-                val rawEbics = if (System.getenv("NEXUS_PRODUCTION") == null) {
+
+                val rawEbics = if (!isProduction()) {
                     EbicsRawBankTransactionEntity.new {
                         sourceFileName = "test"
                         unstructuredRemittanceInformation = 
transferRequest.wtid
@@ -213,7 +252,7 @@ class Taler(app: Route) {
                         creditorName = creditorObj.name
                         creditorIban = creditorObj.iban
                         counterpartBic = creditorObj.bic
-                        bookingDate = DateTime.now().toString("Y-MM-dd")
+                        bookingDate = DateTime.now().millis
                         nexusSubscriber = exchangeBankAccount.subscriber
                         status = "BOOK"
                     }
@@ -264,20 +303,29 @@ class Taler(app: Route) {
                     debitorIban = debtor.iban
                     debitorName = debtor.name
                     counterpartBic = debtor.bic
-                    bookingDate = DateTime.now().toZonedString()
+                    bookingDate = DateTime.now().millis
                     status = "BOOK"
+                    nexusSubscriber = getSubscriberEntityFromId(exchangeId)
                 }
                 /** This payment is "valid by default" and will be returned
                  * as soon as the exchange will ask for new payments.  */
                 val row = TalerIncomingPaymentEntity.new {
                     payment = rawPayment
+                    valid = true
                 }
                 Pair(rawPayment.bookingDate, row.id.value)
             }
-            call.respond(HttpStatusCode.OK, TalerAddIncomingResponse(
-                timestamp = parseDate(bookingDate).millis / 1000,
-                row_id = opaque_row_id
-            ))
+            call.respond(
+                TextContent(
+                    customConverter(
+                        TalerAddIncomingResponse(
+                            timestamp = GnunetTimestamp(bookingDate/ 1000),
+                            row_id = opaque_row_id
+                        )
+                    ),
+                ContentType.Application.Json
+                )
+            )
             return@post
         }
 
@@ -398,9 +446,8 @@ class Taler(app: Route) {
                             row_id = it.id.value,
                             amount = it.amount,
                             wtid = it.wtid,
-                            date = parseDate(it.rawConfirmed?.bookingDate ?: 
throw NexusError(
-                                HttpStatusCode.InternalServerError, "Null 
value met after check, VERY strange.")
-                            ).millis / 1000,
+                            date = it.rawConfirmed?.bookingDate?.div(1000) ?: 
throw NexusError(
+                                HttpStatusCode.InternalServerError, "Null 
value met after check, VERY strange."),
                             credit_account = it.creditAccount,
                             debit_account = 
getPaytoUri(subscriberBankAccount.iban, subscriberBankAccount.bankCode),
                             exchange_base_url = 
"FIXME-to-request-along-subscriber-registration"
@@ -423,26 +470,29 @@ class Taler(app: Route) {
             val startCmpOp = getComparisonOperator(delta, start)
             transaction {
                 val subscriberBankAccount = 
getBankAccountsInfoFromId(subscriberId)
-                TalerIncomingPaymentEntity.find {
+                val orderedPayments = TalerIncomingPaymentEntity.find {
                     TalerIncomingPayments.valid eq true and startCmpOp
-                }.orderTaler(delta).subList(0, abs(delta)).forEach {
-                    history.incoming_transactions.add(
-                        TalerIncomingBankTransaction(
-                            date = parseDate(it.payment.bookingDate).millis / 
1000, // timestamp in seconds
-                            row_id = it.id.value,
-                            amount = 
"${it.payment.currency}:${it.payment.amount}",
-                            reserve_pub = 
it.payment.unstructuredRemittanceInformation,
-                            debit_account = getPaytoUri(
-                                it.payment.debitorName, 
it.payment.debitorIban, it.payment.counterpartBic
-                            ),
-                            credit_account = getPaytoUri(
-                                it.payment.creditorName, 
it.payment.creditorIban, subscriberBankAccount.first().bankCode
+                }.orderTaler(delta)
+                if (orderedPayments.isNotEmpty()) {
+                    orderedPayments.subList(0, min(abs(delta), 
orderedPayments.size)).forEach {
+                        history.incoming_transactions.add(
+                            TalerIncomingBankTransaction(
+                                date = it.payment.bookingDate / 1000, // 
timestamp in seconds
+                                row_id = it.id.value,
+                                amount = 
"${it.payment.currency}:${it.payment.amount}",
+                                reserve_pub = 
it.payment.unstructuredRemittanceInformation,
+                                debit_account = getPaytoUri(
+                                    it.payment.debitorName, 
it.payment.debitorIban, it.payment.counterpartBic
+                                ),
+                                credit_account = getPaytoUri(
+                                    it.payment.creditorName, 
it.payment.creditorIban, subscriberBankAccount.first().bankCode
+                                )
                             )
                         )
-                    )
+                    }
                 }
             }
-            call.respond(history)
+            call.respond(TextContent(customConverter(history), 
ContentType.Application.Json))
             return@get
         }
     }

-- 
To stop receiving notification emails like this one, please contact
address@hidden.



reply via email to

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