/*
* This file is part of LibEuFin.
* Copyright (C) 2023 Taler Systems S.A.
* LibEuFin is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation; either version 3, or
* (at your option) any later version.
* LibEuFin is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
* Public License for more details.
* You should have received a copy of the GNU Affero General Public
* License along with LibEuFin; see the file COPYING. If not, see
*
*/
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.testing.*
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.serialization.json.JsonObject
import tech.libeufin.bank.BankAccountCreateWithdrawalResponse
import tech.libeufin.bank.WithdrawalStatus
import tech.libeufin.common.*
import kotlin.test.assertEquals
// Test endpoint is correctly authenticated
suspend fun ApplicationTestBuilder.authRoutine(
method: HttpMethod,
path: String,
body: JsonObject? = null,
requireExchange: Boolean = false,
requireAdmin: Boolean = false,
allowAdmin: Boolean = false
) {
// No body when authentication must happen before parsing the body
// Unknown account
client.request(path) {
this.method = method
basicAuth("unknown", "password")
}.assertUnauthorized()
// Wrong password
client.request(path) {
this.method = method
basicAuth("merchant", "wrong-password")
}.assertUnauthorized()
// Wrong account
client.request(path) {
this.method = method
basicAuth("exchange", "merchant-password")
}.assertUnauthorized()
if (requireAdmin) {
// Not exchange account
client.request(path) {
this.method = method
pwAuth("merchant")
}.assertUnauthorized()
} else if (!allowAdmin) {
// Check no admin
client.request(path) {
this.method = method
pwAuth("admin")
}.assertUnauthorized()
}
if (requireExchange) {
// Not exchange account
client.request(path) {
this.method = method
if (body != null) json(body)
pwAuth(if (requireAdmin) "admin" else "merchant")
}.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE)
}
}
suspend inline fun ApplicationTestBuilder.historyRoutine(
url: String,
crossinline ids: (B) -> List,
registered: List Unit>,
ignored: List Unit> = listOf(),
polling: Boolean = true,
auth: String? = null
) {
// Get history
val history: suspend (String) -> HttpResponse = { params: String ->
client.get("$url?$params") {
pwAuth(auth)
}
}
// Check history is following specs
val assertHistory: suspend HttpResponse.(Int) -> Unit = { size: Int ->
assertHistoryIds(size, ids)
}
// Get latest registered id
val latestId: suspend () -> Long = {
history("delta=-1").assertOkJson().run { ids(this)[0] }
}
// Check error when no transactions
history("delta=7").assertNoContent()
// Run interleaved registered and ignore transactions
val registered_iter = registered.iterator()
val ignored_iter = ignored.iterator()
while (registered_iter.hasNext() || ignored_iter.hasNext()) {
if (registered_iter.hasNext()) registered_iter.next()()
if (ignored_iter.hasNext()) ignored_iter.next()()
}
val nbRegistered = registered.size
val nbIgnored = ignored.size
val nbTotal = nbRegistered + nbIgnored
// Check ignored
history("delta=$nbTotal").assertHistory(nbRegistered)
// Check skip ignored
history("delta=$nbRegistered").assertHistory(nbRegistered)
if (polling) {
// Check no polling when we cannot have more transactions
assertTime(0, 100) {
history("delta=-${nbRegistered+1}&long_poll_ms=1000")
.assertHistory(nbRegistered)
}
// Check no polling when already find transactions even if less than delta
assertTime(0, 100) {
history("delta=${nbRegistered+1}&long_poll_ms=1000")
.assertHistory(nbRegistered)
}
// Check polling
coroutineScope {
val id = latestId()
launch { // Check polling succeed
assertTime(100, 200) {
history("delta=2&start=$id&long_poll_ms=1000")
.assertHistory(1)
}
}
launch { // Check polling timeout
assertTime(200, 300) {
history("delta=1&start=${id+nbTotal*3}&long_poll_ms=200")
.assertNoContent()
}
}
delay(100)
registered[0]()
}
// Test triggers
for (register in registered) {
coroutineScope {
val id = latestId()
launch {
assertTime(100, 200) {
history("delta=7&start=$id&long_poll_ms=1000")
.assertHistory(1)
}
}
delay(100)
register()
}
}
// Test doesn't trigger
coroutineScope {
val id = latestId()
launch {
assertTime(200, 300) {
history("delta=7&start=$id&long_poll_ms=200")
.assertNoContent()
}
}
delay(100)
for (ignore in ignored) {
ignore()
}
}
}
// Testing ranges.
repeat(20) {
registered[0]()
}
val id = latestId()
// Default
history("").assertHistory(20)
// forward range:
history("delta=10").assertHistory(10)
history("delta=10&start=4").assertHistory(10)
// backward range:
history("delta=-10").assertHistory(10)
history("delta=-10&start=${id-4}").assertHistory(10)
}
suspend inline fun ApplicationTestBuilder.statusRoutine(
url: String,
crossinline status: (B) -> WithdrawalStatus
) {
val amount = TalerAmount("KUDOS:9.0")
client.postA("/accounts/customer/withdrawals") {
json { "amount" to amount }
}.assertOkJson { resp ->
val aborted_uuid = resp.taler_withdraw_uri.split("/").last()
val confirmed_uuid = client.postA("/accounts/customer/withdrawals") {
json { "amount" to amount }
}.assertOkJson()
.taler_withdraw_uri.split("/").last()
// Check no useless polling
assertTime(0, 100) {
client.get("$url/$confirmed_uuid?long_poll_ms=1000&old_state=selected")
.assertOkJson { assertEquals(WithdrawalStatus.pending, status(it)) }
}
// Polling selected
coroutineScope {
launch { // Check polling succeed
assertTime(100, 200) {
client.get("$url/$confirmed_uuid?long_poll_ms=1000")
.assertOkJson { assertEquals(WithdrawalStatus.selected, status(it)) }
}
}
launch { // Check polling succeed
assertTime(100, 200) {
client.get("$url/$aborted_uuid?long_poll_ms=1000")
.assertOkJson { assertEquals(WithdrawalStatus.selected, status(it)) }
}
}
delay(100)
withdrawalSelect(confirmed_uuid)
withdrawalSelect(aborted_uuid)
}
// Polling confirmed
coroutineScope {
launch { // Check polling succeed
assertTime(100, 200) {
client.get("$url/$confirmed_uuid?long_poll_ms=1000&old_state=selected")
.assertOkJson { assertEquals(WithdrawalStatus.confirmed, status(it))}
}
}
launch { // Check polling timeout
assertTime(200, 300) {
client.get("$url/$aborted_uuid?long_poll_ms=200&old_state=selected")
.assertOkJson { assertEquals(WithdrawalStatus.selected, status(it)) }
}
}
delay(100)
client.postA("/accounts/customer/withdrawals/$confirmed_uuid/confirm").assertNoContent()
}
// Polling abort
coroutineScope {
launch {
assertTime(200, 300) {
client.get("$url/$confirmed_uuid?long_poll_ms=200&old_state=confirmed")
.assertOkJson { assertEquals(WithdrawalStatus.confirmed, status(it))}
}
}
launch {
assertTime(100, 200) {
client.get("$url/$aborted_uuid?long_poll_ms=1000&old_state=selected")
.assertOkJson { assertEquals(WithdrawalStatus.aborted, status(it)) }
}
}
delay(100)
client.post("/taler-integration/withdrawal-operation/$aborted_uuid/abort").assertNoContent()
}
}
}