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 }