libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

PreparedTransferApiTest.kt (7778B)


      1 /*
      2  * This file is part of LibEuFin.
      3  * Copyright (C) 2026 Taler Systems S.A.
      4 
      5  * LibEuFin is free software; you can redistribute it and/or modify
      6  * it under the terms of the GNU Affero General Public License as
      7  * published by the Free Software Foundation; either version 3, or
      8  * (at your option) any later version.
      9 
     10  * LibEuFin is distributed in the hope that it will be useful, but
     11  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
     12  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
     13  * Public License for more details.
     14 
     15  * You should have received a copy of the GNU Affero General Public
     16  * License along with LibEuFin; see the file COPYING.  If not, see
     17  * <http://www.gnu.org/licenses/>
     18  */
     19 
     20 import io.ktor.client.request.*
     21 import org.junit.Test
     22 import tech.libeufin.common.*
     23 import tech.libeufin.common.crypto.CryptoUtil
     24 import tech.libeufin.nexus.*
     25 import tech.libeufin.nexus.cli.*
     26 import java.time.Instant
     27 import kotlin.test.*
     28 
     29 class PreparedTransferApiTest {
     30     // GET /taler-prepared-transfer/config
     31     @Test
     32     fun config() = serverSetup {
     33         client.get("/taler-prepared-transfer/config").assertOkJson<PreparedTransferConfig>()
     34     }
     35 
     36     // POST /taler-prepared-transfer/registration
     37     @Test
     38     fun registration() = serverSetup { db ->
     39         val (priv, pub) = EddsaPublicKey.randEdsaKeyPair()
     40         val amount = TalerAmount("KUDOS:55")
     41         val valid_req = obj {
     42             "credit_amount" to amount
     43             "type" to "reserve"
     44             "alg" to "EdDSA"
     45             "account_pub" to pub
     46             "authorization_pub" to pub
     47             "authorization_sig" to CryptoUtil.eddsaSign(pub.raw, priv)
     48             "recurrent" to false
     49         }
     50 
     51         val subjects = listOf(
     52             TransferSubject.QrBill(subjectFmtQrBill(pub), amount),
     53             TransferSubject.Simple("Taler MAP:$pub", amount),
     54         )
     55 
     56         // Valid
     57         client.post("/taler-prepared-transfer/registration") {
     58             json(valid_req)
     59         }.assertOkJson<SubjectResult> {
     60             assertEquals(it.subjects, subjects)
     61         }
     62 
     63         // Idempotent
     64         client.post("/taler-prepared-transfer/registration") {
     65             json(valid_req)
     66         }.assertOkJson<SubjectResult> {
     67             assertEquals(it.subjects, subjects)
     68         }
     69 
     70         // Bad signature
     71         client.post("/taler-prepared-transfer/registration") {
     72             json(valid_req) {
     73                 "authorization_sig" to EddsaSignature.rand()
     74             }
     75         }.assertConflict(TalerErrorCode.BANK_BAD_SIGNATURE)
     76 
     77         // Check authorization field in incoming history
     78         val (testPriv, testAuth) = EddsaPublicKey.randEdsaKeyPair()
     79         val testKey = EddsaPublicKey.randEdsaKey()
     80         val testSig = CryptoUtil.eddsaSign(testKey.raw, testPriv)
     81         val qr = subjectFmtQrBill(testAuth)
     82         client.post("/taler-prepared-transfer/registration") {
     83             json(valid_req) {
     84                 "account_pub" to testKey
     85                 "authorization_pub" to testAuth
     86                 "authorization_sig" to testSig
     87                 "recurrent" to true
     88             }
     89         }.assertOkJson<SubjectResult>()
     90         val cfg = NexusIngestConfig.default(AccountType.exchange)
     91         registerIncomingPayment(db, cfg, genInPay(qr))
     92         registerIncomingPayment(db, cfg, genInPay(qr))
     93         registerIncomingPayment(db, cfg, genInPay(qr))
     94         client.post("/taler-prepared-transfer/registration") {
     95             json(valid_req) {
     96                 "type" to "kyc"
     97                 "account_pub" to testKey
     98                 "authorization_pub" to testAuth
     99                 "authorization_sig" to testSig
    100                 "recurrent" to true
    101             }
    102         }.assertOkJson<SubjectResult>()
    103         val otherPub = EddsaPublicKey.randEdsaKey()
    104         val otherSig = CryptoUtil.eddsaSign(otherPub.raw, testPriv)
    105         client.post("/taler-prepared-transfer/registration") {
    106             json(valid_req) {
    107                 "type" to "reserve"
    108                 "account_pub" to otherPub
    109                 "authorization_pub" to testAuth
    110                 "authorization_sig" to otherSig
    111                 "recurrent" to true
    112             }
    113         }.assertOkJson<SubjectResult>()
    114         val lastPub = EddsaPublicKey.randEdsaKey()
    115         talerableIn(db, reserve_pub=lastPub)
    116         talerableKycIn(db, account_pub=lastPub)
    117         val history = client.getA("/taler-wire-gateway/history/incoming?limit=-5")
    118             .assertOkJson<IncomingHistory>().incoming_transactions.map {
    119                 when (it) {
    120                     is IncomingKycAuthTransaction -> Triple(it.account_pub, it.authorization_pub, it.authorization_sig)
    121                     is IncomingReserveTransaction -> Triple(it.reserve_pub, it.authorization_pub, it.authorization_sig)
    122                     else -> throw UnsupportedOperationException()
    123                 }
    124             }
    125         assertContentEquals(history, listOf(
    126             Triple(lastPub, null, null),
    127             Triple(lastPub, null, null),
    128             Triple(otherPub, testAuth, otherSig),
    129             Triple(testKey, testAuth, testSig),
    130             Triple(testKey, testAuth, testSig)
    131         ))
    132     }
    133 
    134     // DELETE /taler-prepared-transfer/registration
    135     @Test
    136     fun unregistration() = serverSetup {
    137         val (priv, pub) = EddsaPublicKey.randEdsaKeyPair()
    138 
    139         // Unknown
    140         client.delete("/taler-prepared-transfer/registration") {
    141             val now = Instant.now().toString()
    142             json {
    143                 "timestamp" to now
    144                 "authorization_pub" to pub
    145                 "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv)
    146             }
    147         }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
    148                 
    149         // Know
    150         client.post("/taler-prepared-transfer/registration") {
    151             json {
    152                 "credit_amount" to "KUDOS:55"
    153                 "type" to "reserve"
    154                 "alg" to "EdDSA"
    155                 "account_pub" to pub
    156                 "authorization_pub" to pub
    157                 "authorization_sig" to CryptoUtil.eddsaSign(pub.raw, priv)
    158                 "recurrent" to false
    159             }
    160         }.assertOkJson<SubjectResult>()
    161         client.delete("/taler-prepared-transfer/registration") {
    162             val now = Instant.now().toString()
    163             json {
    164                 "timestamp" to now
    165                 "authorization_pub" to pub
    166                 "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv)
    167             }
    168         }.assertNoContent()
    169 
    170         // Idempotent
    171         client.delete("/taler-prepared-transfer/registration") {
    172             val now = Instant.now().toString()
    173             json {
    174                 "timestamp" to now
    175                 "authorization_pub" to pub
    176                 "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv)
    177             }
    178         }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
    179 
    180         // Bad signature
    181         client.delete("/taler-prepared-transfer/registration") {
    182             val now = Instant.now().toString()
    183             json {
    184                 "timestamp" to now
    185                 "authorization_pub" to pub
    186                 "authorization_sig" to CryptoUtil.eddsaSign("lol".toByteArray(), priv)
    187             }
    188         }.assertConflict(TalerErrorCode.BANK_BAD_SIGNATURE)
    189 
    190         // Old timestamp
    191         client.delete("/taler-prepared-transfer/registration") {
    192             val now = Instant.now().minusSeconds(1000000).toString()
    193             json {
    194                 "timestamp" to now
    195                 "authorization_pub" to pub
    196                 "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv)
    197             }
    198         }.assertConflict(TalerErrorCode.BANK_OLD_TIMESTAMP)
    199     }
    200 }