taler-typescript-core

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

commit 148533ba6b91fe8bad5b6940b9c60d9c067d6291
parent 5d35ea590cc66aeba0a6beb69bd94c3b8c6627a5
Author: Florian Dold <dold@taler.net>
Date:   Thu, 27 Aug 2026 01:00:04 +0200

AML web UI: run decision regression tests

Diffstat:
Mpackages/taler-exchange-aml-webui/package.json | 2+-
Apackages/taler-exchange-aml-webui/src/utils/decision-validation.test.ts | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-util/src/types-taler-exchange.test.ts | 316++++++++++++++++++++++++++++++++++++++++---------------------------------------
3 files changed, 299 insertions(+), 155 deletions(-)

diff --git a/packages/taler-exchange-aml-webui/package.json b/packages/taler-exchange-aml-webui/package.json @@ -12,7 +12,7 @@ "clean": "rm -rf dist lib", "build": "tsc && ./build.mjs", "build:with-deps": "pnpm --filter \"{.}...\" run build", - "test": "echo No tests for aml-backoffice-ui", + "test": "./test.mjs && node --test 'dist/test/**/*.test.js' 'dist/test/**/test.js'", "lint": "../qa-tooling/bin/eslint.mjs .", "i18n:source2po": "pogen extract && pogen merge", "i18n:po2strings": "pogen emit", diff --git a/packages/taler-exchange-aml-webui/src/utils/decision-validation.test.ts b/packages/taler-exchange-aml-webui/src/utils/decision-validation.test.ts @@ -0,0 +1,136 @@ +/* + 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. +*/ + +import assert from "node:assert"; +import { test } from "node:test"; +import { + AbsoluteTime, + AvailableMeasureSummary, + TalerFormAttributes, +} from "@gnu-taler/taler-util"; +import { DecisionRequest } from "../hooks/decision-request.js"; +import { decisionTargetsMatch } from "../hooks/decision-request.js"; +import { + attributesAreComplete, + buildAmlDecisionRequest, +} from "./decision-validation.js"; + +const measureSummary: AvailableMeasureSummary = { + roots: { + review: { check_name: "review" }, + skipOne: { check_name: "SKIP" }, + skipTwo: { check_name: "SKIP" }, + }, + programs: {}, + checks: {}, + default_rules: [], +}; + +function completeDecision(): DecisionRequest { + return { + rules: [], + deadline: AbsoluteTime.never(), + properties: { builtin: true }, + custom_properties: { custom: "value" }, + triggering_events: ["KNOWN"], + custom_events: ["CUSTOM", "KNOWN"], + new_measures: ["review"], + measures_and: true, + justification: "Reviewed account", + keep_investigating: false, + }; +} + +test("decision builder preserves custom state and AND measures", () => { + const result = buildAmlDecisionRequest({ + account: "ACCOUNT", + fullPayto: undefined, + decision: completeDecision(), + measures: measureSummary, + }); + assert.deepStrictEqual(result.errors, []); + assert.deepStrictEqual(result.request?.properties, { + builtin: true, + custom: "value", + }); + assert.deepStrictEqual(result.request?.events, ["KNOWN", "CUSTOM"]); + assert.strictEqual(result.request?.new_measures, "+review"); +}); + +test("attribute uploads require salt and a future expiration", () => { + const decision = completeDecision(); + decision.attributes = { + data: { answer: 42 }, + formId: "example", + formVersion: 3, + formSalt: "HIGH_ENTROPY_SALT", + expiration: AbsoluteTime.never(), + }; + assert.strictEqual(attributesAreComplete(decision), true); + const result = buildAmlDecisionRequest({ + account: "ACCOUNT", + fullPayto: undefined, + decision, + measures: measureSummary, + }); + assert.strictEqual( + (result.request?.attributes as Record<string, unknown> | undefined)?.[ + TalerFormAttributes.FORM_SALT + ], + "HIGH_ENTROPY_SALT", + ); + delete decision.attributes.formSalt; + assert.strictEqual(attributesAreComplete(decision), false); +}); + +test("decision builder rejects stale and incompatible measure references", () => { + const decision = completeDecision(); + decision.new_measures = ["skipOne", "skipTwo", "missing"]; + const result = buildAmlDecisionRequest({ + account: "ACCOUNT", + fullPayto: undefined, + decision, + measures: measureSummary, + }); + assert.ok(result.errors.includes("unknown-measure")); + assert.ok(result.errors.includes("multiple-skip-measures")); + assert.strictEqual(result.request, undefined); +}); + +test("custom properties cannot replace built-in properties", () => { + const decision = completeDecision(); + decision.custom_properties = { builtin: "replacement" }; + const result = buildAmlDecisionRequest({ + account: "ACCOUNT", + fullPayto: undefined, + decision, + measures: measureSummary, + }); + assert.ok(result.errors.includes("invalid-properties")); +}); + +test("decision draft targets cannot cross officers or accounts", () => { + const target = { officerId: "OFFICER", account: "ACCOUNT" }; + assert.strictEqual(decisionTargetsMatch(target, target), true); + assert.strictEqual( + decisionTargetsMatch(target, { ...target, account: "OTHER" }), + false, + ); + assert.strictEqual( + decisionTargetsMatch(target, { ...target, officerId: "OTHER" }), + false, + ); + assert.strictEqual( + decisionTargetsMatch( + { ...target, fullPayto: "payto://iban/one" }, + { ...target, fullPayto: "payto://iban/two" }, + ), + false, + ); +}); diff --git a/packages/taler-util/src/types-taler-exchange.test.ts b/packages/taler-util/src/types-taler-exchange.test.ts @@ -18,6 +18,7 @@ import { test } from "node:test"; import assert from "node:assert"; import { codecForAuditor, + codecForAmlSpaDialect, codecForEventCounter, codecForExchangeKeysResponse, codecForPurseCreateSuccessResponse, @@ -25,6 +26,12 @@ import { } from "./types-taler-exchange.js"; import { encodeCrock } from "./taler-crypto.js"; +test("AML SPA dialect rejects removed GLS configuration", () => { + assert.strictEqual(codecForAmlSpaDialect.decode("tops"), "tops"); + assert.strictEqual(codecForAmlSpaDialect.decode("testing"), "testing"); + assert.throws(() => codecForAmlSpaDialect.decode("gls")); +}); + /** * A /keys response captured from a live exchange (protocol 37:0:3), reduced * to one denomination group per cipher and one denomination each, with the @@ -34,178 +41,179 @@ import { encodeCrock } from "./taler-crypto.js"; * carries no age_mask although the interface declares one. */ const REAL_KEYS: any = { - "version": "37:0:3", - "base_url": "http://localhost:8081/", - "currency": "TESTKUDOS", - "currency_specification": { - "name": "Test-kudos (Taler Demonstrator)", - "currency": "TESTKUDOS", - "common_amounts": [ - "TESTKUDOS:5", - "TESTKUDOS:10", - "TESTKUDOS:25", - "TESTKUDOS:50" - ], - "num_fractional_input_digits": 2, - "num_fractional_normal_digits": 2, - "num_fractional_trailing_zero_digits": 2, - "alt_unit_names": { - "0": "\u30c6", - "3": "k\u30c6", - "-3": "m\u30c6" - } + version: "37:0:3", + base_url: "http://localhost:8081/", + currency: "TESTKUDOS", + currency_specification: { + name: "Test-kudos (Taler Demonstrator)", + currency: "TESTKUDOS", + common_amounts: [ + "TESTKUDOS:5", + "TESTKUDOS:10", + "TESTKUDOS:25", + "TESTKUDOS:50", + ], + num_fractional_input_digits: 2, + num_fractional_normal_digits: 2, + num_fractional_trailing_zero_digits: 2, + alt_unit_names: { + "0": "\u30c6", + "3": "k\u30c6", + "-3": "m\u30c6", }, - "hard_limits": [], - "zero_limits": [], - "stefan_abs": "TESTKUDOS:1", - "stefan_log": "TESTKUDOS:1", - "stefan_lin": 0, - "asset_type": "fiat", - "kyc_enabled": false, - "kyc_swap_tos_acceptance": false, - "disable_direct_deposit": false, - "rewards_allowed": false, - "master_public_key": "DK9ESDMR9VGK66PJ", - "reserve_closing_delay": { - "d_us": 2419200000000 + }, + hard_limits: [], + zero_limits: [], + stefan_abs: "TESTKUDOS:1", + stefan_log: "TESTKUDOS:1", + stefan_lin: 0, + asset_type: "fiat", + kyc_enabled: false, + kyc_swap_tos_acceptance: false, + disable_direct_deposit: false, + rewards_allowed: false, + master_public_key: "DK9ESDMR9VGK66PJ", + reserve_closing_delay: { + d_us: 2419200000000, + }, + default_p2p_push_expiration: { + d_us: 2419200000000, + }, + signkeys: [ + { + stamp_start: { + t_s: 1784568055, + }, + stamp_expire: { + t_s: 1791825655, + }, + stamp_end: { + t_s: 1854897655, + }, + master_sig: "5RBMXK3WQK2BX6CH", + key: "GV59TV4135ENJAES", }, - "default_p2p_push_expiration": { - "d_us": 2419200000000 + ], + recoup: [], + wads: [], + accounts: [ + { + payto_uri: + "payto://x-taler-bank/localhost/exchange?receiver-name=exchange", + priority: 0, + debit_restrictions: [], + credit_restrictions: [], + master_sig: "MGFAA1ZVYHPNK54G", }, - "signkeys": [ + ], + wire_fees: { + "x-taler-bank": [ { - "stamp_start": { - "t_s": 1784568055 - }, - "stamp_expire": { - "t_s": 1791825655 + wire_fee: "TESTKUDOS:0.01", + closing_fee: "TESTKUDOS:0.01", + start_date: { + t_s: 1767225600, }, - "stamp_end": { - "t_s": 1854897655 + end_date: { + t_s: 1798761600, }, - "master_sig": "5RBMXK3WQK2BX6CH", - "key": "GV59TV4135ENJAES" - } - ], - "recoup": [], - "wads": [], - "accounts": [ - { - "payto_uri": "payto://x-taler-bank/localhost/exchange?receiver-name=exchange", - "priority": 0, - "debit_restrictions": [], - "credit_restrictions": [], - "master_sig": "MGFAA1ZVYHPNK54G" - } + sig: "F4K7E168YX587XWQ", + }, ], - "wire_fees": { - "x-taler-bank": [ + }, + denominations: [ + { + cipher: "RSA", + denoms: [ { - "wire_fee": "TESTKUDOS:0.01", - "closing_fee": "TESTKUDOS:0.01", - "start_date": { - "t_s": 1767225600 + master_sig: "EBDT9N9RWR4X0SF1", + stamp_start: { + t_s: 1784568055, }, - "end_date": { - "t_s": 1798761600 + stamp_expire_withdraw: { + t_s: 1784764800, }, - "sig": "F4K7E168YX587XWQ" - } - ] - }, - "denominations": [ - { - "cipher": "RSA", - "denoms": [ - { - "master_sig": "EBDT9N9RWR4X0SF1", - "stamp_start": { - "t_s": 1784568055 - }, - "stamp_expire_withdraw": { - "t_s": 1784764800 - }, - "stamp_expire_deposit": { - "t_s": 1847836800 - }, - "stamp_expire_legal": { - "t_s": 1942444800 - }, - "rsa_pub": "020000XDV5WEHG8Q" - } - ], - "fee_withdraw": "TESTKUDOS:0.01", - "fee_deposit": "TESTKUDOS:0.01", - "fee_refresh": "TESTKUDOS:0.01", - "fee_refund": "TESTKUDOS:0.01", - "value": "TESTKUDOS:2" - }, - { - "cipher": "CS", - "denoms": [ - { - "master_sig": "WGMHHKDJ4WRAQM8Q", - "stamp_start": { - "t_s": 1815412833 - }, - "stamp_expire_withdraw": { - "t_s": 1816214400 - }, - "stamp_expire_deposit": { - "t_s": 1879286400 - }, - "stamp_expire_legal": { - "t_s": 1973894400 - }, - "cs_pub": "97ZMTV6M1SC4N00W" - } - ], - "fee_withdraw": "TESTKUDOS:0.01", - "fee_deposit": "TESTKUDOS:0.01", - "fee_refresh": "TESTKUDOS:0.01", - "fee_refund": "TESTKUDOS:0.01", - "value": "TESTKUDOS:0.1" - } - ], - "auditors": [], - "global_fees": [ - { - "start_date": { - "t_s": 1767225600 - }, - "end_date": { - "t_s": 1798761600 - }, - "history_fee": "TESTKUDOS:0.01", - "account_fee": "TESTKUDOS:0.01", - "purse_fee": "TESTKUDOS:0", - "history_expiration": { - "d_us": 31536000000000 + stamp_expire_deposit: { + t_s: 1847836800, + }, + stamp_expire_legal: { + t_s: 1942444800, + }, + rsa_pub: "020000XDV5WEHG8Q", }, - "purse_timeout": { - "d_us": 3600000000 + ], + fee_withdraw: "TESTKUDOS:0.01", + fee_deposit: "TESTKUDOS:0.01", + fee_refresh: "TESTKUDOS:0.01", + fee_refund: "TESTKUDOS:0.01", + value: "TESTKUDOS:2", + }, + { + cipher: "CS", + denoms: [ + { + master_sig: "WGMHHKDJ4WRAQM8Q", + stamp_start: { + t_s: 1815412833, + }, + stamp_expire_withdraw: { + t_s: 1816214400, + }, + stamp_expire_deposit: { + t_s: 1879286400, + }, + stamp_expire_legal: { + t_s: 1973894400, + }, + cs_pub: "97ZMTV6M1SC4N00W", }, - "purse_account_limit": 5, - "master_sig": "V9X36VW7GQD6DF5C" - } - ], - "list_issue_date": { - "t_s": 1784568055 + ], + fee_withdraw: "TESTKUDOS:0.01", + fee_deposit: "TESTKUDOS:0.01", + fee_refresh: "TESTKUDOS:0.01", + fee_refund: "TESTKUDOS:0.01", + value: "TESTKUDOS:0.1", }, - "wallet_balance_limit_without_kyc": [], - "tiny_amount": "TESTKUDOS:0.01", - "exchange_pub": "GV59TV4135ENJAES", - "exchange_sig": "QXYANBP82S1HGKR3" - }; + ], + auditors: [], + global_fees: [ + { + start_date: { + t_s: 1767225600, + }, + end_date: { + t_s: 1798761600, + }, + history_fee: "TESTKUDOS:0.01", + account_fee: "TESTKUDOS:0.01", + purse_fee: "TESTKUDOS:0", + history_expiration: { + d_us: 31536000000000, + }, + purse_timeout: { + d_us: 3600000000, + }, + purse_account_limit: 5, + master_sig: "V9X36VW7GQD6DF5C", + }, + ], + list_issue_date: { + t_s: 1784568055, + }, + wallet_balance_limit_without_kyc: [], + tiny_amount: "TESTKUDOS:0.01", + exchange_pub: "GV59TV4135ENJAES", + exchange_sig: "QXYANBP82S1HGKR3", +}; test("codecForExchangeKeysResponse accepts a real /keys document", (t) => { const keys = codecForExchangeKeysResponse().decode(REAL_KEYS); assert.strictEqual(keys.currency, "TESTKUDOS"); assert.strictEqual(keys.denominations.length, 2); - assert.deepStrictEqual( - keys.denominations.map((g) => g.cipher).sort(), - ["CS", "RSA"], - ); + assert.deepStrictEqual(keys.denominations.map((g) => g.cipher).sort(), [ + "CS", + "RSA", + ]); assert.ok(keys.master_public_key.length > 0); assert.ok(keys.signkeys.length > 0); assert.ok(keys.accounts.length > 0);