commit acb99a89afb91ba4e88f78bbf7533d63a9cb2dd2
parent ea4d0e3eb96997c9e036c4cc3d7e7edec3fbec39
Author: Florian Dold <dold@taler.net>
Date: Mon, 20 Jul 2026 20:43:16 +0200
util: test payto normalization and IBAN validation
Diffstat:
2 files changed, 68 insertions(+), 0 deletions(-)
diff --git a/packages/taler-util/src/iban.test.ts b/packages/taler-util/src/iban.test.ts
@@ -59,3 +59,12 @@ test("validateIban accepts lowercase and multi-space IBANs", () => {
assert.equal(validateIban(iban.toLowerCase()).type, "valid");
assert.equal(validateIban("NL71 RABO 9996 6667 78").type, "valid");
});
+
+test("IBAN check digits must be numeric", (t) => {
+ // ISO 13616 fixes positions 3-4 as two numeric check digits. appendDigit
+ // expands A-Z into two digits, so alphabetic check digits could otherwise
+ // still satisfy the mod-97-10 test.
+ const r = parseIban("DEAA000000000000000008");
+ assert.strictEqual(r.tag, "error");
+});
+
diff --git a/packages/taler-util/src/payto.test.ts b/packages/taler-util/src/payto.test.ts
@@ -239,3 +239,62 @@ test("taler-reserve payto with an unparseable reservePub does not throw when ign
assert.strictEqual(lenient.value.reservePub.length, 32);
assert.ok(lenient.value.reservePub.every((b) => b === 0));
});
+
+test("payto parsing rejects Object.prototype names as target type", (t) => {
+ // supportedTargets is a plain object literal, so a lookup for an inherited
+ // key used to be truthy, fall through the switch and hit assertUnreachable.
+ for (const t of ["constructor", "toString", "valueOf", "__proto__"]) {
+ const r = Paytos.fromString(`payto://${t}/foo`);
+ assert.strictEqual(r.tag, "error", `payto://${t}/foo should be an error`);
+ }
+});
+
+test("payto void normalizes without duplicating the target type", (t) => {
+ const r = Paytos.fromString("payto://void/12345");
+ assert.strictEqual(r.tag, "ok");
+ assert.strictEqual(
+ Paytos.toNormalizedString(Result.unpack(r)),
+ "payto://void/12345",
+ );
+});
+
+test("payto keeps the target type for unsupported schemes", (t) => {
+ const r = Paytos.fromString("payto://unknown-thing/foo", {
+ allowUnsupported: true,
+ });
+ assert.strictEqual(r.tag, "ok");
+ // Serializing used to interpolate an undefined targetType, collapsing every
+ // unsupported target type onto the literal string "undefined".
+ assert.strictEqual(
+ Paytos.toFullString(Result.unpack(r)),
+ "payto://unknown-thing/foo",
+ );
+});
+
+test("payto iban normalization is canonical", (t) => {
+ // Separators and case must not survive into the normalized form, otherwise
+ // the same account yields two different H_PAYTO values.
+ const dashed = Paytos.fromString("payto://iban/de75-5121-0800-1245-1261-99");
+ const plain = Paytos.fromString("payto://iban/DE75512108001245126199");
+ assert.strictEqual(dashed.tag, "ok");
+ assert.strictEqual(plain.tag, "ok");
+ assert.strictEqual(
+ Paytos.toNormalizedString(Result.unpack(dashed)),
+ Paytos.toNormalizedString(Result.unpack(plain)),
+ );
+});
+
+test("payto query is not truncated at a second question mark", (t) => {
+ // RFC 3986 section 3.4 permits "?" inside the query component.
+ const r = Paytos.fromString(
+ "payto://iban/DE75512108001245126199?receiver-name=Ann&msg=a?b",
+ );
+ assert.strictEqual(r.tag, "ok");
+ assert.strictEqual(Result.unpack(r).params?.["msg"], "a?b");
+});
+
+test("addPaytoQueryParams rejects a non-payto input", (t) => {
+ assert.throws(() =>
+ addPaytoQueryParams("https://evil.com/x", { message: "hi" }),
+ );
+});