PreparedTransferApiTest.kt (12056B)
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 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce 109 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce 110 assertBalance("customer", "-KUDOS:1") 111 assertBalance("exchange", "+KUDOS:1") 112 } 113 114 // Withdrawal is aborted on completion 115 client.post("/taler-prepared-transfer/registration") { 116 json(valid_req.copy(type = TransferType.kyc).sign(priv)) 117 }.assertOkJson<SubjectResult> { 118 val uuid = (it.subjects[0] as? TransferSubject.Uri)!!.uri.substringAfterLast('/') 119 println("UUID $uuid") 120 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // kyc 121 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce 122 client.postA("/accounts/customer/withdrawals/$uuid/confirm") 123 .assertConflict(TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT) // aborted 124 assertBalance("customer", "-KUDOS:2") 125 assertBalance("exchange", "+KUDOS:2") 126 } 127 128 // Recurrent accept one and delay others 129 val newKey = EddsaPublicKey.randEdsaKey() 130 client.post("/taler-prepared-transfer/registration") { 131 json(valid_req.copy(account_pub = newKey, recurrent = true).sign(priv)) 132 } 133 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // reserve 134 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending 135 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending 136 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending 137 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending 138 assertBalance("customer", "-KUDOS:7") 139 assertBalance("exchange", "+KUDOS:7") 140 141 // Complete pending on recurrent update 142 val kycKey = EddsaPublicKey.randEdsaKey() 143 client.post("/taler-prepared-transfer/registration") { 144 json(valid_req.copy(type= TransferType.kyc, account_pub = kycKey, recurrent = true).sign(priv)) 145 }.assertOkJson<SubjectResult>() 146 client.post("/taler-prepared-transfer/registration") { 147 json(valid_req.copy(account_pub = kycKey, recurrent = true).sign(priv)) 148 }.assertOkJson<SubjectResult>() 149 assertBalance("customer", "-KUDOS:7") 150 assertBalance("exchange", "+KUDOS:7") 151 152 // Kyc key reuse keep pending ones 153 tx("customer", "KUDOS:1", "exchange", "Taler KYC:$kycKey") 154 assertBalance("customer", "-KUDOS:8") 155 assertBalance("exchange", "+KUDOS:8") 156 157 // Switching to non recurrent cancel pending 158 client.post("/taler-prepared-transfer/registration") { 159 json(valid_req.copy(type= TransferType.kyc, account_pub = kycKey).sign(priv)) 160 }.assertOkJson<SubjectResult>() 161 assertBalance("customer", "-KUDOS:6") 162 assertBalance("exchange", "+KUDOS:6") 163 164 // Check authorization field in incoming history 165 val (testPriv, testAuth) = EddsaPublicKey.randEdsaKeyPair() 166 val testKey = EddsaPublicKey.randEdsaKey() 167 val qr = subjectFmtQrBill(testAuth) 168 client.post("/taler-prepared-transfer/registration") { 169 json(valid_req.copy(account_pub=testKey, authorization_pub=testAuth, recurrent=true).sign(testPriv)) 170 }.assertOkJson<SubjectResult>() 171 tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth") 172 tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth") 173 tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth") 174 client.post("/taler-prepared-transfer/registration") { 175 json(valid_req.copy(type=TransferType.kyc, account_pub=testKey, authorization_pub=testAuth, recurrent=true).sign(testPriv)) 176 }.assertOkJson<SubjectResult>() 177 val otherPub = EddsaPublicKey.randEdsaKey() 178 client.post("/taler-prepared-transfer/registration") { 179 json(valid_req.copy(account_pub=otherPub, authorization_pub=testAuth, recurrent=true).sign(testPriv)) 180 }.assertOkJson<SubjectResult>() 181 val lastPub = EddsaPublicKey.randEdsaKey() 182 tx("customer", "KUDOS:0.1", "exchange", "Taler $lastPub") 183 tx("customer", "KUDOS:0.1", "exchange", "Taler KYC:$lastPub") 184 val history = client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?limit=-5") 185 .assertOkJson<IncomingHistory>().incoming_transactions.map { 186 when (it) { 187 is IncomingKycAuthTransaction -> Pair(it.account_pub, it.authorization_pub) 188 is IncomingReserveTransaction -> Pair(it.reserve_pub, it.authorization_pub) 189 else -> throw UnsupportedOperationException() 190 } 191 } 192 assertContentEquals(history, listOf( 193 Pair(lastPub, null), 194 Pair(lastPub, null), 195 Pair(otherPub, testAuth), 196 Pair(testKey, testAuth), 197 Pair(testKey, testAuth) 198 )) 199 } 200 201 // DELETE /taler-prepared-transfer/registration 202 @Test 203 fun unregistration() = bankSetup { 204 val (priv, pub) = EddsaPublicKey.randEdsaKeyPair() 205 val valid_req = SubjectRequest( 206 exchangePayto, 207 TransferType.reserve, 208 false, 209 TalerAmount("KUDOS:1"), 210 PublicKeyAlg.EdDSA, 211 pub, 212 pub, 213 EddsaSignature.rand() 214 ).sign(priv) 215 216 val now = TalerTimestamp(Instant.now()) 217 val req = Unregistration( 218 now, 219 pub, 220 EddsaSignature.rand() 221 ).sign(priv) 222 223 // Unknown 224 client.post("/taler-prepared-transfer/unregistration") { 225 json(req) 226 }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) 227 228 // Know 229 client.post("/taler-prepared-transfer/registration") { 230 json(valid_req) 231 }.assertOkJson<SubjectResult>() 232 client.post("/taler-prepared-transfer/unregistration") { 233 json(req) 234 }.assertNoContent() 235 236 // Idempotent 237 client.post("/taler-prepared-transfer/unregistration") { 238 json(req) 239 }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) 240 241 // Bad signature 242 client.post("/taler-prepared-transfer/unregistration") { 243 json( 244 Unregistration( 245 now, 246 pub, 247 EddsaSignature.rand() 248 ) 249 ) 250 }.assertForbidden(TalerErrorCode.BANK_BAD_SIGNATURE) 251 252 // Old timestamp 253 client.post("/taler-prepared-transfer/unregistration") { 254 json( 255 Unregistration( 256 TalerTimestamp(Instant.now().minusSeconds(1000000)), 257 pub, 258 EddsaSignature.rand() 259 ).sign(priv) 260 ) 261 }.assertConflict(TalerErrorCode.BANK_OLD_TIMESTAMP) 262 263 // Unknown bounce 264 assertBalance("customer", "+KUDOS:0") 265 assertBalance("exchange", "+KUDOS:0") 266 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce 267 assertBalance("customer", "+KUDOS:0") 268 assertBalance("exchange", "+KUDOS:0") 269 270 // Pending bounced after deletion 271 val newKey = EddsaPublicKey.randEdsaKey() 272 client.post("/taler-prepared-transfer/registration") { 273 json(valid_req.copy(account_pub=newKey, recurrent=true).sign(priv)) 274 }.assertOkJson<SubjectResult>() 275 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // reserve 276 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending 277 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending 278 assertBalance("customer", "-KUDOS:3") 279 assertBalance("exchange", "+KUDOS:3") 280 client.post("/taler-prepared-transfer/unregistration") { 281 json(req) 282 }.assertNoContent() 283 assertBalance("customer", "-KUDOS:1") 284 assertBalance("exchange", "+KUDOS:1") 285 } 286 }