libeufin

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

helpers.kt (6917B)


      1 /*
      2  * This file is part of LibEuFin.
      3  * Copyright (C) 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 package tech.libeufin.common.test
     21 
     22 import io.ktor.client.*
     23 import io.ktor.client.request.*
     24 import io.ktor.client.statement.*
     25 import io.ktor.http.*
     26 import io.ktor.server.testing.*
     27 import tech.libeufin.common.*
     28 import kotlin.test.assertEquals
     29 import kotlinx.serialization.json.*
     30 
     31 /* ----- Assert ----- */
     32 
     33 suspend fun assertTime(min: Int, max: Int, lambda: suspend () -> Unit) {
     34     val start = System.currentTimeMillis()
     35     lambda()
     36     val end = System.currentTimeMillis()
     37     val time = end - start
     38     assert(time >= min) { "Expected to last at least $min ms, lasted $time" }
     39     assert(time <= max) { "Expected to last at most $max ms, lasted $time" }
     40 }
     41 
     42 suspend inline fun <reified B> HttpResponse.assertHistoryIds(size: Int, ids: (B) -> List<Long>): B {
     43     assertOk()
     44     val body = json<B>()
     45     val history = ids(body)
     46     val params = PageParams.extract(call.request.url.parameters)
     47 
     48     // testing the size is like expected.
     49     assertEquals(size, history.size, "bad history length: $history")
     50     if (params.limit < 0) {
     51         // testing that the first id is at most the 'offset' query param.
     52         assert(history[0] <= params.offset) { "bad history offset: $params $history" }
     53         // testing that the id decreases.
     54         if (history.size > 1)
     55             assert(history.windowed(2).all { (a, b) -> a > b }) { "bad history order: $history" }
     56     } else {
     57         // testing that the first id is at least the 'offset' query param.
     58         assert(history[0] >= params.offset) { "bad history offset: $params $history" }
     59         // testing that the id increases.
     60         if (history.size > 1)
     61             assert(history.windowed(2).all { (a, b) -> a < b }) { "bad history order: $history" }
     62     }
     63 
     64     return body
     65 }
     66 
     67 /* ----- Auth ----- */
     68 
     69 typealias RequestLambda = suspend HttpRequestBuilder.() -> Unit;
     70 
     71 /** Auto token auth GET request */
     72 suspend fun HttpClient.getA(url: String, builder: RequestLambda = {}): HttpResponse
     73     = tokenAuthRequest(url, HttpMethod.Get, null, builder)
     74 /** Auto token auth POST request */
     75 suspend fun HttpClient.postA(url: String, builder: RequestLambda = {}): HttpResponse
     76     = tokenAuthRequest(url, HttpMethod.Post, null, builder)
     77 /** Auto token auth PATCH request */
     78 suspend fun HttpClient.patchA(url: String, builder: RequestLambda = {}): HttpResponse
     79     = tokenAuthRequest(url, HttpMethod.Patch, null, builder)
     80 /** Auto token auth DELETE request */
     81 suspend fun HttpClient.deleteA(url: String, builder: RequestLambda = {}): HttpResponse
     82     = tokenAuthRequest(url, HttpMethod.Delete, null, builder)
     83 
     84 /** Admin token auth GET request */
     85 suspend fun HttpClient.getAdmin(url: String, builder: RequestLambda = {}): HttpResponse
     86     = tokenAuthRequest(url, HttpMethod.Get, "admin", builder)
     87 /** Admin token auth PATCH request */
     88 suspend fun HttpClient.patchAdmin(url: String, builder: RequestLambda = {}): HttpResponse
     89     = tokenAuthRequest(url, HttpMethod.Patch, "admin", builder)
     90 /** Admin token auth POST request */
     91 suspend fun HttpClient.postAdmin(url: String, builder: RequestLambda = {}): HttpResponse
     92     = tokenAuthRequest(url, HttpMethod.Post, "admin", builder)
     93 /** Admin token auth DELETE request */
     94 suspend fun HttpClient.deleteAdmin(url: String, builder: RequestLambda = {}): HttpResponse
     95     = tokenAuthRequest(url, HttpMethod.Delete, "admin", builder)
     96 
     97 /** Auto pw auth GET request */
     98 suspend fun HttpClient.getPw(url: String, builder: RequestLambda = {}): HttpResponse
     99     = pwAuthRequest(url, HttpMethod.Get, null, builder)
    100 /** Auto pw auth POST request */
    101 suspend fun HttpClient.postPw(url: String, builder: RequestLambda = {}): HttpResponse
    102     = pwAuthRequest(url, HttpMethod.Post, null, builder)
    103 /** Auto pw auth PATCH request */
    104 suspend fun HttpClient.patchPw(url: String, builder: RequestLambda = {}): HttpResponse
    105     = pwAuthRequest(url, HttpMethod.Patch, null, builder)
    106 /** Auto pw auth DELETE request */
    107 suspend fun HttpClient.deletePw(url: String, builder: RequestLambda = {}): HttpResponse
    108     = pwAuthRequest(url, HttpMethod.Delete, null, builder)
    109 
    110 private suspend fun HttpClient.tokenAuthRequest(
    111     url: String,
    112     method: HttpMethod,
    113     username: String?,
    114     builder: RequestLambda = {}
    115 ): HttpResponse = request(url) {
    116     this.method = method
    117     tokenAuth(this@tokenAuthRequest, username)
    118     builder(this)
    119 }
    120 
    121 private suspend fun HttpClient.pwAuthRequest(
    122     url: String,
    123     method: HttpMethod,
    124     username: String?,
    125     builder: RequestLambda = {}
    126 ): HttpResponse = request(url) {
    127     this.method = method
    128     pwAuth(username)
    129     builder(this)
    130 }
    131 
    132 private fun HttpRequestBuilder.extractUsername(username: String? = null): String?
    133     = when {
    134         username != null -> username
    135         url.pathSegments.contains("admin") -> "admin"
    136         url.pathSegments[1] == "accounts" -> url.pathSegments[2]
    137         else -> null
    138     }
    139 
    140 /** Authenticate a request for [username] with basic auth */
    141 fun HttpRequestBuilder.pwAuth(username: String? = null) {
    142     val username = extractUsername(username) ?: return
    143     basicAuth("$username", "$username-password")
    144 }
    145 
    146 val globalTestTokens = mutableMapOf<String, String>()
    147 
    148 /** Get cached token or create it */
    149 suspend fun HttpClient.cachedToken(username: String): String {
    150     // Get cached token or create it
    151     var token = globalTestTokens.get(username)
    152     if (token == null) {
    153         val response = this.post("/accounts/$username/token") {
    154             pwAuth()
    155             json {
    156                 "scope" to "readwrite"
    157                 "duration" to obj {
    158                     "d_us" to "forever"
    159                 }
    160             }
    161         }.assertOkJson<JsonObject>()
    162         token = Json.decodeFromJsonElement<String>(response.get("access_token")!!)
    163         globalTestTokens.set(username, token)
    164     }
    165     return token
    166 }
    167 
    168 /** Authenticate a request for [username] with a bearer token */
    169 suspend fun HttpRequestBuilder.tokenAuth(client: HttpClient, username: String? = null) {
    170     // Get username from arg or path
    171     val username = extractUsername(username) ?: return
    172     // Get cached token or create it
    173     var token = client.cachedToken(username)
    174     // Set authorization header
    175     headers[HttpHeaders.Authorization] = "Bearer $token"
    176 }