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