helpers.kt (10698B)
1 /* 2 * This file is part of LibEuFin. 3 * Copyright (C) 2024, 2025, 2026 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.* 21 import io.ktor.client.request.* 22 import io.ktor.client.statement.* 23 import io.ktor.http.* 24 import io.ktor.server.testing.* 25 import kotlinx.coroutines.runBlocking 26 import tech.libeufin.common.* 27 import tech.libeufin.common.db.dbInit 28 import tech.libeufin.common.db.pgDataSource 29 import tech.libeufin.ebics.* 30 import tech.libeufin.nexus.* 31 import tech.libeufin.nexus.cli.registerIncomingPayment 32 import tech.libeufin.nexus.cli.registerOutgoingPayment 33 import tech.libeufin.nexus.db.Database 34 import tech.libeufin.nexus.db.InitiatedPayment 35 import tech.libeufin.nexus.db.TransferDAO.RegistrationResult 36 import tech.libeufin.nexus.iso20022.* 37 import java.time.Instant 38 import kotlin.io.path.Path 39 import kotlin.test.assertEquals 40 41 fun conf( 42 conf: String = "test.conf", 43 lambda: suspend (NexusConfig) -> Unit 44 ) = runBlocking { 45 val cfg = nexusConfig(Path("conf/$conf")) 46 lambda(cfg) 47 } 48 49 fun setup( 50 conf: String = "test.conf", 51 lambda: suspend (Database, NexusConfig) -> Unit 52 ) = conf(conf) { cfg -> 53 pgDataSource(cfg.dbCfg.dbConnStr).dbInit(cfg.dbCfg, "libeufin-nexus", true) 54 cfg.withDb(lambda) 55 } 56 57 fun serverSetup( 58 conf: String = "test.conf", 59 lambda: suspend ApplicationTestBuilder.(Database) -> Unit 60 ) = setup(conf) { db, cfg -> 61 testApplication { 62 application { 63 nexusApi(db, cfg) 64 } 65 lambda(db) 66 } 67 } 68 69 const val grothoffPayto = "payto://iban/CH4189144589712575493?receiver-name=Grothoff%20Hans" 70 71 val clientKeys = generateNewKeys() 72 73 /** Generates a payment initiation, given its subject */ 74 fun genInitPay( 75 endToEndId: String, 76 subject: String = "init payment", 77 amount: String = "KUDOS:44", 78 creditor: IbanPayto = ibanPayto("CH4189144589712575493", "Test") 79 ) = InitiatedPayment( 80 id = -1, 81 amount = TalerAmount(amount), 82 creditor = creditor, 83 subject = subject, 84 initiationTime = Instant.now(), 85 endToEndId = endToEndId 86 ) 87 88 /** Generates an incoming payment, given its subject */ 89 fun genInPay( 90 subject: String, 91 amount: String = "KUDOS:44", 92 executionTime: Instant = Instant.now() 93 ) = IncomingPayment( 94 amount = TalerAmount(amount), 95 debtor = ibanPayto("DE84500105177118117964", "John Smith"), 96 subject = subject, 97 executionTime = executionTime, 98 id = IncomingId(null, randEbicsId(), null) 99 ) 100 101 /** Generates an outgoing payment, given its subject and end-to-end ID */ 102 fun genOutPay( 103 subject: String, 104 endToEndId: String? = null, 105 msgId: String? = null, 106 executionTime: Instant = Instant.now() 107 ) = OutgoingPayment( 108 id = OutgoingId(msgId, endToEndId ?: randEbicsId(), null), 109 amount = TalerAmount(44, 0, "KUDOS"), 110 creditor = ibanPayto("CH4189144589712575493", "Test"), 111 subject = subject, 112 executionTime = executionTime, 113 ) 114 115 /** Perform a taler outgoing transaction */ 116 suspend fun ApplicationTestBuilder.transfer() { 117 client.postA("/taler-wire-gateway/transfer") { 118 json { 119 "request_uid" to HashCode.rand() 120 "amount" to "CHF:55" 121 "exchange_base_url" to "http://exchange.example.com/" 122 "wtid" to ShortHashCode.rand() 123 "credit_account" to grothoffPayto 124 } 125 }.assertOk() 126 } 127 128 /** Perform a taler incoming transaction of [amount] from merchant to exchange */ 129 suspend fun ApplicationTestBuilder.addIncoming(amount: String) { 130 client.postA("/taler-wire-gateway/admin/add-incoming") { 131 json { 132 "amount" to TalerAmount(amount) 133 "reserve_pub" to EddsaPublicKey.randEdsaKey() 134 "debit_account" to grothoffPayto 135 } 136 }.assertOk() 137 } 138 139 /** Perform a taler kyc transaction of [amount] from merchant to exchange */ 140 suspend fun ApplicationTestBuilder.addKyc(amount: String) { 141 client.postA("/taler-wire-gateway/admin/add-kycauth") { 142 json { 143 "amount" to TalerAmount(amount) 144 "account_pub" to EddsaPublicKey.randEdsaKey() 145 "debit_account" to grothoffPayto 146 } 147 }.assertOk() 148 } 149 150 /** Register a talerable outgoing transaction */ 151 suspend fun talerableOut(db: Database, metadata: String? = null) { 152 val wtid = EddsaPublicKey.randEdsaKey() 153 registerOutgoingPayment(db, genOutPay(fmtOutgoingSubject(wtid, BaseURL.parse("http://exchange.example.com/"), metadata))) 154 } 155 156 /** Register a talerable reserve incoming transaction */ 157 suspend fun talerableIn( 158 db: Database, 159 amount: String = "CHF:44", 160 reserve_pub: EddsaPublicKey = EddsaPublicKey.randEdsaKey() 161 ) { 162 registerIncomingPayment( 163 db, NexusIngestConfig.default(AccountType.exchange), 164 genInPay("test with $reserve_pub reserve pub", amount) 165 ) 166 } 167 168 private suspend fun prepare(db: Database): String { 169 val pub = EddsaPublicKey.randEdsaKey() 170 val sig = EddsaSignature.rand() 171 val referenceNumber = subjectFmtQrBill(pub) 172 assertEquals( 173 RegistrationResult.Success, 174 db.transfer.register( 175 type = TransferType.reserve, 176 accountPub = pub, 177 authPub = pub, 178 authSig = sig, 179 referenceNumber = referenceNumber, 180 timestamp = Instant.now(), 181 recurrent = false 182 ) 183 ) 184 return referenceNumber 185 } 186 187 /** Register a talerable reserve prepared incoming transaction */ 188 suspend fun talerablePreparedIn(db: Database, amount: String = "CHF:44") { 189 val referenceNumber = prepare(db) 190 registerIncomingPayment( 191 db, NexusIngestConfig.default(AccountType.exchange), 192 genInPay(referenceNumber, amount) 193 ) 194 } 195 196 /** Register an incomplete talerable reserve prepared incoming transaction */ 197 suspend fun talerablePreparedIncompleteIn(db: Database, amount: String = "CHF:44") { 198 val referenceNumber = prepare(db) 199 val incomplete = genInPay(referenceNumber).copy(subject = null, debtor = null) 200 registerIncomingPayment( 201 db, NexusIngestConfig.default(AccountType.exchange), incomplete 202 ) 203 } 204 205 /** Register a completed talerable reserve prepared incoming transaction */ 206 suspend fun talerablePreparedCompletedIn(db: Database, amount: String = "CHF:44") { 207 val referenceNumber = prepare(db) 208 val original = genInPay(referenceNumber, amount) 209 val incomplete = original.copy(subject = null, debtor = null) 210 registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), incomplete) 211 registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), original) 212 } 213 214 /** Register an incomplete talerable reserve incoming transaction */ 215 suspend fun talerableIncompleteIn(db: Database) { 216 val reserve_pub = EddsaPublicKey.randEdsaKey() 217 val incomplete = genInPay("test with $reserve_pub reserve pub").copy(subject = null, debtor = null) 218 registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), incomplete) 219 } 220 221 /** Register a completed talerable reserve incoming transaction */ 222 suspend fun talerableCompletedIn(db: Database) { 223 val reserve_pub = EddsaPublicKey.randEdsaKey() 224 val original = genInPay("test with $reserve_pub reserve pub") 225 val incomplete = original.copy(subject = null, debtor = null) 226 registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), incomplete) 227 registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), original) 228 } 229 230 /** Register a talerable KYC incoming transaction */ 231 suspend fun talerableKycIn( 232 db: Database, 233 amount: String = "CHF:44", 234 account_pub: EddsaPublicKey = EddsaPublicKey.randEdsaKey() 235 ) { 236 registerIncomingPayment( 237 db, NexusIngestConfig.default(AccountType.exchange), 238 genInPay("test with KYC:$account_pub account pub", amount) 239 ) 240 } 241 242 /** Register an incoming transaction */ 243 suspend fun registerIn(db: Database) { 244 registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), genInPay("ignored")) 245 } 246 247 /** Register an incomplete incoming transaction */ 248 suspend fun registerIncompleteIn(db: Database) { 249 val incomplete = genInPay("ignored").copy(subject = null, debtor = null) 250 registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), incomplete) 251 } 252 253 /** Register a completed incoming transaction */ 254 suspend fun registerCompletedIn(db: Database) { 255 val original = genInPay("ignored") 256 val incomplete = original.copy(subject = null, debtor = null) 257 registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), incomplete) 258 registerIncomingPayment(db, NexusIngestConfig.default(AccountType.exchange), original) 259 } 260 261 /** Register an outgoing transaction */ 262 suspend fun registerOut(db: Database) { 263 registerOutgoingPayment(db, genOutPay("ignored")) 264 } 265 266 /** Register an incomplete outgoing transaction */ 267 suspend fun registerIncompleteOut(db: Database) { 268 val original = genOutPay("ignored") 269 val incomplete = original.copy(id = OutgoingId(null, null, original.id.endToEndId), creditor = null) 270 registerOutgoingPayment(db, incomplete) 271 } 272 273 /* ----- Auth ----- */ 274 275 /** Auto auth get request */ 276 suspend inline fun HttpClient.getA(url: String, builder: HttpRequestBuilder.() -> Unit = {}): HttpResponse { 277 return get(url) { 278 auth() 279 builder(this) 280 } 281 } 282 283 /** Auto auth post request */ 284 suspend inline fun HttpClient.postA(url: String, builder: HttpRequestBuilder.() -> Unit = {}): HttpResponse { 285 return post(url) { 286 auth() 287 builder(this) 288 } 289 } 290 291 /** Auto auth patch request */ 292 suspend inline fun HttpClient.patchA(url: String, builder: HttpRequestBuilder.() -> Unit = {}): HttpResponse { 293 return patch(url) { 294 auth() 295 builder(this) 296 } 297 } 298 299 /** Auto auth delete request */ 300 suspend inline fun HttpClient.deleteA(url: String, builder: HttpRequestBuilder.() -> Unit = {}): HttpResponse { 301 return delete(url) { 302 auth() 303 builder(this) 304 } 305 } 306 307 fun HttpRequestBuilder.auth() { 308 headers[HttpHeaders.Authorization] = "Bearer secret-token" 309 }