PreparedTransferApiTest.kt (12205B)
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.common.test.* 25 import java.time.Instant 26 import kotlin.test.* 27 28 class PreparedTransferApiTest { 29 // GET /taler-prepared-transfer/config 30 @Test 31 fun config() = bankSetup { 32 client.get("/taler-prepared-transfer/config").assertOkJson<PreparedTransferConfig>() 33 } 34 35 // POST /taler-prepared-transfer/registration 36 @Test 37 fun registration() = bankSetup { 38 val (priv, pub) = EddsaPublicKey.randEdsaKeyPair() 39 val amount = TalerAmount("KUDOS:1") 40 val valid_req = SubjectRequest( 41 exchangePayto, 42 TransferType.reserve, 43 false, 44 amount, 45 PublicKeyAlg.EdDSA, 46 pub, 47 pub, 48 EddsaSignature.rand() 49 ) 50 51 val simpleSubject = TransferSubject.Simple("Taler MAP:$pub", amount) 52 53 // Valid 54 val subjects = client.post("/taler-prepared-transfer/registration") { 55 json(valid_req.sign(priv)) 56 }.assertOkJson<SubjectResult> { 57 assertEquals(it.subjects[1], simpleSubject) 58 assertIs<TransferSubject.Uri>(it.subjects[0]) 59 }.subjects 60 61 // Idempotent 62 client.post("/taler-prepared-transfer/registration") { 63 json(valid_req.sign(priv)) 64 }.assertOkJson<SubjectResult> { 65 assertEquals(it.subjects, subjects) 66 } 67 68 // KYC has a different withdrawal uri 69 client.post("/taler-prepared-transfer/registration") { 70 json(valid_req.copy(type = TransferType.kyc).sign(priv)) 71 }.assertOkJson<SubjectResult> { 72 assertEquals(it.subjects[1], simpleSubject) 73 val uriSubject = assertIs<TransferSubject.Uri>(it.subjects[0]) 74 assertNotEquals(subjects[0], uriSubject) 75 } 76 77 // Recurrent only has simple subject 78 client.post("/taler-prepared-transfer/registration") { 79 json(valid_req.copy(recurrent = true).sign(priv)) 80 }.assertOkJson<SubjectResult> { 81 assertEquals(it.subjects, listOf(simpleSubject)) 82 } 83 84 // Bad signature 85 client.post("/taler-prepared-transfer/registration") { 86 json(valid_req) 87 }.assertForbidden(TalerErrorCode.BANK_BAD_SIGNATURE) 88 89 // Not exchange 90 client.post("/taler-prepared-transfer/registration") { 91 json(valid_req.copy(credit_account = merchantPayto).sign(priv)) 92 }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE) 93 94 // Unknown account 95 client.post("/taler-prepared-transfer/registration") { 96 json(valid_req.copy(credit_account = unknownPayto).sign(priv)) 97 }.assertConflict(TalerErrorCode.BANK_UNKNOWN_CREDITOR) 98 99 assertBalance("customer", "+KUDOS:0") 100 assertBalance("exchange", "+KUDOS:0") 101 102 // Non recurrent accept on then bounce 103 client.post("/taler-prepared-transfer/registration") { 104 json(valid_req.sign(priv)) 105 }.assertOkJson<SubjectResult> { 106 val uuid = (it.subjects[0] as? TransferSubject.Uri)!!.uri.substringAfterLast('/') 107 client.postA("/accounts/customer/withdrawals/$uuid/confirm").assertNoContent() // reserve 108 client.postA("/accounts/merchant/withdrawals/$uuid/confirm") 109 .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) 110 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce 111 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce 112 assertBalance("customer", "-KUDOS:1") 113 assertBalance("exchange", "+KUDOS:1") 114 } 115 116 // Withdrawal is aborted on completion 117 client.post("/taler-prepared-transfer/registration") { 118 json(valid_req.copy(type = TransferType.kyc).sign(priv)) 119 }.assertOkJson<SubjectResult> { 120 val uuid = (it.subjects[0] as? TransferSubject.Uri)!!.uri.substringAfterLast('/') 121 println("UUID $uuid") 122 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // kyc 123 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce 124 client.postA("/accounts/customer/withdrawals/$uuid/confirm") 125 .assertConflict(TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT) // aborted 126 assertBalance("customer", "-KUDOS:2") 127 assertBalance("exchange", "+KUDOS:2") 128 } 129 130 // Recurrent accept one and delay others 131 val newKey = EddsaPublicKey.randEdsaKey() 132 client.post("/taler-prepared-transfer/registration") { 133 json(valid_req.copy(account_pub = newKey, recurrent = true).sign(priv)) 134 } 135 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // reserve 136 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending 137 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending 138 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending 139 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending 140 assertBalance("customer", "-KUDOS:7") 141 assertBalance("exchange", "+KUDOS:7") 142 143 // Complete pending on recurrent update 144 val kycKey = EddsaPublicKey.randEdsaKey() 145 client.post("/taler-prepared-transfer/registration") { 146 json(valid_req.copy(type= TransferType.kyc, account_pub = kycKey, recurrent = true).sign(priv)) 147 }.assertOkJson<SubjectResult>() 148 client.post("/taler-prepared-transfer/registration") { 149 json(valid_req.copy(account_pub = kycKey, recurrent = true).sign(priv)) 150 }.assertOkJson<SubjectResult>() 151 assertBalance("customer", "-KUDOS:7") 152 assertBalance("exchange", "+KUDOS:7") 153 154 // Kyc key reuse keep pending ones 155 tx("customer", "KUDOS:1", "exchange", "Taler KYC:$kycKey") 156 assertBalance("customer", "-KUDOS:8") 157 assertBalance("exchange", "+KUDOS:8") 158 159 // Switching to non recurrent cancel pending 160 client.post("/taler-prepared-transfer/registration") { 161 json(valid_req.copy(type= TransferType.kyc, account_pub = kycKey).sign(priv)) 162 }.assertOkJson<SubjectResult>() 163 assertBalance("customer", "-KUDOS:6") 164 assertBalance("exchange", "+KUDOS:6") 165 166 // Check authorization field in incoming history 167 val (testPriv, testAuth) = EddsaPublicKey.randEdsaKeyPair() 168 val testKey = EddsaPublicKey.randEdsaKey() 169 val qr = subjectFmtQrBill(testAuth) 170 client.post("/taler-prepared-transfer/registration") { 171 json(valid_req.copy(account_pub=testKey, authorization_pub=testAuth, recurrent=true).sign(testPriv)) 172 }.assertOkJson<SubjectResult>() 173 tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth") 174 tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth") 175 tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth") 176 client.post("/taler-prepared-transfer/registration") { 177 json(valid_req.copy(type=TransferType.kyc, account_pub=testKey, authorization_pub=testAuth, recurrent=true).sign(testPriv)) 178 }.assertOkJson<SubjectResult>() 179 val otherPub = EddsaPublicKey.randEdsaKey() 180 client.post("/taler-prepared-transfer/registration") { 181 json(valid_req.copy(account_pub=otherPub, authorization_pub=testAuth, recurrent=true).sign(testPriv)) 182 }.assertOkJson<SubjectResult>() 183 val lastPub = EddsaPublicKey.randEdsaKey() 184 tx("customer", "KUDOS:0.1", "exchange", "Taler $lastPub") 185 tx("customer", "KUDOS:0.1", "exchange", "Taler KYC:$lastPub") 186 val history = client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?limit=-5") 187 .assertOkJson<IncomingHistory>().incoming_transactions.map { 188 when (it) { 189 is IncomingKycAuthTransaction -> Pair(it.account_pub, it.authorization_pub) 190 is IncomingReserveTransaction -> Pair(it.reserve_pub, it.authorization_pub) 191 else -> throw UnsupportedOperationException() 192 } 193 } 194 assertContentEquals(history, listOf( 195 Pair(lastPub, null), 196 Pair(lastPub, null), 197 Pair(otherPub, testAuth), 198 Pair(testKey, testAuth), 199 Pair(testKey, testAuth) 200 )) 201 } 202 203 // DELETE /taler-prepared-transfer/registration 204 @Test 205 fun unregistration() = bankSetup { 206 val (priv, pub) = EddsaPublicKey.randEdsaKeyPair() 207 val valid_req = SubjectRequest( 208 exchangePayto, 209 TransferType.reserve, 210 false, 211 TalerAmount("KUDOS:1"), 212 PublicKeyAlg.EdDSA, 213 pub, 214 pub, 215 EddsaSignature.rand() 216 ).sign(priv) 217 218 val now = TalerTimestamp(Instant.now()) 219 val req = Unregistration( 220 now, 221 pub, 222 EddsaSignature.rand() 223 ).sign(priv) 224 225 // Unknown 226 client.post("/taler-prepared-transfer/unregistration") { 227 json(req) 228 }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) 229 230 // Know 231 client.post("/taler-prepared-transfer/registration") { 232 json(valid_req) 233 }.assertOkJson<SubjectResult>() 234 client.post("/taler-prepared-transfer/unregistration") { 235 json(req) 236 }.assertNoContent() 237 238 // Idempotent 239 client.post("/taler-prepared-transfer/unregistration") { 240 json(req) 241 }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) 242 243 // Bad signature 244 client.post("/taler-prepared-transfer/unregistration") { 245 json( 246 Unregistration( 247 now, 248 pub, 249 EddsaSignature.rand() 250 ) 251 ) 252 }.assertForbidden(TalerErrorCode.BANK_BAD_SIGNATURE) 253 254 // Old timestamp 255 client.post("/taler-prepared-transfer/unregistration") { 256 json( 257 Unregistration( 258 TalerTimestamp(Instant.now().minusSeconds(1000000)), 259 pub, 260 EddsaSignature.rand() 261 ).sign(priv) 262 ) 263 }.assertConflict(TalerErrorCode.BANK_OLD_TIMESTAMP) 264 265 // Unknown bounce 266 assertBalance("customer", "+KUDOS:0") 267 assertBalance("exchange", "+KUDOS:0") 268 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce 269 assertBalance("customer", "+KUDOS:0") 270 assertBalance("exchange", "+KUDOS:0") 271 272 // Pending bounced after deletion 273 val newKey = EddsaPublicKey.randEdsaKey() 274 client.post("/taler-prepared-transfer/registration") { 275 json(valid_req.copy(account_pub=newKey, recurrent=true).sign(priv)) 276 }.assertOkJson<SubjectResult>() 277 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // reserve 278 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending 279 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending 280 assertBalance("customer", "-KUDOS:3") 281 assertBalance("exchange", "+KUDOS:3") 282 client.post("/taler-prepared-transfer/unregistration") { 283 json(req) 284 }.assertNoContent() 285 assertBalance("customer", "-KUDOS:1") 286 assertBalance("exchange", "+KUDOS:1") 287 } 288 }