BankIntegrationApiTest.kt (12056B)
1 /* 2 * This file is part of LibEuFin. 3 * Copyright (C) 2023 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.bank.BankAccountCreateWithdrawalResponse 23 import tech.libeufin.bank.BankWithdrawalOperationPostResponse 24 import tech.libeufin.bank.BankWithdrawalOperationStatus 25 import tech.libeufin.bank.WithdrawalStatus 26 import tech.libeufin.common.* 27 import tech.libeufin.common.test.* 28 import java.util.* 29 import kotlin.test.assertEquals 30 31 class BankIntegrationApiTest { 32 // GET /taler-integration/config 33 @Test 34 fun config() = bankSetup { 35 client.get("/taler-integration/config").assertOk() 36 } 37 38 // GET /taler-integration/withdrawal-operation/UUID 39 @Test 40 fun get() = bankSetup { 41 // Check OK 42 for (valid in listOf( 43 Pair(null, null), 44 Pair("KUDOS:1.0", null), 45 Pair(null, "KUDOS:2.0") , 46 Pair("KUDOS:3.0", "KUDOS:4.0") 47 )) { 48 val amount = valid.first?.run(::TalerAmount) 49 val suggested = valid.second?.run(::TalerAmount) 50 client.postA("/accounts/merchant/withdrawals") { 51 json { 52 "amount" to amount 53 "suggested_amount" to suggested 54 } 55 }.assertOkJson<BankAccountCreateWithdrawalResponse> { 56 val uuid = it.taler_withdraw_uri.split("/").last() 57 client.get("/taler-integration/withdrawal-operation/$uuid") 58 .assertOkJson<BankWithdrawalOperationStatus> { 59 assert(!it.selection_done) 60 assert(!it.aborted) 61 assert(!it.transfer_done) 62 assertEquals(it.card_fees, TalerAmount.zero("KUDOS")) 63 assertEquals(it.min_amount, TalerAmount.zero("KUDOS")) 64 assertEquals(it.max_amount, TalerAmount("KUDOS:10")) 65 assertEquals(amount, it.amount) 66 assertEquals(suggested, it.suggested_amount) 67 assertEquals(listOf("iban"), it.wire_types) 68 assertEquals("KUDOS", it.currency) 69 } 70 } 71 } 72 73 // Check polling 74 statusRoutine<BankWithdrawalOperationStatus>("/taler-integration/withdrawal-operation") { it.status } 75 76 // Check unknown 77 client.get("/taler-integration/withdrawal-operation/${UUID.randomUUID()}") 78 .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) 79 80 // Check bad UUID 81 client.get("/taler-integration/withdrawal-operation/chocolate") 82 .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) 83 } 84 85 // POST /taler-integration/withdrawal-operation/UUID 86 @Test 87 fun select() = bankSetup { 88 val reserve_pub = EddsaPublicKey.rand() 89 val req = obj { 90 "reserve_pub" to reserve_pub 91 "selected_exchange" to exchangePayto.canonical 92 } 93 94 // Check bad UUID 95 client.post("/taler-integration/withdrawal-operation/chocolate") { 96 json(req) 97 }.assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) 98 99 // Check unknown 100 client.post("/taler-integration/withdrawal-operation/${UUID.randomUUID()}") { 101 json(req) 102 }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) 103 104 client.postA("/accounts/merchant/withdrawals") { 105 json { "amount" to "KUDOS:1" } 106 }.assertOkJson<BankAccountCreateWithdrawalResponse> { 107 val uuid = it.withdrawal_id 108 109 // Check OK 110 client.post("/taler-integration/withdrawal-operation/$uuid") { 111 json(req) 112 }.assertOkJson<BankWithdrawalOperationPostResponse> { 113 assertEquals(WithdrawalStatus.selected, it.status) 114 assertEquals("http://localhost:8080/webui/#/operation/$uuid", it.confirm_transfer_url) 115 } 116 // Check idempotence 117 client.post("/taler-integration/withdrawal-operation/$uuid") { 118 json(req) 119 }.assertOkJson<BankWithdrawalOperationPostResponse> { 120 assertEquals(WithdrawalStatus.selected, it.status) 121 assertEquals("http://localhost:8080/webui/#/operation/$uuid", it.confirm_transfer_url) 122 } 123 // Check already selected 124 client.post("/taler-integration/withdrawal-operation/$uuid") { 125 json(req) { 126 "reserve_pub" to EddsaPublicKey.rand() 127 } 128 }.assertConflict(TalerErrorCode.BANK_WITHDRAWAL_OPERATION_RESERVE_SELECTION_CONFLICT) 129 } 130 131 client.postA("/accounts/merchant/withdrawals") { 132 json { "amount" to "KUDOS:1" } 133 }.assertOkJson<BankAccountCreateWithdrawalResponse> { 134 val uuid = it.withdrawal_id 135 136 // Check reserve_pub_reuse 137 client.post("/taler-integration/withdrawal-operation/$uuid") { 138 json(req) 139 }.assertConflict(TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT) 140 141 // Check amount differs 142 client.post("/taler-integration/withdrawal-operation/$uuid") { 143 json(req) { 144 "amount" to "KUDOS:2" 145 } 146 }.assertConflict(TalerErrorCode.BANK_AMOUNT_DIFFERS) 147 148 // Check unknown account 149 client.post("/taler-integration/withdrawal-operation/$uuid") { 150 json { 151 "reserve_pub" to EddsaPublicKey.rand() 152 "selected_exchange" to unknownPayto 153 } 154 }.assertConflict(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) 155 156 // Check account not exchange 157 client.post("/taler-integration/withdrawal-operation/$uuid") { 158 json { 159 "reserve_pub" to EddsaPublicKey.rand() 160 "selected_exchange" to merchantPayto 161 } 162 }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE) 163 164 client.post("/taler-integration/withdrawal-operation/$uuid") { 165 json { 166 "reserve_pub" to EddsaPublicKey.rand() 167 "selected_exchange" to exchangePayto.canonical 168 "amount" to "KUDOS:1" 169 } 170 }.assertOkJson<BankWithdrawalOperationPostResponse>() 171 } 172 173 // Check select aborted 174 client.postA("/accounts/merchant/withdrawals") { 175 json { "amount" to "KUDOS:1" } 176 }.assertOkJson<BankAccountCreateWithdrawalResponse> { 177 val uuid = it.withdrawal_id 178 client.postA("/accounts/merchant/withdrawals/$uuid/abort").assertNoContent() 179 180 // Check error 181 client.postA("/taler-integration/withdrawal-operation/$uuid") { 182 json { 183 "reserve_pub" to EddsaPublicKey.rand() 184 "selected_exchange" to exchangePayto.canonical 185 } 186 }.assertConflict(TalerErrorCode.BANK_UPDATE_ABORT_CONFLICT) 187 } 188 189 client.postA("/accounts/merchant/withdrawals") { 190 json {} 191 }.assertOkJson<BankAccountCreateWithdrawalResponse> { 192 val uuid = it.withdrawal_id 193 194 // Check insufficient fund 195 client.post("/taler-integration/withdrawal-operation/$uuid") { 196 json { 197 "reserve_pub" to EddsaPublicKey.rand() 198 "selected_exchange" to exchangePayto.canonical 199 "amount" to "KUDOS:11" 200 } 201 }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) 202 203 client.post("/taler-integration/withdrawal-operation/$uuid") { 204 json { 205 "reserve_pub" to EddsaPublicKey.rand() 206 "selected_exchange" to exchangePayto.canonical 207 "amount" to "KUDOS:1.1" 208 } 209 }.assertOkJson<BankWithdrawalOperationPostResponse>() 210 client.get("/taler-integration/withdrawal-operation/$uuid") 211 .assertOkJson<BankWithdrawalOperationStatus> { 212 assertEquals(TalerAmount("KUDOS:1.1"), it.amount) 213 assertEquals(TalerAmount("KUDOS:10"), it.max_amount) 214 } 215 } 216 } 217 218 @Test 219 fun selectWithFee() = bankSetup(conf = "test_with_fees.conf") { 220 val uuid = client.postA("/accounts/merchant/withdrawals") { 221 json {} 222 }.assertOkJson<BankAccountCreateWithdrawalResponse>().withdrawal_id 223 // Check insufficient fund 224 for (amount in listOf("KUDOS:11", "KUDOS:10", "KUDOS:0", "KUDOS:150")) { 225 client.post("/taler-integration/withdrawal-operation/$uuid") { 226 json { 227 "reserve_pub" to EddsaPublicKey.rand() 228 "selected_exchange" to exchangePayto.canonical 229 "amount" to amount 230 } 231 }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) 232 } 233 234 // Check OK 235 client.post("/taler-integration/withdrawal-operation/$uuid") { 236 json { 237 "reserve_pub" to EddsaPublicKey.rand() 238 "selected_exchange" to exchangePayto.canonical 239 "amount" to "KUDOS:9" 240 } 241 }.assertOk() 242 } 243 244 // POST /taler-integration/withdrawal-operation/UUID/abort 245 @Test 246 fun abort() = bankSetup { 247 // Check abort created 248 client.postA("/accounts/merchant/withdrawals") { 249 json { "amount" to "KUDOS:1" } 250 }.assertOkJson<BankAccountCreateWithdrawalResponse> { 251 val uuid = it.withdrawal_id 252 253 // Check OK 254 client.postA("/taler-integration/withdrawal-operation/$uuid/abort").assertNoContent() 255 // Check idempotence 256 client.postA("/taler-integration/withdrawal-operation/$uuid/abort").assertNoContent() 257 } 258 259 // Check abort selected 260 client.postA("/accounts/merchant/withdrawals") { 261 json { "amount" to "KUDOS:1" } 262 }.assertOkJson<BankAccountCreateWithdrawalResponse> { 263 val uuid = it.withdrawal_id 264 withdrawalSelect(uuid) 265 266 // Check OK 267 client.postA("/taler-integration/withdrawal-operation/$uuid/abort").assertNoContent() 268 // Check idempotence 269 client.postA("/taler-integration/withdrawal-operation/$uuid/abort").assertNoContent() 270 } 271 272 // Check abort confirmed 273 client.postA("/accounts/merchant/withdrawals") { 274 json { "amount" to "KUDOS:1" } 275 }.assertOkJson<BankAccountCreateWithdrawalResponse> { 276 val uuid = it.withdrawal_id 277 withdrawalSelect(uuid) 278 client.postA("/accounts/merchant/withdrawals/$uuid/confirm").assertNoContent() 279 280 // Check error 281 client.postA("/taler-integration/withdrawal-operation/$uuid/abort") 282 .assertConflict(TalerErrorCode.BANK_ABORT_CONFIRM_CONFLICT) 283 } 284 285 // Check bad UUID 286 client.postA("/taler-integration/withdrawal-operation/chocolate/abort") 287 .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) 288 289 // Check unknown 290 client.postA("/taler-integration/withdrawal-operation/${UUID.randomUUID()}/abort") 291 .assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) 292 } 293 }