summaryrefslogtreecommitdiff
path: root/bank/src
diff options
context:
space:
mode:
Diffstat (limited to 'bank/src')
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/Config.kt13
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/Constants.kt5
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/Main.kt49
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt59
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt27
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt83
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt5
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt39
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt5
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt3
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt10
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/helpers.kt16
-rw-r--r--bank/src/test/kotlin/AmountTest.kt1
-rw-r--r--bank/src/test/kotlin/ConversionApiTest.kt2
-rw-r--r--bank/src/test/kotlin/CoreBankApiTest.kt69
-rw-r--r--bank/src/test/kotlin/RevenueApiTest.kt1
-rw-r--r--bank/src/test/kotlin/StatsTest.kt4
-rw-r--r--bank/src/test/kotlin/WireGatewayApiTest.kt13
-rw-r--r--bank/src/test/kotlin/helpers.kt54
-rw-r--r--bank/src/test/kotlin/routines.kt122
20 files changed, 204 insertions, 376 deletions
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Config.kt b/bank/src/main/kotlin/tech/libeufin/bank/Config.kt
index 9cdfcf0f..17fef641 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Config.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Config.kt
@@ -68,11 +68,6 @@ data class ConversionRate (
val cashout_min_amount: TalerAmount,
)
-sealed interface ServerConfig {
- data class Unix(val path: String, val mode: Int): ServerConfig
- data class Tcp(val addr: String, val port: Int): ServerConfig
-}
-
fun talerConfig(configPath: Path?): TalerConfig = BANK_CONFIG_SOURCE.fromFile(configPath)
fun TalerConfig.loadDbConfig(): DatabaseConfig {
@@ -82,14 +77,6 @@ fun TalerConfig.loadDbConfig(): DatabaseConfig {
)
}
-fun TalerConfig.loadServerConfig(): ServerConfig {
- return when (val method = requireString("libeufin-bank", "serve")) {
- "tcp" -> ServerConfig.Tcp(lookupString("libeufin-bank", "address") ?: requireString("libeufin-bank", "bind_to"), requireNumber("libeufin-bank", "port"))
- "unix" -> ServerConfig.Unix(requireString("libeufin-bank", "unixpath"), requireNumber("libeufin-bank", "unixpath_mode"))
- else -> throw TalerConfigError.invalid("server method", "libeufin-bank", "serve", "expected 'tcp' or 'unix' got '$method'")
- }
-}
-
fun TalerConfig.loadBankConfig(): BankConfig {
val regionalCurrency = requireString("libeufin-bank", "currency")
var fiatCurrency: String? = null
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt b/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt
index d91d779d..209ea700 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt
@@ -37,7 +37,6 @@ val RESERVED_ACCOUNTS = setOf("admin", "bank")
const val IBAN_ALLOCATION_RETRY_COUNTER: Int = 5
// API version
-const val COREBANK_API_VERSION: String = "4:7:0"
-const val CONVERSION_API_VERSION: String = "0:0:0"
+const val COREBANK_API_VERSION: String = "4:8:0"
+const val CONVERSION_API_VERSION: String = "0:1:0"
const val INTEGRATION_API_VERSION: String = "2:0:2"
-const val REVENUE_API_VERSION: String = "0:0:0" \ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
index 8733b907..1f3ebbc8 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -62,8 +62,7 @@ import kotlin.io.path.exists
import kotlin.io.path.readText
private val logger: Logger = LoggerFactory.getLogger("libeufin-bank")
-// Dirty local variable to stop the server in test TODO remove this ugly hack
-var engine: ApplicationEngine? = null
+
/**
@@ -117,7 +116,7 @@ class ServeBank : CliktCommand("Run libeufin-bank HTTP server", name = "serve")
val cfg = talerConfig(common.config)
val ctx = cfg.loadBankConfig()
val dbCfg = cfg.loadDbConfig()
- val serverCfg = cfg.loadServerConfig()
+ val serverCfg = cfg.loadServerConfig("libeufin-bank")
Database(dbCfg, ctx.regionalCurrency, ctx.fiatCurrency).use { db ->
if (ctx.allowConversion) {
logger.info("Ensure exchange account exists")
@@ -142,25 +141,9 @@ class ServeBank : CliktCommand("Run libeufin-bank HTTP server", name = "serve")
db.conn { it.execSQLUpdate(sqlProcedures.readText()) }
// Remove conversion info from the database ?
}
-
- val env = applicationEngineEnvironment {
- when (serverCfg) {
- is ServerConfig.Tcp -> {
- for (addr in InetAddress.getAllByName(serverCfg.addr)) {
- connector {
- port = serverCfg.port
- host = addr.hostAddress
- }
- }
- }
- is ServerConfig.Unix ->
- throw Exception("Can only serve libeufin-bank via TCP")
- }
- module { corebankWebApp(db, ctx) }
+ serve(serverCfg) {
+ corebankWebApp(db, ctx)
}
- val local = embeddedServer(Netty, env)
- engine = local
- local.start(wait = true)
}
}
}
@@ -216,7 +199,14 @@ class EditAccount : CliktCommand(
private val tan_channel: String? by option(help = "which channel TAN challenges should be sent to")
private val cashout_payto_uri: IbanPayto? by option(help = "Payto URI of a fiant account who receive cashout amount").convert { Payto.parse(it).expectIban() }
private val debit_threshold: TalerAmount? by option(help = "Max debit allowed for this account").convert { TalerAmount(it) }
-
+ private val min_cashout: Option<TalerAmount>? by option(help = "Custom minimum cashout amount for this account").convert {
+ if (it == "") {
+ Option.None
+ } else {
+ Option.Some(TalerAmount(it))
+ }
+ }
+
override fun run() = cliCmd(logger, common.log) {
val cfg = talerConfig(common.config)
val ctx = cfg.loadBankConfig()
@@ -232,7 +222,12 @@ class EditAccount : CliktCommand(
phone = if (phone == null) Option.None else Option.Some(if (phone != "") phone else null),
),
cashout_payto_uri = Option.Some(cashout_payto_uri),
- debit_threshold = debit_threshold
+ debit_threshold = debit_threshold,
+ min_cashout = when (val tmp = min_cashout) {
+ null -> Option.None
+ is Option.None -> Option.Some(null)
+ is Option.Some -> Option.Some(tmp.value)
+ }
)
when (patchAccount(db, ctx, req, username, true, true)) {
AccountPatchResult.Success ->
@@ -244,6 +239,7 @@ class EditAccount : CliktCommand(
AccountPatchResult.NonAdminName,
AccountPatchResult.NonAdminCashout,
AccountPatchResult.NonAdminDebtLimit,
+ AccountPatchResult.NonAdminMinCashout,
is AccountPatchResult.TanRequired -> {
// Unreachable as we edit account as admin
}
@@ -282,6 +278,10 @@ class CreateAccountOption: OptionGroup() {
val debit_threshold: TalerAmount? by option(
help = "Max debit allowed for this account"
).convert { TalerAmount(it) }
+ val min_cashout: TalerAmount? by option(
+ help = "Custom minimum cashout amount for this account"
+ ).convert { TalerAmount(it) }
+
}
class CreateAccount : CliktCommand(
@@ -312,7 +312,8 @@ class CreateAccount : CliktCommand(
),
cashout_payto_uri = cashout_payto_uri,
payto_uri = payto_uri,
- debit_threshold = debit_threshold
+ debit_threshold = debit_threshold,
+ min_cashout = min_cashout
)
}
req?.let {
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt
index 5966cba9..57c3fda8 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt
@@ -37,65 +37,6 @@ import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.concurrent.TimeUnit
-@Serializable(with = DecimalNumber.Serializer::class)
-class DecimalNumber {
- val value: Long
- val frac: Int
-
- constructor(value: Long, frac: Int) {
- this.value = value
- this.frac = frac
- }
- constructor(encoded: String) {
- val match = PATTERN.matchEntire(encoded) ?: throw badRequest("Invalid decimal number format")
- val (value, frac) = match.destructured
- this.value = value.toLongOrNull() ?: throw badRequest("Invalid value")
- if (this.value > TalerAmount.MAX_VALUE) throw badRequest("Value specified in decimal number is too large")
- this.frac = if (frac.isEmpty()) {
- 0
- } else {
- var tmp = frac.toIntOrNull() ?: throw badRequest("Invalid fractional value")
- repeat(8 - frac.length) {
- tmp *= 10
- }
- tmp
- }
- }
-
- override fun equals(other: Any?): Boolean {
- return other is DecimalNumber &&
- other.value == this.value &&
- other.frac == this.frac
- }
-
- override fun toString(): String {
- if (frac == 0) {
- return "$value"
- } else {
- return "$value.${frac.toString().padStart(8, '0')}"
- .dropLastWhile { it == '0' } // Trim useless fractional trailing 0
- }
- }
-
- internal object Serializer : KSerializer<DecimalNumber> {
- override val descriptor: SerialDescriptor =
- PrimitiveSerialDescriptor("DecimalNumber", PrimitiveKind.STRING)
-
- override fun serialize(encoder: Encoder, value: DecimalNumber) {
- encoder.encodeString(value.toString())
- }
-
- override fun deserialize(decoder: Decoder): DecimalNumber {
- return DecimalNumber(decoder.decodeString())
- }
- }
-
- companion object {
- private val PATTERN = Regex("([0-9]+)(?:\\.([0-9]{1,8}))?")
- }
-}
-
-
/**
* Internal representation of relative times. The
* "forever" case is represented with Long.MAX_VALUE.
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
index 2527bae6..cca56e57 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
@@ -182,6 +182,7 @@ data class RegisterAccountRequest(
val cashout_payto_uri: IbanPayto? = null,
val payto_uri: Payto? = null,
val debit_threshold: TalerAmount? = null,
+ val min_cashout: TalerAmount? = null,
val tan_channel: TanChannel? = null,
) {
init {
@@ -209,6 +210,7 @@ data class AccountReconfiguration(
val name: String? = null,
val is_public: Boolean? = null,
val debit_threshold: TalerAmount? = null,
+ val min_cashout: Option<TalerAmount?> = Option.None,
val tan_channel: Option<TanChannel?> = Option.None,
val is_taler_exchange: Boolean? = null,
)
@@ -327,14 +329,6 @@ data class TalerIntegrationConfigResponse(
val version: String = INTEGRATION_API_VERSION
}
-@Serializable
-data class RevenueConfig(
- val currency: String
-) {
- val name: String = "taler-revenue"
- val version: String = REVENUE_API_VERSION
-}
-
enum class CreditDebitInfo {
credit, debit
}
@@ -355,6 +349,7 @@ data class AccountMinimalData(
val payto_uri: String,
val balance: Balance,
val debit_threshold: TalerAmount,
+ val min_cashout: TalerAmount? = null,
val is_public: Boolean,
val is_taler_exchange: Boolean,
val row_id: Long,
@@ -378,6 +373,7 @@ data class AccountData(
val balance: Balance,
val payto_uri: String,
val debit_threshold: TalerAmount,
+ val min_cashout: TalerAmount? = null,
val contact_data: ChallengeContactData? = null,
val cashout_payto_uri: String? = null,
val tan_channel: TanChannel? = null,
@@ -546,21 +542,6 @@ data class ConversionResponse(
val amount_credit: TalerAmount,
)
-@Serializable
-data class RevenueIncomingHistory(
- val incoming_transactions : List<RevenueIncomingBankTransaction>,
- val credit_account: String
-)
-
-@Serializable
-data class RevenueIncomingBankTransaction(
- val row_id: Long,
- val date: TalerProtocolTimestamp,
- val amount: TalerAmount,
- val debit_account: String,
- val subject: String
-)
-
/**
* Response to GET /public-accounts
*/
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt
index c659b0f0..d57c504d 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt
@@ -126,7 +126,7 @@ private fun Routing.coreBankTokenApi(db: Database) {
}
call.respond(
TokenSuccessResponse(
- access_token = token.encoded(),
+ access_token = "$TOKEN_PREFIX$token",
expiration = TalerProtocolTimestamp(t_s = expirationTimestamp)
)
)
@@ -160,6 +160,12 @@ suspend fun createAccount(
"only admin account can choose the debit limit",
TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT
)
+
+ if (req.min_cashout != null)
+ throw conflict(
+ "only admin account can choose the minimum cashout amount",
+ TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT
+ )
if (req.tan_channel != null)
throw conflict(
@@ -188,31 +194,33 @@ suspend fun createAccount(
TalerErrorCode.END
)
+ suspend fun doDb(internalPayto: Payto) = db.account.create(
+ login = req.username,
+ name = req.name,
+ email = req.contact_data?.email?.get(),
+ phone = req.contact_data?.phone?.get(),
+ cashoutPayto = req.cashout_payto_uri,
+ password = req.password,
+ internalPayto = internalPayto,
+ isPublic = req.is_public,
+ isTalerExchange = req.is_taler_exchange,
+ maxDebt = req.debit_threshold ?: cfg.defaultDebtLimit,
+ bonus = if (!req.is_taler_exchange) cfg.registrationBonus
+ else TalerAmount(0, 0, cfg.regionalCurrency),
+ tanChannel = req.tan_channel,
+ checkPaytoIdempotent = req.payto_uri != null,
+ ctx = cfg.payto,
+ minCashout = req.min_cashout
+ )
+
when (cfg.wireMethod) {
WireMethod.IBAN -> {
- if (req.payto_uri != null && !(req.payto_uri is IbanPayto))
- throw badRequest("Expected an IBAN payto uri")
+ req.payto_uri?.expectRequestIban()
var retry = if (req.payto_uri == null) IBAN_ALLOCATION_RETRY_COUNTER else 0
while (true) {
val internalPayto = req.payto_uri ?: IbanPayto.rand() as Payto
- val res = db.account.create(
- login = req.username,
- name = req.name,
- email = req.contact_data?.email?.get(),
- phone = req.contact_data?.phone?.get(),
- cashoutPayto = req.cashout_payto_uri,
- password = req.password,
- internalPayto = internalPayto,
- isPublic = req.is_public,
- isTalerExchange = req.is_taler_exchange,
- maxDebt = req.debit_threshold ?: cfg.defaultDebtLimit,
- bonus = if (!req.is_taler_exchange) cfg.registrationBonus
- else TalerAmount(0, 0, cfg.regionalCurrency),
- tanChannel = req.tan_channel,
- checkPaytoIdempotent = req.payto_uri != null,
- ctx = cfg.payto
- )
+ val res = doDb(internalPayto)
// Retry with new IBAN
if (res == AccountCreationResult.PayToReuse && retry > 0) {
retry--
@@ -223,31 +231,13 @@ suspend fun createAccount(
}
WireMethod.X_TALER_BANK -> {
if (req.payto_uri != null) {
- if (!(req.payto_uri is XTalerBankPayto))
- throw badRequest("Expected an IBAN payto uri")
- else if (req.payto_uri.username != req.username)
- throw badRequest("Expected a payto uri for '${req.username}' got one for '${req.payto_uri.username}'")
+ val payto = req.payto_uri.expectRequestXTalerBank()
+ if (payto.username != req.username)
+ throw badRequest("Expected a payto uri for '${req.username}' got one for '${payto.username}'")
}
val internalPayto = XTalerBankPayto.forUsername(req.username)
-
- return db.account.create(
- login = req.username,
- name = req.name,
- email = req.contact_data?.email?.get(),
- phone = req.contact_data?.phone?.get(),
- cashoutPayto = req.cashout_payto_uri,
- password = req.password,
- internalPayto = internalPayto,
- isPublic = req.is_public,
- isTalerExchange = req.is_taler_exchange,
- maxDebt = req.debit_threshold ?: cfg.defaultDebtLimit,
- bonus = if (!req.is_taler_exchange) cfg.registrationBonus
- else TalerAmount(0, 0, cfg.regionalCurrency),
- tanChannel = req.tan_channel,
- checkPaytoIdempotent = req.payto_uri != null,
- ctx = cfg.payto
- )
+ return doDb(internalPayto)
}
}
}
@@ -283,6 +273,7 @@ suspend fun patchAccount(
tan_channel = req.tan_channel,
isPublic = req.is_public,
debtLimit = req.debit_threshold,
+ minCashout = req.min_cashout,
isAdmin = isAdmin,
is2fa = is2fa,
faChannel = channel,
@@ -367,6 +358,10 @@ private fun Routing.coreBankAccountsApi(db: Database, ctx: BankConfig) {
"non-admin user cannot change their debt limit",
TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT
)
+ AccountPatchResult.NonAdminMinCashout -> throw conflict(
+ "non-admin user cannot change their min cashout amount",
+ TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT
+ )
AccountPatchResult.MissingTanInfo -> throw conflict(
"missing info for tan channel ${req.tan_channel.get()}",
TalerErrorCode.BANK_MISSING_TAN_INFO
@@ -595,6 +590,10 @@ private fun Routing.coreBankCashoutApi(db: Database, ctx: BankConfig) = conditio
"Wrong currency conversion",
TalerErrorCode.BANK_BAD_CONVERSION
)
+ CashoutCreationResult.UnderMin -> throw conflict(
+ "Amount of currency conversion it less than the minimum allowed",
+ TalerErrorCode.BANK_CONVERSION_AMOUNT_TO_SMALL
+ )
CashoutCreationResult.AccountIsExchange -> throw conflict(
"Exchange account cannot perform cashout operation",
TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt b/bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt
index c0ed1100..56bdba2d 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/auth/auth.kt
@@ -28,6 +28,7 @@ import io.ktor.util.pipeline.*
import tech.libeufin.bank.*
import tech.libeufin.bank.db.Database
import tech.libeufin.common.*
+import tech.libeufin.common.api.*
import tech.libeufin.common.crypto.PwCrypto
import java.time.Instant
@@ -37,6 +38,8 @@ private val AUTH_IS_ADMIN = AttributeKey<Boolean>("is_admin")
/** Used to store used auth token */
private val AUTH_TOKEN = AttributeKey<ByteArray>("auth_token")
+const val TOKEN_PREFIX = "secret-token:"
+
/** Get username of the request account */
val ApplicationCall.username: String get() = parameters.expect("USERNAME")
/** Get username of the request account */
@@ -155,7 +158,7 @@ private suspend fun ApplicationCall.doTokenAuth(
bearer: String,
requiredScope: TokenScope,
): String {
- if (!bearer.startsWith("secret-token:")) throw badRequest(
+ if (!bearer.startsWith(TOKEN_PREFIX)) throw badRequest(
"Bearer token malformed",
TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED
)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt
index 036ec116..97ca874b 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt
@@ -47,6 +47,7 @@ class AccountDAO(private val db: Database) {
isPublic: Boolean,
isTalerExchange: Boolean,
maxDebt: TalerAmount,
+ minCashout: TalerAmount?,
bonus: TalerAmount,
tanChannel: TanChannel?,
// Whether to check [internalPaytoUri] for idempotency
@@ -70,7 +71,7 @@ class AccountDAO(private val db: Database) {
ON customer_id=owning_customer_id
WHERE login=?
""").run {
- // TODO check max debt
+ // TODO check max debt and min checkout ?
setString(1, name)
setString(2, email)
setString(3, phone)
@@ -141,7 +142,8 @@ class AccountDAO(private val db: Database) {
,is_public
,is_taler_exchange
,max_debt
- ) VALUES (?, ?, ?, ?, (?, ?)::taler_amount)
+ ,min_cashout
+ ) VALUES (?, ?, ?, ?, (?, ?)::taler_amount, ${if (minCashout == null) "NULL" else "(?, ?)::taler_amount"})
""").run {
setString(1, internalPayto.canonical)
setLong(2, customerId)
@@ -149,6 +151,10 @@ class AccountDAO(private val db: Database) {
setBoolean(4, isTalerExchange)
setLong(5, maxDebt.value)
setInt(6, maxDebt.frac)
+ if (minCashout != null) {
+ setLong(7, minCashout.value)
+ setInt(8, minCashout.frac)
+ }
if (!executeUpdateViolation()) {
conn.rollback()
return@transaction AccountCreationResult.PayToReuse
@@ -223,6 +229,7 @@ class AccountDAO(private val db: Database) {
data object NonAdminName: AccountPatchResult
data object NonAdminCashout: AccountPatchResult
data object NonAdminDebtLimit: AccountPatchResult
+ data object NonAdminMinCashout: AccountPatchResult
data object MissingTanInfo: AccountPatchResult
data class TanRequired(val channel: TanChannel?, val info: String?): AccountPatchResult
data object Success: AccountPatchResult
@@ -238,6 +245,7 @@ class AccountDAO(private val db: Database) {
tan_channel: Option<TanChannel?>,
isPublic: Boolean?,
debtLimit: TalerAmount?,
+ minCashout: Option<TalerAmount?>,
isAdmin: Boolean,
is2fa: Boolean,
faChannel: TanChannel?,
@@ -248,6 +256,7 @@ class AccountDAO(private val db: Database) {
val checkName = !isAdmin && !allowEditName && name != null
val checkCashout = !isAdmin && !allowEditCashout && cashoutPayto.isSome()
val checkDebtLimit = !isAdmin && debtLimit != null
+ val checkMinCashout = !isAdmin && minCashout.isSome()
data class CurrentAccount(
val id: Long,
@@ -257,6 +266,7 @@ class AccountDAO(private val db: Database) {
val name: String,
val cashoutPayTo: String?,
val debtLimit: TalerAmount,
+ val minCashout: TalerAmount?
)
// Get user ID and current data
@@ -265,6 +275,8 @@ class AccountDAO(private val db: Database) {
customer_id, tan_channel, phone, email, name, cashout_payto
,(max_debt).val AS max_debt_val
,(max_debt).frac AS max_debt_frac
+ ,(min_cashout).val AS min_cashout_val
+ ,(min_cashout).frac AS min_cashout_frac
FROM customers
JOIN bank_accounts
ON customer_id=owning_customer_id
@@ -280,6 +292,7 @@ class AccountDAO(private val db: Database) {
name = it.getString("name"),
cashoutPayTo = it.getString("cashout_payto"),
debtLimit = it.getAmount("max_debt", db.bankCurrency),
+ minCashout = it.getOptAmount("min_cashout", db.bankCurrency),
)
} ?: return@transaction AccountPatchResult.UnknownAccount
}
@@ -310,10 +323,11 @@ class AccountDAO(private val db: Database) {
return@transaction AccountPatchResult.NonAdminCashout
if (checkDebtLimit && debtLimit != curr.debtLimit)
return@transaction AccountPatchResult.NonAdminDebtLimit
+ if (checkMinCashout && minCashout.get() != curr.minCashout)
+ return@transaction AccountPatchResult.NonAdminMinCashout
if (patchChannel != null && newInfo == null)
return@transaction AccountPatchResult.MissingTanInfo
-
// Tan channel verification
if (!isAdmin) {
// Check performed 2fa check
@@ -340,11 +354,24 @@ class AccountDAO(private val db: Database) {
sequence {
if (isPublic != null) yield("is_public=?")
if (debtLimit != null) yield("max_debt=(?, ?)::taler_amount")
+ minCashout.some {
+ if (it != null) {
+ yield("min_cashout=(?, ?)::taler_amount")
+ } else {
+ yield("min_cashout=null")
+ }
+ }
},
"WHERE owning_customer_id = ?",
sequence {
isPublic?.let { yield(it) }
debtLimit?.let { yield(it.value); yield(it.frac) }
+ minCashout.some {
+ if (it != null) {
+ yield(it.value)
+ yield(it.frac)
+ }
+ }
yield(curr.id)
}
)
@@ -464,6 +491,8 @@ class AccountDAO(private val db: Database) {
,has_debt
,(max_debt).val AS max_debt_val
,(max_debt).frac AS max_debt_frac
+ ,(min_cashout).val AS min_cashout_val
+ ,(min_cashout).frac AS min_cashout_frac
,is_public
,is_taler_exchange
,CASE
@@ -496,6 +525,7 @@ class AccountDAO(private val db: Database) {
}
),
debit_threshold = it.getAmount("max_debt", db.bankCurrency),
+ min_cashout = it.getOptAmount("min_cashout", db.bankCurrency),
is_public = it.getBoolean("is_public"),
is_taler_exchange = it.getBoolean("is_taler_exchange"),
status = AccountStatus.valueOf(it.getString("status"))
@@ -557,6 +587,8 @@ class AccountDAO(private val db: Database) {
has_debt AS balance_has_debt,
(max_debt).val as max_debt_val,
(max_debt).frac as max_debt_frac
+ ,(min_cashout).val AS min_cashout_val
+ ,(min_cashout).frac AS min_cashout_frac
,is_public
,is_taler_exchange
,internal_payto_uri
@@ -587,6 +619,7 @@ class AccountDAO(private val db: Database) {
}
),
debit_threshold = it.getAmount("max_debt", db.bankCurrency),
+ min_cashout = it.getOptAmount("min_cashout", db.bankCurrency),
is_public = it.getBoolean("is_public"),
is_taler_exchange = it.getBoolean("is_taler_exchange"),
payto_uri = it.getBankPayto("internal_payto_uri", "name", ctx),
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt
index 799d466a..d8779613 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt
@@ -29,6 +29,7 @@ class CashoutDAO(private val db: Database) {
/** Result of cashout operation creation */
sealed interface CashoutCreationResult {
data class Success(val id: Long): CashoutCreationResult
+ data object UnderMin: CashoutCreationResult
data object BadConversion: CashoutCreationResult
data object AccountNotFound: CashoutCreationResult
data object AccountIsExchange: CashoutCreationResult
@@ -57,7 +58,8 @@ class CashoutDAO(private val db: Database) {
out_request_uid_reuse,
out_no_cashout_payto,
out_tan_required,
- out_cashout_id
+ out_cashout_id,
+ out_under_min
FROM cashout_create(?,?,(?,?)::taler_amount,(?,?)::taler_amount,?,?,?)
""")
stmt.setString(1, login)
@@ -73,6 +75,7 @@ class CashoutDAO(private val db: Database) {
when {
!it.next() ->
throw internalServerError("No result from DB procedure cashout_create")
+ it.getBoolean("out_under_min") -> CashoutCreationResult.UnderMin
it.getBoolean("out_bad_conversion") -> CashoutCreationResult.BadConversion
it.getBoolean("out_account_not_found") -> CashoutCreationResult.AccountNotFound
it.getBoolean("out_account_is_exchange") -> CashoutCreationResult.AccountIsExchange
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt
index 04317b68..8afd4f7f 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt
@@ -20,7 +20,6 @@
package tech.libeufin.bank.db
import tech.libeufin.bank.ConversionRate
-import tech.libeufin.bank.DecimalNumber
import tech.libeufin.bank.RoundingMode
import tech.libeufin.common.*
import tech.libeufin.common.db.*
@@ -110,7 +109,7 @@ class ConversionDAO(private val db: Database) {
/** Perform [direction] conversion of [amount] using in-db [function] */
private suspend fun conversion(amount: TalerAmount, direction: String, function: String): ConversionResult = db.conn { conn ->
- val stmt = conn.prepareStatement("SELECT too_small, no_config, (converted).val AS amount_val, (converted).frac AS amount_frac FROM $function((?, ?)::taler_amount, ?)")
+ val stmt = conn.prepareStatement("SELECT too_small, no_config, (converted).val AS amount_val, (converted).frac AS amount_frac FROM $function((?, ?)::taler_amount, ?, (0, 0)::taler_amount)")
stmt.setLong(1, amount.value)
stmt.setInt(2, amount.frac)
stmt.setString(3, direction)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt
index 9efb821a..1236b1ad 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt
@@ -96,13 +96,13 @@ class Database(dbConfig: DatabaseConfig, internal val bankCurrency: String, inte
/** Listen for new bank transactions for [account] */
suspend fun <R> listenBank(account: Long, lambda: suspend (Flow<Long>) -> R): R
= listen(bankTxFlows, account, lambda)
- /** Listen for new taler outgoing transactions from [account] */
+ /** Listen for new taler outgoing transactions from [exchange] */
suspend fun <R> listenOutgoing(exchange: Long, lambda: suspend (Flow<Long>) -> R): R
= listen(outgoingTxFlows, exchange, lambda)
- /** Listen for new taler incoming transactions to [account] */
+ /** Listen for new taler incoming transactions to [exchange] */
suspend fun <R> listenIncoming(exchange: Long, lambda: suspend (Flow<Long>) -> R): R
= listen(incomingTxFlows, exchange, lambda)
- /** Listen for new taler outgoing transactions to [account] */
+ /** Listen for new incoming transactions to [merchant] */
suspend fun <R> listenRevenue(merchant: Long, lambda: suspend (Flow<Long>) -> R): R
= listen(revenueTxFlows, merchant, lambda)
/** Listen for new withdrawal confirmations */
@@ -163,8 +163,4 @@ enum class AbortResult {
Success,
UnknownOperation,
AlreadyConfirmed
-}
-
-fun ResultSet.getTalerTimestamp(name: String): TalerProtocolTimestamp{
- return TalerProtocolTimestamp(getLong(name).asInstant())
} \ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
index 094b7996..8c7bd21b 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
@@ -37,6 +37,7 @@ import tech.libeufin.bank.auth.username
import tech.libeufin.bank.db.AccountDAO.AccountCreationResult
import tech.libeufin.bank.db.Database
import tech.libeufin.common.*
+import tech.libeufin.common.api.*
import java.util.*
fun ApplicationCall.uuidPath(name: String): UUID {
@@ -128,24 +129,11 @@ suspend fun createAdminAccount(db: Database, cfg: BankConfig, pw: String? = null
phone = null,
cashoutPayto = null,
tanChannel = null,
+ minCashout = null,
ctx = cfg.payto
)
}
-fun Route.intercept(callback: Route.() -> Unit, interceptor: suspend PipelineContext<Unit, ApplicationCall>.() -> Unit): Route {
- val subRoute = createChild(object : RouteSelector() {
- override fun evaluate(context: RoutingResolveContext, segmentIndex: Int): RouteSelectorEvaluation =
- RouteSelectorEvaluation.Constant
- })
- subRoute.intercept(ApplicationCallPipeline.Plugins) {
- interceptor()
- proceed()
- }
-
- callback(subRoute)
- return subRoute
-}
-
fun Route.conditional(implemented: Boolean, callback: Route.() -> Unit): Route =
intercept(callback) {
if (!implemented) {
diff --git a/bank/src/test/kotlin/AmountTest.kt b/bank/src/test/kotlin/AmountTest.kt
index 9f7b14aa..a93a10ba 100644
--- a/bank/src/test/kotlin/AmountTest.kt
+++ b/bank/src/test/kotlin/AmountTest.kt
@@ -18,7 +18,6 @@
*/
import org.junit.Test
-import tech.libeufin.bank.DecimalNumber
import tech.libeufin.bank.db.TransactionDAO.BankTransactionResult
import tech.libeufin.bank.db.WithdrawalDAO.WithdrawalCreationResult
import tech.libeufin.common.*
diff --git a/bank/src/test/kotlin/ConversionApiTest.kt b/bank/src/test/kotlin/ConversionApiTest.kt
index 0954d3fa..c4aa0096 100644
--- a/bank/src/test/kotlin/ConversionApiTest.kt
+++ b/bank/src/test/kotlin/ConversionApiTest.kt
@@ -45,7 +45,7 @@ class ConversionApiTest {
}
// Too small
- client.get("/conversion-info/cashout-rate?amount_debit=KUDOS:0.08")
+ client.get("/conversion-info/cashout-rate?amount_debit=KUDOS:0.0008")
.assertConflict(TalerErrorCode.BANK_BAD_CONVERSION)
// No amount
client.get("/conversion-info/cashout-rate")
diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt
index 6e8af912..43a65985 100644
--- a/bank/src/test/kotlin/CoreBankApiTest.kt
+++ b/bank/src/test/kotlin/CoreBankApiTest.kt
@@ -1,6 +1,6 @@
/*
* This file is part of LibEuFin.
- * Copyright (C) 2023 Taler Systems S.A.
+ * Copyright (C) 2023-2024 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
@@ -24,6 +24,7 @@ import io.ktor.server.testing.*
import kotlinx.serialization.json.JsonElement
import org.junit.Test
import tech.libeufin.bank.*
+import tech.libeufin.bank.auth.*
import tech.libeufin.common.*
import java.time.Duration
import java.time.Instant
@@ -64,7 +65,7 @@ class CoreBankTokenApiTest {
json { "scope" to "readonly" }
}.assertOkJson<TokenSuccessResponse> {
// Checking that the token lifetime defaulted to 24 hours.
- val token = db.token.get(Base32Crockford.decode(it.access_token))
+ val token = db.token.get(Base32Crockford.decode(it.access_token.removePrefix(TOKEN_PREFIX)))
val lifeTime = Duration.between(token!!.creationTime, token.expirationTime)
assertEquals(Duration.ofDays(1), lifeTime)
}
@@ -74,7 +75,7 @@ class CoreBankTokenApiTest {
json { "scope" to "readonly" }
}.assertOkJson<TokenSuccessResponse> {
// Checking that the token lifetime defaulted to 24 hours.
- val token = db.token.get(Base32Crockford.decode(it.access_token))
+ val token = db.token.get(Base32Crockford.decode(it.access_token.removePrefix(TOKEN_PREFIX)))
val lifeTime = Duration.between(token!!.creationTime, token.expirationTime)
assertEquals(Duration.ofDays(1), lifeTime)
}
@@ -88,7 +89,7 @@ class CoreBankTokenApiTest {
}.assertOkJson<TokenSuccessResponse> {
val token = it.access_token
client.post("/accounts/merchant/token") {
- headers["Authorization"] = "Bearer secret-token:$token"
+ headers["Authorization"] = "Bearer $token"
json { "scope" to "readonly" }
}.assertOk()
}
@@ -142,11 +143,11 @@ class CoreBankTokenApiTest {
}.assertOkJson<TokenSuccessResponse>().access_token
// Check OK
client.delete("/accounts/merchant/token") {
- headers["Authorization"] = "Bearer secret-token:$token"
+ headers["Authorization"] = "Bearer $token"
}.assertNoContent()
// Check token no longer work
client.delete("/accounts/merchant/token") {
- headers["Authorization"] = "Bearer secret-token:$token"
+ headers["Authorization"] = "Bearer $token"
}.assertUnauthorized()
// Checking merchant can still be served by basic auth, after token deletion.
@@ -226,11 +227,27 @@ class CoreBankAccountsApiTest {
}.assertOk()
}
- // Check admin only tan_channel
+ // Check admin only min_cashout
obj {
"username" to "bat2"
"password" to "password"
"name" to "Bat"
+ "min_cashout" to "KUDOS:42"
+ }.let { req ->
+ client.post("/accounts") {
+ json(req)
+ }.assertConflict(TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT)
+ client.post("/accounts") {
+ json(req)
+ pwAuth("admin")
+ }.assertOk()
+ }
+
+ // Check admin only tan_channel
+ obj {
+ "username" to "bat3"
+ "password" to "password"
+ "name" to "Bat"
"contact_data" to obj {
"phone" to "+456"
}
@@ -459,8 +476,6 @@ class CoreBankAccountsApiTest {
}.assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT)
}
-
-
@Test
fun softDelete() = bankSetup { db ->
// Create all kind of operations
@@ -501,7 +516,7 @@ class CoreBankAccountsApiTest {
// Check account can no longer login
client.delete("/accounts/customer/token") {
- headers["Authorization"] = "Bearer secret-token:$token"
+ headers["Authorization"] = "Bearer $token"
}.assertUnauthorized()
client.getA("/accounts/customer/transactions/$tx_id").assertUnauthorized()
client.getA("/accounts/customer/cashouts/$cashout_id").assertUnauthorized()
@@ -600,6 +615,10 @@ class CoreBankAccountsApiTest {
obj(req) { "debit_threshold" to "KUDOS:100" },
TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT
)
+ checkAdminOnly(
+ obj(req) { "min_cashout" to "KUDOS:100" },
+ TalerErrorCode.BANK_NON_ADMIN_SET_MIN_CASHOUT
+ )
// Check currency
client.patch("/accounts/merchant") {
@@ -1319,6 +1338,36 @@ class CoreBankCashoutApiTest {
}
}.assertConflict(TalerErrorCode.BANK_BAD_CONVERSION)
+ // Check min amount
+ client.postA("/accounts/customer/cashouts") {
+ json(req) {
+ "request_uid" to ShortHashCode.rand()
+ "amount_debit" to "KUDOS:0.09"
+ "amount_credit" to convert("KUDOS:0.09")
+ }
+ }.assertConflict(TalerErrorCode.BANK_CONVERSION_AMOUNT_TO_SMALL)
+
+ // Check custom min account
+ client.patch("/accounts/customer") {
+ pwAuth("admin")
+ json {
+ "min_cashout" to "KUDOS:10"
+ }
+ }.assertNoContent()
+ client.postA("/accounts/customer/cashouts") {
+ json(req) {
+ "request_uid" to ShortHashCode.rand()
+ "amount_debit" to "KUDOS:5"
+ "amount_credit" to convert("KUDOS:5")
+ }
+ }.assertConflict(TalerErrorCode.BANK_CONVERSION_AMOUNT_TO_SMALL)
+ client.patch("/accounts/customer") {
+ pwAuth("admin")
+ json {
+ "min_cashout" to (null as String?)
+ }
+ }.assertNoContent()
+
// Check wrong currency
client.postA("/accounts/customer/cashouts") {
json(req) {
diff --git a/bank/src/test/kotlin/RevenueApiTest.kt b/bank/src/test/kotlin/RevenueApiTest.kt
index 6c694842..3c692f90 100644
--- a/bank/src/test/kotlin/RevenueApiTest.kt
+++ b/bank/src/test/kotlin/RevenueApiTest.kt
@@ -19,7 +19,6 @@
import io.ktor.http.*
import org.junit.Test
-import tech.libeufin.bank.RevenueIncomingHistory
import tech.libeufin.common.*
class RevenueApiTest {
diff --git a/bank/src/test/kotlin/StatsTest.kt b/bank/src/test/kotlin/StatsTest.kt
index f5ec3161..240c23a4 100644
--- a/bank/src/test/kotlin/StatsTest.kt
+++ b/bank/src/test/kotlin/StatsTest.kt
@@ -23,10 +23,8 @@ import tech.libeufin.bank.MonitorParams
import tech.libeufin.bank.MonitorResponse
import tech.libeufin.bank.MonitorWithConversion
import tech.libeufin.bank.Timeframe
-import tech.libeufin.common.ShortHashCode
-import tech.libeufin.common.TalerAmount
import tech.libeufin.common.db.executeQueryCheck
-import tech.libeufin.common.micros
+import tech.libeufin.common.*
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneOffset
diff --git a/bank/src/test/kotlin/WireGatewayApiTest.kt b/bank/src/test/kotlin/WireGatewayApiTest.kt
index 1c6c60ef..10482bdb 100644
--- a/bank/src/test/kotlin/WireGatewayApiTest.kt
+++ b/bank/src/test/kotlin/WireGatewayApiTest.kt
@@ -30,7 +30,7 @@ class WireGatewayApiTest {
client.getA("/accounts/merchant/taler-wire-gateway/config").assertOk()
}
- // Testing the POST /transfer call from the TWG API.
+ // POST /accounts/{USERNAME}/taler-wire-gateway/transfer
@Test
fun transfer() = bankSetup { _ ->
val valid_req = obj {
@@ -121,9 +121,7 @@ class WireGatewayApiTest {
}.assertBadRequest()
}
- /**
- * Testing the /history/incoming call from the TWG API.
- */
+ // GET /accounts/{USERNAME}/taler-wire-gateway/history/incoming
@Test
fun historyIncoming() = bankSetup {
// Give Foo reasonable debt allowance:
@@ -159,10 +157,7 @@ class WireGatewayApiTest {
)
}
-
- /**
- * Testing the /history/outgoing call from the TWG API.
- */
+ // GET /accounts/{USERNAME}/taler-wire-gateway/history/outgoing
@Test
fun historyOutgoing() = bankSetup {
setMaxDebt("exchange", "KUDOS:1000000")
@@ -193,7 +188,7 @@ class WireGatewayApiTest {
)
}
- // Testing the /admin/add-incoming call from the TWG API.
+ // POST /accounts/{USERNAME}/taler-wire-gateway/admin/add-incoming
@Test
fun addIncoming() = bankSetup { _ ->
val valid_req = obj {
diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt
index 73240c15..0a253929 100644
--- a/bank/src/test/kotlin/helpers.kt
+++ b/bank/src/test/kotlin/helpers.kt
@@ -96,6 +96,7 @@ fun bankSetup(
phone = null,
cashoutPayto = null,
tanChannel = null,
+ minCashout = null,
ctx = cfg.payto
))
assertIs<AccountCreationResult.Success>(db.account.create(
@@ -112,6 +113,7 @@ fun bankSetup(
phone = null,
cashoutPayto = null,
tanChannel = null,
+ minCashout = null,
ctx = cfg.payto
))
assertIs<AccountCreationResult.Success>(db.account.create(
@@ -128,6 +130,7 @@ fun bankSetup(
phone = null,
cashoutPayto = null,
tanChannel = null,
+ minCashout = null,
ctx = cfg.payto
))
// Create admin account
@@ -343,15 +346,6 @@ suspend fun HttpResponse.assertChallenge(
}
}
-suspend fun assertTime(min: Int, max: Int, lambda: suspend () -> Unit) {
- val start = System.currentTimeMillis()
- lambda()
- val end = System.currentTimeMillis()
- val time = end - start
- assert(time >= min) { "Expected to last at least $min ms, lasted $time" }
- assert(time <= max) { "Expected to last at most $max ms, lasted $time" }
-}
-
fun assertException(msg: String, lambda: () -> Unit) {
try {
lambda()
@@ -361,47 +355,6 @@ fun assertException(msg: String, lambda: () -> Unit) {
}
}
-suspend inline fun <reified B> HttpResponse.assertHistoryIds(size: Int, ids: (B) -> List<Long>): B {
- assertOk()
- val body = json<B>()
- val history = ids(body)
- val params = PageParams.extract(call.request.url.parameters)
-
- // testing the size is like expected.
- assertEquals(size, history.size, "bad history length: $history")
- if (params.delta < 0) {
- // testing that the first id is at most the 'start' query param.
- assert(history[0] <= params.start) { "bad history start: $params $history" }
- // testing that the id decreases.
- if (history.size > 1)
- assert(history.windowed(2).all { (a, b) -> a > b }) { "bad history order: $history" }
- } else {
- // testing that the first id is at least the 'start' query param.
- assert(history[0] >= params.start) { "bad history start: $params $history" }
- // testing that the id increases.
- if (history.size > 1)
- assert(history.windowed(2).all { (a, b) -> a < b }) { "bad history order: $history" }
- }
-
- return body
-}
-
-/* ----- Body helper ----- */
-
-suspend inline fun <reified B> HttpResponse.assertOkJson(lambda: (B) -> Unit = {}): B {
- assertOk()
- val body = json<B>()
- lambda(body)
- return body
-}
-
-suspend inline fun <reified B> HttpResponse.assertAcceptedJson(lambda: (B) -> Unit = {}): B {
- assertAccepted()
- val body = json<B>()
- lambda(body)
- return body
-}
-
/* ----- Auth ----- */
/** Auto auth get request */
@@ -446,7 +399,6 @@ fun HttpRequestBuilder.pwAuth(username: String? = null) {
val login = url.pathSegments[2]
basicAuth("$login", "$login-password")
}
-
}
/* ----- Random data generation ----- */
diff --git a/bank/src/test/kotlin/routines.kt b/bank/src/test/kotlin/routines.kt
index d49a3324..93e619ed 100644
--- a/bank/src/test/kotlin/routines.kt
+++ b/bank/src/test/kotlin/routines.kt
@@ -28,6 +28,7 @@ import kotlinx.serialization.json.JsonObject
import tech.libeufin.bank.BankAccountCreateWithdrawalResponse
import tech.libeufin.bank.WithdrawalStatus
import tech.libeufin.common.*
+import tech.libeufin.common.test.*
import kotlin.test.assertEquals
// Test endpoint is correctly authenticated
@@ -40,6 +41,17 @@ suspend fun ApplicationTestBuilder.authRoutine(
allowAdmin: Boolean = false
) {
// No body when authentication must happen before parsing the body
+
+ // No header
+ client.request(path) {
+ this.method = method
+ }.assertUnauthorized(TalerErrorCode.GENERIC_PARAMETER_MISSING)
+
+ // Bad header
+ client.request(path) {
+ this.method = method
+ headers["Authorization"] = "WTF"
+ }.assertBadRequest(TalerErrorCode.GENERIC_HTTP_HEADERS_MALFORMED)
// Unknown account
client.request(path) {
@@ -60,7 +72,7 @@ suspend fun ApplicationTestBuilder.authRoutine(
}.assertUnauthorized()
if (requireAdmin) {
- // Not exchange account
+ // Not exchange account
client.request(path) {
this.method = method
pwAuth("merchant")
@@ -91,117 +103,11 @@ suspend inline fun <reified B> ApplicationTestBuilder.historyRoutine(
polling: Boolean = true,
auth: String? = null
) {
- // Get history
- val history: suspend (String) -> HttpResponse = { params: String ->
+ abstractHistoryRoutine(ids, registered, ignored, polling) { params: String ->
client.get("$url?$params") {
pwAuth(auth)
}
}
- // Check history is following specs
- val assertHistory: suspend HttpResponse.(Int) -> Unit = { size: Int ->
- assertHistoryIds<B>(size, ids)
- }
- // Get latest registered id
- val latestId: suspend () -> Long = {
- history("delta=-1").assertOkJson<B>().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 <reified B> ApplicationTestBuilder.statusRoutine(