libeufin

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

PreparedTransferApiTest.kt (7566B)


      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 = SubjectRequest(
     42             Payto.parse("payto://iban/CH7789144474425692816"),
     43             TransferType.reserve,
     44             false,
     45             amount,
     46             PublicKeyAlg.EdDSA,
     47             pub,
     48             pub,
     49             EddsaSignature.rand()
     50         )
     51 
     52         val simple = listOf(TransferSubject.Simple("Taler MAP:$pub", amount))
     53         val qrs = listOf(TransferSubject.QrBill(subjectFmtQrBill(pub), amount))
     54 
     55         // Valid simple
     56         client.post("/taler-prepared-transfer/registration") {
     57             json(valid_req.sign(priv))
     58         }.assertOkJson<SubjectResult> {
     59             assertEquals(it.subjects, simple)
     60         }
     61 
     62         // Idempotent simple
     63         client.post("/taler-prepared-transfer/registration") {
     64             json(valid_req.sign(priv))
     65         }.assertOkJson<SubjectResult> {
     66             assertEquals(it.subjects, simple)
     67         }
     68 
     69         // Valid qr
     70         client.post("/taler-prepared-transfer/registration") {
     71             json(valid_req.copy(credit_account=Payto.parse("payto://iban/CH4431999123000889012")).sign(priv))
     72         }.assertOkJson<SubjectResult> {
     73             assertEquals(it.subjects, qrs)
     74         }
     75 
     76         // Idempotent qr
     77         client.post("/taler-prepared-transfer/registration") {
     78             json(valid_req.copy(credit_account=Payto.parse("payto://iban/CH4431999123000889012")).sign(priv))
     79         }.assertOkJson<SubjectResult> {
     80             assertEquals(it.subjects, qrs)
     81         }
     82 
     83         // Bad signature
     84         client.post("/taler-prepared-transfer/registration") {
     85             json(valid_req)
     86         }.assertForbidden(TalerErrorCode.BANK_BAD_SIGNATURE)
     87 
     88         // Unknown account
     89         client.post("/taler-prepared-transfer/registration") {
     90             json(valid_req.copy(credit_account=Payto.parse(grothoffPayto)).sign(priv))
     91         }.assertConflict(TalerErrorCode.BANK_UNKNOWN_CREDITOR)
     92 
     93         // Check authorization field in incoming history
     94         val (testPriv, testAuth) = EddsaPublicKey.randEdsaKeyPair()
     95         val testKey = EddsaPublicKey.randEdsaKey()
     96         val qr = subjectFmtQrBill(testAuth)
     97         client.post("/taler-prepared-transfer/registration") {
     98             json(valid_req.copy(
     99                 credit_account=Payto.parse("payto://iban/CH4431999123000889012"),
    100                 account_pub=testKey,
    101                 authorization_pub=testAuth,
    102                 recurrent=true
    103             ).sign(testPriv))
    104         }.assertOkJson<SubjectResult>()
    105         val cfg = NexusIngestConfig.default(AccountType.exchange)
    106         registerIncomingPayment(db, cfg, genInPay(qr))
    107         registerIncomingPayment(db, cfg, genInPay(qr))
    108         registerIncomingPayment(db, cfg, genInPay(qr))
    109         client.post("/taler-prepared-transfer/registration") {
    110             json(valid_req.copy(
    111                 type=TransferType.kyc,
    112                 account_pub=testKey,
    113                 authorization_pub=testAuth,
    114                 recurrent=true
    115             ).sign(testPriv))
    116         }.assertOkJson<SubjectResult>()
    117         val otherPub = EddsaPublicKey.randEdsaKey()
    118         client.post("/taler-prepared-transfer/registration") {
    119             json(valid_req.copy(
    120                 account_pub=otherPub,
    121                 authorization_pub=testAuth,
    122                 recurrent=true
    123             ).sign(testPriv))
    124         }.assertOkJson<SubjectResult>()
    125         val lastPub = EddsaPublicKey.randEdsaKey()
    126         talerableIn(db, reserve_pub=lastPub)
    127         talerableKycIn(db, account_pub=lastPub)
    128         val history = client.getA("/taler-wire-gateway/history/incoming?limit=-5")
    129             .assertOkJson<IncomingHistory>().incoming_transactions.map {
    130                 when (it) {
    131                     is IncomingKycAuthTransaction -> Pair(it.account_pub, it.authorization_pub)
    132                     is IncomingReserveTransaction -> Pair(it.reserve_pub, it.authorization_pub)
    133                     else -> throw UnsupportedOperationException()
    134                 }
    135             }
    136         assertContentEquals(history, listOf(
    137             Pair(lastPub, null),
    138             Pair(lastPub, null),
    139             Pair(otherPub, testAuth),
    140             Pair(testKey, testAuth),
    141             Pair(testKey, testAuth)
    142         ))
    143     }
    144 
    145     // DELETE /taler-prepared-transfer/registration
    146     @Test
    147     fun unregistration() = serverSetup {
    148         val (priv, pub) = EddsaPublicKey.randEdsaKeyPair()
    149         val now = TalerTimestamp(Instant.now())
    150         val req = Unregistration(
    151             now,
    152             pub,
    153             EddsaSignature.rand()
    154         ).sign(priv)
    155         
    156         // Unknown
    157         client.post("/taler-prepared-transfer/unregistration") {
    158             json(req)
    159         }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
    160 
    161         // Know
    162         client.post("/taler-prepared-transfer/registration") {
    163             json(SubjectRequest(
    164                 Payto.parse("payto://iban/CH7789144474425692816"),
    165                 TransferType.reserve,
    166                 false,
    167                 TalerAmount("KUDOS:55"),
    168                 PublicKeyAlg.EdDSA,
    169                 pub,
    170                 pub,
    171                 EddsaSignature.rand()
    172             ).sign(priv))
    173         }.assertOkJson<SubjectResult>()
    174         client.post("/taler-prepared-transfer/unregistration") {
    175             json(req)
    176         }.assertNoContent()
    177 
    178         // Idempotent
    179         client.post("/taler-prepared-transfer/unregistration") {
    180             json(req)
    181         }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND)
    182 
    183         // Bad signature
    184         client.post("/taler-prepared-transfer/unregistration") {
    185             json(Unregistration(
    186                 now,
    187                 pub,
    188                 EddsaSignature.rand()
    189             ))
    190         }.assertForbidden(TalerErrorCode.BANK_BAD_SIGNATURE)
    191 
    192         // Old timestamp
    193         client.post("/taler-prepared-transfer/unregistration") {
    194             json(
    195                 Unregistration(
    196                     TalerTimestamp(Instant.now().minusSeconds(1000000)),
    197                     pub,
    198                     EddsaSignature.rand()
    199                 ).sign(priv)
    200             )
    201         }.assertConflict(TalerErrorCode.BANK_OLD_TIMESTAMP)
    202     }
    203 }