libeufin

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

PreparedTransferApiTest.kt (8832B)


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