PreparedApiTest.kt (14709B)
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 WireTransferApiTest { 29 // GET /accounts/{USERNAME}/taler-prepared-transfer/config 30 @Test 31 fun config() = bankSetup { 32 client.get("/accounts/merchant/taler-prepared-transfer/config").assertOkJson<PreparedTransferConfig>() 33 } 34 35 // POST /accounts/{USERNAME}/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 = obj { 41 "credit_amount" to amount 42 "type" to "reserve" 43 "alg" to "EdDSA" 44 "account_pub" to pub 45 "authorization_pub" to pub 46 "authorization_sig" to CryptoUtil.eddsaSign(pub.raw, priv) 47 "recurrent" to false 48 } 49 50 val simpleSubject = TransferSubject.Simple("Taler MAP:$pub", amount) 51 52 // Valid 53 val subjects = client.post("/accounts/exchange/taler-prepared-transfer/registration") { 54 json(valid_req) 55 }.assertOkJson<SubjectResult> { 56 assertEquals(it.subjects[1], simpleSubject) 57 assertIs<TransferSubject.Uri>(it.subjects[0]) 58 }.subjects 59 60 // Idempotent 61 client.post("/accounts/exchange/taler-prepared-transfer/registration") { 62 json(valid_req) 63 }.assertOkJson<SubjectResult> { 64 assertEquals(it.subjects, subjects) 65 } 66 67 // KYC has a different withdrawal uri 68 client.post("/accounts/exchange/taler-prepared-transfer/registration") { 69 json(valid_req) { 70 "type" to "kyc" 71 } 72 }.assertOkJson<SubjectResult> { 73 assertEquals(it.subjects[1], simpleSubject) 74 val uriSubject = assertIs<TransferSubject.Uri>(it.subjects[0]) 75 assertNotEquals(subjects[0], uriSubject) 76 } 77 78 // Recurrent only has simple subject 79 client.post("/accounts/exchange/taler-prepared-transfer/registration") { 80 json(valid_req) { 81 "recurrent" to true 82 } 83 }.assertOkJson<SubjectResult> { 84 assertEquals(it.subjects, listOf(simpleSubject)) 85 } 86 87 // Bad signature 88 client.post("/accounts/exchange/taler-prepared-transfer/registration") { 89 json(valid_req) { 90 "authorization_sig" to EddsaSignature.rand() 91 } 92 }.assertConflict(TalerErrorCode.BANK_BAD_SIGNATURE) 93 94 // Not exchange 95 client.post("/accounts/merchant/taler-prepared-transfer/registration") { 96 json(valid_req) 97 }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE) 98 99 // Unknown account 100 client.post("/accounts/unknown/taler-prepared-transfer/registration") { 101 json(valid_req) 102 }.assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) 103 104 assertBalance("customer", "+KUDOS:0") 105 assertBalance("exchange", "+KUDOS:0") 106 107 // Non recurrent accept on then bounce 108 client.post("/accounts/exchange/taler-prepared-transfer/registration") { 109 json(valid_req) { 110 "type" to "reserve" 111 } 112 }.assertOkJson<SubjectResult> { 113 val uuid = (it.subjects[0] as? TransferSubject.Uri)!!.uri.substringAfterLast('/') 114 client.postA("/accounts/customer/withdrawals/$uuid/confirm").assertNoContent() // reserve 115 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce 116 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce 117 assertBalance("customer", "-KUDOS:1") 118 assertBalance("exchange", "+KUDOS:1") 119 } 120 121 // Withdrawal is aborted on completion 122 client.post("/accounts/exchange/taler-prepared-transfer/registration") { 123 json(valid_req) { 124 "type" to "kyc" 125 } 126 }.assertOkJson<SubjectResult> { 127 val uuid = (it.subjects[0] as? TransferSubject.Uri)!!.uri.substringAfterLast('/') 128 println("UUID $uuid") 129 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // kyc 130 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce 131 client.postA("/accounts/customer/withdrawals/$uuid/confirm") 132 .assertConflict(TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT) // aborted 133 assertBalance("customer", "-KUDOS:2") 134 assertBalance("exchange", "+KUDOS:2") 135 } 136 137 // Recurrent accept one and delay others 138 val newKey = EddsaPublicKey.randEdsaKey() 139 client.post("/accounts/exchange/taler-prepared-transfer/registration") { 140 json(valid_req) { 141 "account_pub" to newKey 142 "authorization_sig" to CryptoUtil.eddsaSign(newKey.raw, priv) 143 "recurrent" to true 144 } 145 } 146 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // reserve 147 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending 148 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending 149 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending 150 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending 151 assertBalance("customer", "-KUDOS:7") 152 assertBalance("exchange", "+KUDOS:7") 153 154 // Complete pending on recurrent update 155 val kycKey = EddsaPublicKey.randEdsaKey() 156 client.post("/accounts/exchange/taler-prepared-transfer/registration") { 157 json(valid_req) { 158 "type" to "kyc" 159 "account_pub" to kycKey 160 "authorization_sig" to CryptoUtil.eddsaSign(kycKey.raw, priv) 161 "recurrent" to true 162 } 163 }.assertOkJson<SubjectResult>() 164 client.post("/accounts/exchange/taler-prepared-transfer/registration") { 165 json(valid_req) { 166 "type" to "reserve" 167 "account_pub" to kycKey 168 "authorization_sig" to CryptoUtil.eddsaSign(kycKey.raw, priv) 169 "recurrent" to true 170 } 171 }.assertOkJson<SubjectResult>() 172 assertBalance("customer", "-KUDOS:7") 173 assertBalance("exchange", "+KUDOS:7") 174 175 // Kyc key reuse keep pending ones 176 tx("customer", "KUDOS:1", "exchange", "Taler KYC:$kycKey") 177 assertBalance("customer", "-KUDOS:8") 178 assertBalance("exchange", "+KUDOS:8") 179 180 // Switching to non recurrent cancel pending 181 client.post("/accounts/exchange/taler-prepared-transfer/registration") { 182 json(valid_req) { 183 "type" to "kyc" 184 "account_pub" to kycKey 185 "authorization_sig" to CryptoUtil.eddsaSign(kycKey.raw, priv) 186 } 187 }.assertOkJson<SubjectResult>() 188 assertBalance("customer", "-KUDOS:6") 189 assertBalance("exchange", "+KUDOS:6") 190 191 // Check authorization field in incoming history 192 val (testPriv, testAuth) = EddsaPublicKey.randEdsaKeyPair() 193 val testKey = EddsaPublicKey.randEdsaKey() 194 val testSig = CryptoUtil.eddsaSign(testKey.raw, testPriv) 195 val qr = subjectFmtQrBill(testAuth) 196 client.post("/accounts/exchange/taler-prepared-transfer/registration") { 197 json(valid_req) { 198 "account_pub" to testKey 199 "authorization_pub" to testAuth 200 "authorization_sig" to testSig 201 "recurrent" to true 202 } 203 }.assertOkJson<SubjectResult>() 204 tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth") 205 tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth") 206 tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth") 207 client.post("/accounts/exchange/taler-prepared-transfer/registration") { 208 json(valid_req) { 209 "type" to "kyc" 210 "account_pub" to testKey 211 "authorization_pub" to testAuth 212 "authorization_sig" to testSig 213 "recurrent" to true 214 } 215 }.assertOkJson<SubjectResult>() 216 val otherPub = EddsaPublicKey.randEdsaKey() 217 val otherSig = CryptoUtil.eddsaSign(otherPub.raw, testPriv) 218 client.post("/accounts/exchange/taler-prepared-transfer/registration") { 219 json(valid_req) { 220 "type" to "reserve" 221 "account_pub" to otherPub 222 "authorization_pub" to testAuth 223 "authorization_sig" to otherSig 224 "recurrent" to true 225 } 226 }.assertOkJson<SubjectResult>() 227 val lastPub = EddsaPublicKey.randEdsaKey() 228 tx("customer", "KUDOS:0.1", "exchange", "Taler $lastPub") 229 tx("customer", "KUDOS:0.1", "exchange", "Taler KYC:$lastPub") 230 val history = client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?limit=-5") 231 .assertOkJson<IncomingHistory>().incoming_transactions.map { 232 when (it) { 233 is IncomingKycAuthTransaction -> Triple(it.account_pub, it.authorization_pub, it.authorization_sig) 234 is IncomingReserveTransaction -> Triple(it.reserve_pub, it.authorization_pub, it.authorization_sig) 235 else -> throw UnsupportedOperationException() 236 } 237 } 238 assertContentEquals(history, listOf( 239 Triple(lastPub, null, null), 240 Triple(lastPub, null, null), 241 Triple(otherPub, testAuth, otherSig), 242 Triple(testKey, testAuth, testSig), 243 Triple(testKey, testAuth, testSig) 244 )) 245 } 246 247 // DELETE /accounts/{USERNAME}/taler-prepared-transfer/registration 248 @Test 249 fun unregistration() = bankSetup { 250 val (priv, pub) = EddsaPublicKey.randEdsaKeyPair() 251 val valid_req = obj { 252 "credit_amount" to "KUDOS:1" 253 "type" to "reserve" 254 "alg" to "EdDSA" 255 "account_pub" to pub 256 "authorization_pub" to pub 257 "authorization_sig" to CryptoUtil.eddsaSign(pub.raw, priv) 258 "recurrent" to false 259 } 260 261 // Unknown 262 client.delete("/accounts/exchange/taler-prepared-transfer/registration") { 263 val now = Instant.now().toString() 264 json { 265 "timestamp" to now 266 "authorization_pub" to pub 267 "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv) 268 } 269 }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) 270 271 // Know 272 client.post("/accounts/exchange/taler-prepared-transfer/registration") { 273 json(valid_req) 274 }.assertOkJson<SubjectResult>() 275 client.delete("/accounts/exchange/taler-prepared-transfer/registration") { 276 val now = Instant.now().toString() 277 json { 278 "timestamp" to now 279 "authorization_pub" to pub 280 "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv) 281 } 282 }.assertNoContent() 283 284 // Idempotent 285 client.delete("/accounts/exchange/taler-prepared-transfer/registration") { 286 val now = Instant.now().toString() 287 json { 288 "timestamp" to now 289 "authorization_pub" to pub 290 "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv) 291 } 292 }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) 293 294 // Bad signature 295 client.delete("/accounts/exchange/taler-prepared-transfer/registration") { 296 val now = Instant.now().toString() 297 json { 298 "timestamp" to now 299 "authorization_pub" to pub 300 "authorization_sig" to CryptoUtil.eddsaSign("lol".toByteArray(), priv) 301 } 302 }.assertConflict(TalerErrorCode.BANK_BAD_SIGNATURE) 303 304 // Old timestamp 305 client.delete("/accounts/exchange/taler-prepared-transfer/registration") { 306 val now = Instant.now().minusSeconds(1000000).toString() 307 json { 308 "timestamp" to now 309 "authorization_pub" to pub 310 "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv) 311 } 312 }.assertConflict(TalerErrorCode.BANK_OLD_TIMESTAMP) 313 314 // Unknown bounce 315 assertBalance("customer", "+KUDOS:0") 316 assertBalance("exchange", "+KUDOS:0") 317 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce 318 assertBalance("customer", "+KUDOS:0") 319 assertBalance("exchange", "+KUDOS:0") 320 321 // Pending bounced after deletion 322 val newKey = EddsaPublicKey.randEdsaKey() 323 client.post("/accounts/exchange/taler-prepared-transfer/registration") { 324 json(valid_req) { 325 "account_pub" to newKey 326 "authorization_sig" to CryptoUtil.eddsaSign(newKey.raw, priv) 327 "recurrent" to true 328 } 329 }.assertOkJson<SubjectResult>() 330 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // reserve 331 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending 332 tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending 333 assertBalance("customer", "-KUDOS:3") 334 assertBalance("exchange", "+KUDOS:3") 335 client.delete("/accounts/exchange/taler-prepared-transfer/registration") { 336 val now = Instant.now().toString() 337 json { 338 "timestamp" to now 339 "authorization_pub" to pub 340 "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv) 341 } 342 }.assertNoContent() 343 assertBalance("customer", "-KUDOS:1") 344 assertBalance("exchange", "+KUDOS:1") 345 } 346 }