taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

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:
Apackages/taler-wallet-core/src/kyc.test.ts | 113+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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, + ); +});