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:
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));
+});