libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

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 }