commit 1bdc774241a0f1a55ca9151a5c1c208add21fbba
parent 8de0b35050e69dd9e312499c4e7dc29def231ad9
Author: Florian Dold <florian@dold.me>
Date: Sat, 18 Jul 2026 16:10:27 +0200
new tests for URI and IBAN parsing, merge test file according to our naming convention
Diffstat:
5 files changed, 212 insertions(+), 168 deletions(-)
diff --git a/packages/taler-util/src/iban.test.ts b/packages/taler-util/src/iban.test.ts
@@ -14,9 +14,14 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { test } from "node:test";
import assert from "node:assert";
-import { generateIban, validateIban } from "./iban.js";
+import { test } from "node:test";
+import {
+ constructIban,
+ generateIban,
+ parseIban,
+ validateIban,
+} from "./iban.js";
test("iban validation", (t) => {
assert.ok(validateIban("foo").type === "invalid");
@@ -28,3 +33,29 @@ test("iban generation", (t) => {
let iban1 = generateIban("DE", 10);
assert.ok(validateIban(iban1).type === "valid");
});
+
+// BUG-024: parseIban applied its 4..34 length check to the raw input, before
+// stripping grouping separators, so a valid IBAN entered with spaces exceeded
+// 34 characters and was wrongly rejected as TOO_LONG.
+test("BUG-024 parseIban accepts a valid IBAN with grouping spaces", () => {
+ // A 31-character IBAN, valid by construction (parseIban does not enforce a
+ // per-country length, so we build one long enough to matter once grouped).
+ const iban = constructIban("DE", "123456789012345678901234567");
+ assert.equal(iban.length, 31);
+ assert.equal(parseIban(iban).tag, "ok"); // baseline: construct/parse round-trips
+
+ const grouped = iban.replace(/(.{4})/g, "$1 ").trim();
+ assert.ok(grouped.length > 34, "grouped form must exceed the 34-char limit");
+ assert.equal(parseIban(grouped).tag, "ok");
+});
+
+// BUG-028: the deprecated validateIban removed only the first space
+// (String.replace with a string pattern) and read the country/check-digit
+// characters from the original (non-uppercased) input, so lowercase or
+// multi-space IBANs that parseIban accepts were rejected.
+test("validateIban accepts lowercase and multi-space IBANs", () => {
+ const iban = "NL71RABO9996666778";
+ assert.equal(validateIban(iban).type, "valid"); // baseline
+ assert.equal(validateIban(iban.toLowerCase()).type, "valid");
+ assert.equal(validateIban("NL71 RABO 9996 6667 78").type, "valid");
+});
diff --git a/packages/taler-util/src/payto.test.ts b/packages/taler-util/src/payto.test.ts
@@ -117,3 +117,158 @@ test("basic cyclos payto string", (t) => {
assert.strictEqual(result.host, "communities.cyclos.org");
assert.strictEqual(result.account, "31000163100000000");
});
+
+import { HostPortPath, PaytoType, Paytos } from "./payto.js";
+import { Result } from "./result.js";
+
+test("basic payto parsing", (t) => {
+ const r1 = Paytos.fromString("https://example.com/");
+ assert.strictEqual(r1.tag, "error");
+
+ const r2 = Paytos.fromString("payto:blabla");
+ assert.strictEqual(r2.tag, "error");
+
+ // this doesn't work because x-taler-bank requires host and account
+ const r3 = Paytos.fromString("payto:://x-taler-bank/12");
+ assert.strictEqual(r3.tag, "error");
+
+ const r4 = Paytos.fromString("payto://x-taler-bank/host/account");
+ if (r4.tag === "error") {
+ assert.fail();
+ throw Error();
+ }
+ if (r4.value.targetType !== PaytoType.TalerBank) {
+ assert.fail(r4.value.targetType);
+ throw Error();
+ }
+ assert.strictEqual(r4.value.normalizedPath, "host/account");
+});
+
+test("basic x-taler-bank payto string", (t) => {
+ const result = Result.unpack(
+ Paytos.fromString("payto://x-taler-bank/bank.demo.taler.net/accountName"),
+ );
+
+ if (result.targetType !== PaytoType.TalerBank) {
+ assert.fail();
+ throw Error();
+ }
+ assert.strictEqual(result.normalizedPath, "bank.demo.taler.net/accountName");
+ assert.strictEqual(result.host, "bank.demo.taler.net");
+ assert.strictEqual(
+ result.url,
+ "https://bank.demo.taler.net/" as HostPortPath,
+ );
+ assert.strictEqual(result.account, "accountName");
+});
+
+test("parsing payto and stringify again on normalized strings are unchanged", (t) => {
+ const payto1 = "payto://iban/DE1231231231?reciever-name=John%20Doe";
+ assert.strictEqual(
+ Paytos.toFullString(Result.unpack(Paytos.fromString(payto1))),
+ payto1 as Paytos.FullPaytoString,
+ );
+
+ const normalized = "payto://iban/DE1231231231";
+ assert.strictEqual(
+ Paytos.toNormalizedString(Result.unpack(Paytos.fromString(payto1))),
+ normalized as Paytos.NormalizedPaytoString,
+ );
+});
+test("parsing payto and stringify again converts to the normal form", (t) => {
+ const fullPayto_not_normalized =
+ "payto://iban/de1231231231?reciever-name=John%20Doe";
+ // after normalization the country code is uppercased
+ const fullPayto_normalized =
+ "payto://iban/DE1231231231?reciever-name=John%20Doe" as Paytos.FullPaytoString;
+ assert.strictEqual(
+ Paytos.toFullString(
+ Result.unpack(Paytos.fromString(fullPayto_not_normalized)),
+ ),
+ fullPayto_normalized,
+ );
+
+ const normalized_lowercase = "payto://iban/DE1231231231";
+ const normalized_uppercase =
+ "payto://iban/DE1231231231" as Paytos.NormalizedPaytoString;
+ assert.strictEqual(
+ Paytos.toNormalizedString(
+ Result.unpack(Paytos.fromString(normalized_lowercase)),
+ ),
+ normalized_uppercase,
+ );
+});
+test("parsing payto with % carh", (t) => {
+ const payto1 =
+ "payto://iban/DE7763544441436?receiver-name=Test%20123%2B-%24%25%5E%3Cem%3Ehi%3C%2Fem%3E" as Paytos.FullPaytoString;
+
+ assert.strictEqual(
+ Paytos.toFullString(Result.unpack(Paytos.fromString(payto1))),
+ payto1,
+ );
+});
+
+test("adding payto query params", (t) => {
+ const payto1 =
+ "payto://iban/DE1231231231?receiver-name=John%20Doe" as Paytos.FullPaytoString;
+ const p = Result.unpack(Paytos.fromString(payto1));
+
+ p.params["foo"] = "42";
+
+ assert.deepStrictEqual(
+ Paytos.toFullString(p),
+ "payto://iban/DE1231231231?receiver-name=John%20Doe&foo=42",
+ );
+});
+
+test("basic cyclos payto string", (t) => {
+ {
+ const result = Result.unpack(
+ Paytos.fromString(
+ "payto://cyclos/demo.cyclos.org/31000163100000000?receiver-name=John%20Doe",
+ ),
+ );
+
+ if (result.targetType !== PaytoType.Cyclos) {
+ assert.fail();
+ throw Error();
+ }
+ assert.strictEqual(
+ result.normalizedPath,
+ "demo.cyclos.org/31000163100000000",
+ );
+ assert.strictEqual(result.url, "https://demo.cyclos.org/" as HostPortPath);
+ assert.strictEqual(result.account, "31000163100000000");
+ assert.strictEqual(result.params["receiver-name"], "John Doe");
+ }
+
+ {
+ const result = Result.unpack(
+ Paytos.fromString(
+ "payto://cyclos/communities.cyclos.org/utrecht/31000163100000000?receiver-name=John%20Doe",
+ ),
+ );
+
+ if (result.targetType !== PaytoType.Cyclos) {
+ assert.fail();
+ throw Error();
+ }
+ assert.strictEqual(
+ result.normalizedPath,
+ "communities.cyclos.org/utrecht/31000163100000000",
+ );
+ assert.strictEqual(
+ result.url,
+ "https://communities.cyclos.org/utrecht/" as HostPortPath,
+ );
+ assert.strictEqual(result.account, "31000163100000000");
+ assert.strictEqual(result.params["receiver-name"], "John Doe");
+ }
+});
+
+test("Paytos.parseHostPortPath keeps the path", () => {
+ assert.deepStrictEqual(
+ Paytos.parseHostPortPath("example.com/foo/bar"),
+ "https://example.com/foo/bar/",
+ );
+});
diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts
@@ -285,6 +285,7 @@ export namespace Paytos {
return undefined;
}
}
+
function withoutScheme(h: HostPortPath): HostPortPath {
return (
h.startsWith("http://")
@@ -294,6 +295,7 @@ export namespace Paytos {
: h
) as HostPortPath;
}
+
/**
* Same as `parseHostPortPath2` but only takes one string.
* This should be the definitive signature.
@@ -308,6 +310,7 @@ export namespace Paytos {
const [host, path] = hostnameAndPath.split("/", 1);
return parseHostPortPath2(host, path ?? "");
}
+
/**
* FIXME: add ethereum address validator
* @param str
@@ -331,6 +334,7 @@ export namespace Paytos {
}
return account;
}
+
//////////////////
// function to create objs
//////////////////
@@ -348,6 +352,7 @@ export namespace Paytos {
displayName: path,
};
}
+
export function createIban(
iban: IbanString,
bic: string | undefined,
@@ -364,6 +369,7 @@ export namespace Paytos {
displayName: iban,
};
}
+
export function createBitcoin(
address: BtAddrString,
reservePub: Uint8Array | undefined,
@@ -385,6 +391,7 @@ export namespace Paytos {
displayName: address,
};
}
+
export function createEthereum(
address: EthAddrString,
params: Record<string, string> = {},
@@ -398,6 +405,7 @@ export namespace Paytos {
displayName: address,
};
}
+
export function createTalerReserve(
exchange: HostPortPath,
reservePub: Uint8Array,
@@ -415,6 +423,7 @@ export namespace Paytos {
displayName: `${path}@${pub}`,
};
}
+
export function createCyclos(
url: HostPortPath,
account: string,
@@ -431,6 +440,7 @@ export namespace Paytos {
displayName: `${account}@${path}`,
};
}
+
export function createTalerReserveHttp(
exchange: HostPortPath,
reservePub: Uint8Array,
@@ -448,6 +458,7 @@ export namespace Paytos {
displayName: `${path}@${pub}`,
};
}
+
export function createTalerBank(
url: HostPortPath,
account: string,
@@ -809,6 +820,7 @@ export function codecForPaytoHash(): Codec<PaytoHash> {
},
};
}
+
export function codecFullForPaytoString(): Codec<Paytos.FullPaytoString> {
return {
decode(x: any, c?: Context): Paytos.FullPaytoString {
diff --git a/packages/taler-util/src/paytos.test.ts b/packages/taler-util/src/paytos.test.ts
@@ -1,166 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2019 GNUnet e.V.
-
- 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 { test } from "node:test";
-import assert from "node:assert";
-
-import { HostPortPath, PaytoType, Paytos } from "./payto.js";
-import { Result } from "./result.js";
-
-test("basic payto parsing", (t) => {
- const r1 = Paytos.fromString("https://example.com/");
- assert.strictEqual(r1.tag, "error");
-
- const r2 = Paytos.fromString("payto:blabla");
- assert.strictEqual(r2.tag, "error");
-
- // this doesn't work because x-taler-bank requires host and account
- const r3 = Paytos.fromString("payto:://x-taler-bank/12");
- assert.strictEqual(r3.tag, "error");
-
- const r4 = Paytos.fromString("payto://x-taler-bank/host/account");
- if (r4.tag === "error") {
- assert.fail();
- throw Error();
- }
- if (r4.value.targetType !== PaytoType.TalerBank) {
- assert.fail(r4.value.targetType);
- throw Error();
- }
- assert.strictEqual(r4.value.normalizedPath, "host/account");
-});
-
-test("basic x-taler-bank payto string", (t) => {
- const result = Result.unpack(
- Paytos.fromString("payto://x-taler-bank/bank.demo.taler.net/accountName"),
- );
-
- if (result.targetType !== PaytoType.TalerBank) {
- assert.fail();
- throw Error();
- }
- assert.strictEqual(result.normalizedPath, "bank.demo.taler.net/accountName");
- assert.strictEqual(result.host, "bank.demo.taler.net");
- assert.strictEqual(
- result.url,
- "https://bank.demo.taler.net/" as HostPortPath,
- );
- assert.strictEqual(result.account, "accountName");
-});
-
-test("parsing payto and stringify again on normalized strings are unchanged", (t) => {
- const payto1 = "payto://iban/DE1231231231?reciever-name=John%20Doe";
- assert.strictEqual(
- Paytos.toFullString(Result.unpack(Paytos.fromString(payto1))),
- payto1 as Paytos.FullPaytoString,
- );
-
- const normalized = "payto://iban/DE1231231231";
- assert.strictEqual(
- Paytos.toNormalizedString(Result.unpack(Paytos.fromString(payto1))),
- normalized as Paytos.NormalizedPaytoString,
- );
-});
-test("parsing payto and stringify again converts to the normal form", (t) => {
- const fullPayto_not_normalized =
- "payto://iban/de1231231231?reciever-name=John%20Doe";
- // after normalization the country code is uppercased
- const fullPayto_normalized =
- "payto://iban/DE1231231231?reciever-name=John%20Doe" as Paytos.FullPaytoString;
- assert.strictEqual(
- Paytos.toFullString(
- Result.unpack(Paytos.fromString(fullPayto_not_normalized)),
- ),
- fullPayto_normalized,
- );
-
- const normalized_lowercase = "payto://iban/DE1231231231";
- const normalized_uppercase =
- "payto://iban/DE1231231231" as Paytos.NormalizedPaytoString;
- assert.strictEqual(
- Paytos.toNormalizedString(
- Result.unpack(Paytos.fromString(normalized_lowercase)),
- ),
- normalized_uppercase,
- );
-});
-test("parsing payto with % carh", (t) => {
- const payto1 =
- "payto://iban/DE7763544441436?receiver-name=Test%20123%2B-%24%25%5E%3Cem%3Ehi%3C%2Fem%3E" as Paytos.FullPaytoString;
-
- assert.strictEqual(
- Paytos.toFullString(Result.unpack(Paytos.fromString(payto1))),
- payto1,
- );
-});
-
-test("adding payto query params", (t) => {
- const payto1 =
- "payto://iban/DE1231231231?receiver-name=John%20Doe" as Paytos.FullPaytoString;
- const p = Result.unpack(Paytos.fromString(payto1));
-
- p.params["foo"] = "42";
-
- assert.deepStrictEqual(
- Paytos.toFullString(p),
- "payto://iban/DE1231231231?receiver-name=John%20Doe&foo=42",
- );
-});
-
-test("basic cyclos payto string", (t) => {
- {
- const result = Result.unpack(
- Paytos.fromString(
- "payto://cyclos/demo.cyclos.org/31000163100000000?receiver-name=John%20Doe",
- ),
- );
-
- if (result.targetType !== PaytoType.Cyclos) {
- assert.fail();
- throw Error();
- }
- assert.strictEqual(
- result.normalizedPath,
- "demo.cyclos.org/31000163100000000",
- );
- assert.strictEqual(result.url, "https://demo.cyclos.org/" as HostPortPath);
- assert.strictEqual(result.account, "31000163100000000");
- assert.strictEqual(result.params["receiver-name"], "John Doe");
- }
-
- {
- const result = Result.unpack(
- Paytos.fromString(
- "payto://cyclos/communities.cyclos.org/utrecht/31000163100000000?receiver-name=John%20Doe",
- ),
- );
-
- if (result.targetType !== PaytoType.Cyclos) {
- assert.fail();
- throw Error();
- }
- assert.strictEqual(
- result.normalizedPath,
- "communities.cyclos.org/utrecht/31000163100000000",
- );
- assert.strictEqual(
- result.url,
- "https://communities.cyclos.org/utrecht/" as HostPortPath,
- );
- assert.strictEqual(result.account, "31000163100000000");
- assert.strictEqual(result.params["receiver-name"], "John Doe");
- }
-});
diff --git a/packages/taler-util/src/taleruri.test.ts b/packages/taler-util/src/taleruri.test.ts
@@ -748,3 +748,15 @@ import { AmountString } from "./types-taler-common.js";
assert.ok(Result.isError(r2));
});
}
+
+test("parseAddContact keeps the mailbox sub-path", () => {
+ const r = Result.orUndefined(
+ TalerUris.parseRestricted(
+ "taler://add-contact/email/bob@example.com/mailbox.example.com/mb/SOMEHASHOFPUBKEY",
+ TalerUriAction.AddContact,
+ ),
+ );
+ assert.ok(r, "add-contact URI should parse");
+ assert.deepStrictEqual(r.mailboxIdentity, "SOMEHASHOFPUBKEY");
+ assert.deepStrictEqual(r.mailboxBaseUri, "https://mailbox.example.com/mb/");
+});