taler-typescript-core

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

commit d0775fae165d66ed22f21a688b00a4d62508022c
parent b0afe06ff68bf2f9a16ddbdd70bd96a97bd0d0cd
Author: Florian Dold <dold@taler.net>
Date:   Tue, 21 Jul 2026 11:04:47 +0200

util: test currency checking in sub and normalization in divide

Diffstat:
Mpackages/taler-util/src/amounts.test.ts | 77++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 72 insertions(+), 5 deletions(-)

diff --git a/packages/taler-util/src/amounts.test.ts b/packages/taler-util/src/amounts.test.ts @@ -20,6 +20,7 @@ import assert from "node:assert"; import { Amounts, AmountJson, + AmountLike, amountMaxValue, amountFractionalBase, codecForAmountJson, @@ -316,10 +317,26 @@ test("currency validation runs on the raw string, before case folding", (t) => { test("parse and parseWithError agree", (t) => { const corpus = [ - "EUR:1", "eur:1", "EUR:0.99999999", "EUR:1.999999999", "E:1", "EURO:1", - "ABCDEFGHIJK:1", "ABCDEFGHIJKL:1", ":1", "EUR:", "EUR:x", "EUR:1.", - "EU R:1", "EU-R:1", "ſ:5", "fi:5", "EüR:1", "EUR:-1", - "EUR:9007199254740993", "EUR:4503599627370496", + "EUR:1", + "eur:1", + "EUR:0.99999999", + "EUR:1.999999999", + "E:1", + "EURO:1", + "ABCDEFGHIJK:1", + "ABCDEFGHIJKL:1", + ":1", + "EUR:", + "EUR:x", + "EUR:1.", + "EU R:1", + "EU-R:1", + "ſ:5", + "fi:5", + "EüR:1", + "EUR:-1", + "EUR:9007199254740993", + "EUR:4503599627370496", ]; for (const s of corpus) { const viaParse = Amounts.parse(s); @@ -338,7 +355,13 @@ test("parse and parseWithError agree", (t) => { }); test("amount objects are rejected when the currency is not a currency", (t) => { - for (const currency of ["", "not a currency €", "EUR1", "ABCDEFGHIJKL", "ſ"]) { + for (const currency of [ + "", + "not a currency €", + "EUR1", + "ABCDEFGHIJKL", + "ſ", + ]) { assert.throws( () => Amounts.parseOrThrow({ currency, value: 1, fraction: 0 }), `parseOrThrow should reject currency ${JSON.stringify(currency)}`, @@ -416,3 +439,47 @@ test("arithmetic still accepts denormalized amounts", (t) => { "EUR:2" as AmountString, ); }); + +test("subtraction validates all currencies before subtracting", (t) => { + // add() checks every operand's currency up front. sub() returning early on + // underflow made the check depend on argument order, so the same multiset + // of operands either threw or produced a plausible saturated result. + assert.throws(() => + Amounts.sub( + "EUR:1" as AmountString, + "EUR:2" as AmountString, + "USD:1" as AmountString, + ), + ); + assert.throws(() => + Amounts.sub( + "EUR:1" as AmountString, + "USD:1" as AmountString, + "EUR:2" as AmountString, + ), + ); +}); + +test("division normalizes and validates its dividend", (t) => { + // Widened here so the file compiles before divide accepts an AmountLike. + const divide = Amounts.divide as (a: AmountLike, n: number) => AmountJson; + // Every other arithmetic entry point goes through jsonifyAmount. + assert.deepStrictEqual( + divide("EUR:10" as AmountString, 2), + Amounts.parseOrThrow("EUR:5"), + ); + // A denormalized dividend must not yield fraction === amountFractionalBase, + // which the library's own parser then rejects. + const denormalized = { value: 0, fraction: 200000000, currency: "EUR" }; + assert.deepStrictEqual(divide(denormalized, 2), { + currency: "EUR", + value: 1, + fraction: 0, + }); + assert.deepStrictEqual(divide(denormalized, 1), { + currency: "EUR", + value: 2, + fraction: 0, + }); + assert.throws(() => divide({ value: -5, fraction: 0, currency: "EUR" }, 2)); +});