taler-typescript-core

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

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:
Mpackages/taler-util/src/iban.test.ts | 35+++++++++++++++++++++++++++++++++--
Mpackages/taler-util/src/payto.test.ts | 155+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-util/src/payto.ts | 12++++++++++++
Dpackages/taler-util/src/paytos.test.ts | 166-------------------------------------------------------------------------------
Mpackages/taler-util/src/taleruri.test.ts | 12++++++++++++
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/"); +});