routines.kt (6472B)
1 /* 2 * This file is part of LibEuFin. 3 * Copyright (C) 2023-2024 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 kotlinx.coroutines.coroutineScope 24 import kotlinx.coroutines.delay 25 import kotlinx.coroutines.launch 26 import kotlinx.serialization.json.JsonObject 27 import tech.libeufin.bank.BankAccountCreateWithdrawalResponse 28 import tech.libeufin.bank.WithdrawalStatus 29 import tech.libeufin.common.* 30 import tech.libeufin.common.test.* 31 import kotlin.test.assertEquals 32 33 // Test endpoint is correctly authenticated 34 suspend fun ApplicationTestBuilder.authRoutine( 35 method: HttpMethod, 36 path: String, 37 body: JsonObject? = null, 38 requireExchange: Boolean = false, 39 requireAdmin: Boolean = false, 40 allowAdmin: Boolean = false, 41 optional: Boolean = false 42 ) { 43 // No body when authentication must happen before parsing the body 44 45 if (!optional) { 46 // No header 47 client.request(path) { 48 this.method = method 49 if (body != null) json(body) 50 }.assertUnauthorized(TalerErrorCode.GENERIC_PARAMETER_MISSING) 51 } 52 53 // Bad header 54 client.request(path) { 55 this.method = method 56 if (body != null) json(body) 57 headers[HttpHeaders.Authorization] = "WTF" 58 }.assertBadRequest(TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED) 59 60 if (requireAdmin) { 61 // Not an admin account 62 client.request(path) { 63 this.method = method 64 if (body != null) json(body) 65 tokenAuth(client, "merchant") 66 }.assertForbidden() 67 } else if (!allowAdmin) { 68 // Check no admin 69 client.request(path) { 70 this.method = method 71 if (body != null) json(body) 72 tokenAuth(client, "admin") 73 }.assertStatus(HttpStatusCode.Forbidden, null) 74 } 75 76 if (requireExchange) { 77 // Not exchange account 78 client.request(path) { 79 this.method = method 80 if (body != null) json(body) 81 tokenAuth(client, if (requireAdmin) "admin" else "merchant") 82 }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE) 83 } 84 } 85 86 suspend inline fun <reified B> ApplicationTestBuilder.historyRoutine( 87 url: String, 88 crossinline ids: (B) -> List<Long>, 89 registered: List<suspend () -> Unit>, 90 ignored: List<suspend () -> Unit> = listOf(), 91 polling: Boolean = true, 92 auth: String? = null 93 ) { 94 abstractHistoryRoutine(ids, registered, ignored, polling) { params: String -> 95 client.getA("$url?$params") { 96 tokenAuth(client, auth) 97 } 98 } 99 } 100 101 suspend inline fun <reified B> ApplicationTestBuilder.statusRoutine( 102 url: String, 103 crossinline status: (B) -> WithdrawalStatus 104 ) { 105 val amount = TalerAmount("KUDOS:9.0") 106 client.postA("/accounts/customer/withdrawals") { 107 json { "amount" to amount } 108 }.assertOkJson<BankAccountCreateWithdrawalResponse> { resp -> 109 val aborted_uuid = resp.taler_withdraw_uri.split("/").last() 110 val confirmed_uuid = client.postA("/accounts/customer/withdrawals") { 111 json { "amount" to amount } 112 }.assertOkJson<BankAccountCreateWithdrawalResponse>() 113 .taler_withdraw_uri.split("/").last() 114 115 // Check no useless polling 116 assertTime(0, 100) { 117 client.get("$url/$confirmed_uuid?timeout_ms=1000&old_state=selected") 118 .assertOkJson<B> { assertEquals(WithdrawalStatus.pending, status(it)) } 119 } 120 121 // Polling selected 122 coroutineScope { 123 launch { // Check polling succeed 124 assertTime(100, 200) { 125 client.get("$url/$confirmed_uuid?timeout_ms=1000") 126 .assertOkJson<B> { assertEquals(WithdrawalStatus.selected, status(it)) } 127 } 128 } 129 launch { // Check polling succeed 130 assertTime(100, 200) { 131 client.get("$url/$aborted_uuid?timeout_ms=1000") 132 .assertOkJson<B> { assertEquals(WithdrawalStatus.selected, status(it)) } 133 } 134 } 135 delay(100) 136 withdrawalSelect(confirmed_uuid) 137 withdrawalSelect(aborted_uuid) 138 } 139 140 // Polling confirmed 141 coroutineScope { 142 launch { // Check polling succeed 143 assertTime(100, 200) { 144 client.get("$url/$confirmed_uuid?timeout_ms=1000&old_state=selected") 145 .assertOkJson<B> { assertEquals(WithdrawalStatus.confirmed, status(it))} 146 } 147 } 148 launch { // Check polling timeout 149 assertTime(200, 300) { 150 client.get("$url/$aborted_uuid?timeout_ms=200&old_state=selected") 151 .assertOkJson<B> { assertEquals(WithdrawalStatus.selected, status(it)) } 152 } 153 } 154 delay(100) 155 client.postA("/accounts/customer/withdrawals/$confirmed_uuid/confirm").assertNoContent() 156 } 157 158 // Polling abort 159 coroutineScope { 160 launch { 161 assertTime(200, 300) { 162 client.get("$url/$confirmed_uuid?timeout_ms=200&old_state=confirmed") 163 .assertOkJson<B> { assertEquals(WithdrawalStatus.confirmed, status(it))} 164 } 165 } 166 launch { 167 assertTime(100, 200) { 168 client.get("$url/$aborted_uuid?timeout_ms=1000&old_state=selected") 169 .assertOkJson<B> { assertEquals(WithdrawalStatus.aborted, status(it)) } 170 } 171 } 172 delay(100) 173 client.post("/taler-integration/withdrawal-operation/$aborted_uuid/abort").assertNoContent() 174 } 175 } 176 }