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 }