commit 5ce1e53ef652bd991a5c790f95177dc4fe2f64db
parent 287de663c448d082b8f3af217d32b2e9f8c23ddb
Author: Florian Dold <dold@taler.net>
Date: Wed, 22 Jul 2026 10:40:36 +0200
wallet: test limit checking against a mix of soft and hard rules
Covers the threshold boundary, operation filtering and which of several
breached rules decides the outcome.
Diffstat:
1 file changed, 113 insertions(+), 0 deletions(-)
diff --git a/packages/taler-wallet-core/src/kyc.test.ts b/packages/taler-wallet-core/src/kyc.test.ts
@@ -0,0 +1,113 @@
+/*
+ This file is part of GNU Taler
+ (C) 2026 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import {
+ AccountLimit,
+ AmountString,
+ Duration,
+ LimitOperationType,
+} from "@gnu-taler/taler-util";
+import assert from "node:assert";
+import { test } from "node:test";
+import { checkLimit, LimitCheckResult } from "./kyc.js";
+
+function limit(
+ threshold: AmountString,
+ soft: boolean | undefined,
+): AccountLimit {
+ return {
+ operation_type: LimitOperationType.withdraw,
+ timeframe: Duration.toTalerProtocolDuration(Duration.fromSpec({ days: 1 })),
+ threshold,
+ soft_limit: soft,
+ };
+}
+
+test("a limit is only breached once the amount exceeds it", async (t) => {
+ const rules = [limit("KUDOS:10" as AmountString, false)];
+
+ assert.strictEqual(
+ await checkLimit(rules, LimitOperationType.withdraw, "KUDOS:10"),
+ LimitCheckResult.Allowed,
+ );
+ assert.strictEqual(
+ await checkLimit(rules, LimitOperationType.withdraw, "KUDOS:10.01"),
+ LimitCheckResult.DeniedVerboten,
+ );
+});
+
+test("only rules for the requested operation are considered", async (t) => {
+ const rules = [
+ {
+ ...limit("KUDOS:5" as AmountString, false),
+ operation_type: LimitOperationType.deposit,
+ },
+ ];
+
+ assert.strictEqual(
+ await checkLimit(rules, LimitOperationType.withdraw, "KUDOS:100"),
+ LimitCheckResult.Allowed,
+ );
+});
+
+test("a breached soft limit alone can be raised by KYC", async (t) => {
+ const rules = [limit("KUDOS:5" as AmountString, true)];
+
+ assert.strictEqual(
+ await checkLimit(rules, LimitOperationType.withdraw, "KUDOS:100"),
+ LimitCheckResult.DeniedSoft,
+ );
+});
+
+test("a breached hard limit wins over a smaller breached soft limit", async (t) => {
+ const rules = [
+ limit("KUDOS:5" as AmountString, true),
+ limit("KUDOS:20" as AmountString, false),
+ ];
+
+ assert.strictEqual(
+ await checkLimit(rules, LimitOperationType.withdraw, "KUDOS:100"),
+ LimitCheckResult.DeniedVerboten,
+ );
+ // Order of the rules must not matter.
+ assert.strictEqual(
+ await checkLimit(rules.slice().reverse(), LimitOperationType.withdraw, "KUDOS:100"),
+ LimitCheckResult.DeniedVerboten,
+ );
+});
+
+test("a hard limit that is not breached does not deny", async (t) => {
+ const rules = [
+ limit("KUDOS:5" as AmountString, true),
+ limit("KUDOS:200" as AmountString, false),
+ ];
+
+ assert.strictEqual(
+ await checkLimit(rules, LimitOperationType.withdraw, "KUDOS:100"),
+ LimitCheckResult.DeniedSoft,
+ );
+});
+
+test("a missing soft_limit means the rule is hard", async (t) => {
+ const rules = [
+ limit("KUDOS:5" as AmountString, true),
+ limit("KUDOS:20" as AmountString, undefined),
+ ];
+
+ assert.strictEqual(
+ await checkLimit(rules, LimitOperationType.withdraw, "KUDOS:100"),
+ LimitCheckResult.DeniedVerboten,
+ );
+});