taler-typescript-core

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

commit bec18897cc7344c077be019b84d560d8716c4deb
parent c4ce5227b13fb241ed037f7d486aa91ef05c6a7b
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Mon, 27 Apr 2026 10:51:13 -0300

int test for #11356

Diffstat:
Mpackages/taler-harness/src/harness/environments.ts | 27+++++++++++++++++++++++++++
Mpackages/taler-harness/src/harness/harness.ts | 144+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/test-fee-regression.ts | 7+++++++
Apackages/taler-harness/src/integrationtests/test-paivana.ts | 164+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/test-revocation.ts | 6++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 2++
Mpackages/taler-util/src/index.ts | 2++
Mpackages/taler-util/src/talerconfig.ts | 7+++++++
Mpackages/taler-util/src/taleruri.ts | 90++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mpackages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts | 1+
10 files changed, 406 insertions(+), 44 deletions(-)

diff --git a/packages/taler-harness/src/harness/environments.ts b/packages/taler-harness/src/harness/environments.ts @@ -53,6 +53,7 @@ import { TalerMerchantInstanceHttpClient, TalerProtocolTimestamp, TalerWireGatewayHttpClient, + TemplateType, TransactionIdStr, TransactionMajorState, TransactionMinorState, @@ -87,6 +88,7 @@ import { MERCHANT_DEFAULT_LOGIN_SCOPE, MerchantService, MerchantServiceInterface, + PaivanaService, setupDb, setupSharedDb, useLibeufinBank, @@ -141,6 +143,7 @@ export interface SimpleTestEnvironmentNg3 { exchange: ExchangeService; exchangeBankAccount: HarnessExchangeBankAccount; merchant: MerchantService; + paivana: PaivanaService; merchantAdminAccessToken: AccessToken; walletClient: WalletClient; walletService: WalletService; @@ -155,6 +158,8 @@ export interface EnvOptions { */ ageMaskSpec?: string; + paivanaWebsite?: string; + mixedAgeRestriction?: boolean; skipWireFeeCreation?: boolean; @@ -561,6 +566,11 @@ export async function createSimpleTestkudosEnvironmentV3( ? await LibeufinBankService.create(t, bc) : await FakebankService.create(t, bc); + const paivana = await PaivanaService.create(t, { + destination: "https://grothoff.org/", + httpPort: 8088 + }) + const exchange = ExchangeService.create(t, { name: "testexchange-1", currency: "TESTKUDOS", @@ -703,6 +713,22 @@ export async function createSimpleTestkudosEnvironmentV3( }, ); + if (opts.paivanaWebsite) { + const instanceUrl = merchant.makeInstanceBaseUrl("admin") + const api = new TalerMerchantInstanceHttpClient(instanceUrl); + const t = await api.addTemplate(merchantAdminAccessToken, { + template_id: "paivana", + template_description: "test paivana", + template_contract: { + template_type: TemplateType.PAIVANA, + choices: [{ amount: "TESTKUDOS:1" }], + website_regex: opts.paivanaWebsite, + } + }) + paivana.setMerchant(instanceUrl, merchantAdminAccessToken) + await paivana.start() + } + console.log("setup done!"); return { @@ -714,6 +740,7 @@ export async function createSimpleTestkudosEnvironmentV3( walletService, bank, bankClient, + paivana, exchangeBankAccount, createBrowser: createBrowser(t), }; diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts @@ -580,6 +580,13 @@ export interface BankConfig { overrideTestDir?: string; } +export interface PaivanaConfig { + destination: string; + httpPort: number; + secret?: string; + overrideTestDir?: string; +} + export interface FakeBankConfig { currency: string; httpPort: number; @@ -1157,6 +1164,143 @@ export interface BankServiceHandle { changeConfig(f: (config: Configuration) => void): void; } +export interface PaivanaServiceHandle { + readonly paivanaApiBaseUrl: string; + readonly http: HttpRequestLibrary; + + setMerchant(url: string, authToken: AccessToken): void; + start(): Promise<void>; + pingUntilAvailable(): Promise<void>; + stop(): Promise<void>; + changeConfig(f: (config: Configuration) => void): void; +} + +/** + * Implementation of the bank service using the "taler-fakebank-run" tool. + */ +export class PaivanaService + implements PaivanaServiceHandle { + public proc: ProcessWrapper | undefined; + // public paivanaApiBaseUrl: string; + + + get paivanaApiBaseUrl() { + return this.baseUrl + } + + constructor( + private globalState: GlobalTestState, + private config: PaivanaConfig, + private configFile: string, + ) { } + + http = createPlatformHttpLib({ enableThrottling: false }); + + + /** + */ + static async create( + gc: GlobalTestState, + pc: PaivanaConfig, + ): Promise<PaivanaService> { + const config = new Configuration(ConfigSources["paivana"]); + const testDir = pc.overrideTestDir ?? gc.testDir; + setTalerPaths(config, testDir + "/talerhome"); + const hostname = "localhost"; + config.setString("paivana", "port", `${pc.httpPort}`); + config.setString("paivana", "serve", "tcp"); + config.setString("paivana", "DESTINATION_BASE_URL", pc.destination); + config.setString("paivana", "SECRET", Math.random().toString().substring(2, 6)); + config.setString( + "paivana", + "base_url", + `http://${hostname}:${pc.httpPort}/`, + ); + + const cfgFilename = testDir + "/paivana.conf"; + config.writeTo(cfgFilename, { excludeDefaults: true }); + + return new PaivanaService(gc, pc, cfgFilename); + } + + static fromExistingConfig( + gc: GlobalTestState, + opts: { overridePath?: string }, + ): PaivanaService { + const testDir = opts.overridePath ?? gc.testDir; + const cfgFilename = testDir + `/paivana.conf`; + const config = Configuration.load( + cfgFilename, + ConfigSources["paivana"], + ); + const pc: PaivanaConfig = { + httpPort: config.getNumber("paivana", "port").required(), + destination: config.getString("paivana", "DESTINATION_BASE_URL").required(), + secret: config.getString("paivana", "secret").required(), + }; + return new PaivanaService(gc, pc, cfgFilename); + } + + changeConfig(f: (config: Configuration) => void) { + const config = Configuration.load( + this.configFile, + ConfigSources["paivana"], + ); + f(config); + config.writeTo(this.configFile, { excludeDefaults: true }); + } + + get baseUrl(): string { + return `http://localhost:${this.config.httpPort}/`; + } + + + setMerchant(backendUrl: string, authToken: AccessToken): void { + const config = Configuration.load( + this.configFile, + ConfigSources["paivana"], + ); + config.setString("paivana", "MERCHANT_BACKEND_URL", backendUrl); + config.setString("paivana", "MERCHANT_ACCESS_TOKEN", authToken); + config.writeTo(this.configFile, { excludeDefaults: true }); + + } + get port() { + return this.config.httpPort; + } + + async start(): Promise<void> { + logger.info("starting paivana"); + if (this.proc) { + logger.info("paivana already running, not starting again"); + return; + } + this.proc = this.globalState.spawnService( + "paivana-httpd", + [ + "-c", + this.configFile, + ], + "paivana", + ); + await this.pingUntilAvailable(); + } + + async pingUntilAvailable(): Promise<void> { + const url = `http://localhost:${this.config.httpPort}/`; + await pingProc(this.proc, url, "paivana"); + } + + async stop(): Promise<void> { + const paivanaProc = this.proc; + if (paivanaProc) { + paivanaProc.proc.kill("SIGTERM"); + await paivanaProc.wait(); + this.proc = undefined; + } + } +} + export type BankService = BankServiceHandle; export const BankService = useLibeufinBank ? LibeufinBankService diff --git a/packages/taler-harness/src/integrationtests/test-fee-regression.ts b/packages/taler-harness/src/integrationtests/test-fee-regression.ts @@ -35,6 +35,7 @@ import { GlobalTestState, HarnessExchangeBankAccount, MerchantService, + PaivanaService, getTestHarnessPaytoForLabel, setupDb, } from "../harness/harness.js"; @@ -68,6 +69,11 @@ export async function createMyTestkudosEnvironment( database: db.connStr, }); + const paivana = await PaivanaService.create(t, { + destination: "https://grothoff.org/", + httpPort: 8088 + }) + let receiverName = "Exchange"; let exchangeBankUsername = "exchange"; let exchangeBankPassword = "mypw-password"; @@ -199,6 +205,7 @@ export async function createMyTestkudosEnvironment( walletService, bankClient, bank, + paivana, createBrowser: createBrowser(t), exchangeBankAccount, }; diff --git a/packages/taler-harness/src/integrationtests/test-paivana.ts b/packages/taler-harness/src/integrationtests/test-paivana.ts @@ -0,0 +1,164 @@ +/* + This file is part of GNU Taler + (C) 2020-2024 Taler Systems S.A. + + 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/> + */ + +/** + * Imports. + */ +import { + bytesToString, + ConfirmPayResultType, + Logger, + PreparePayResultType, + Result, + sha256, + stringToBytes, + TalerUriAction, + TalerUris, + WalletNotification +} from "@gnu-taler/taler-util"; +import { createPlatformHttpLib } from "@gnu-taler/taler-util/http"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { + createSimpleTestkudosEnvironmentV3, + withdrawViaBankV3 +} from "../harness/environments.js"; +import { GlobalTestState } from "../harness/harness.js"; + +const harnessHttpLib = createPlatformHttpLib({ + enableThrottling: false, +}); +export const logger = new Logger("test-paivana.ts"); + +export async function runPaivanaTest(t: GlobalTestState) { + // Set up test environment + + const { + walletClient, + bankClient, + exchange, + paivana, + } = await createSimpleTestkudosEnvironmentV3(t, undefined, { + paivanaWebsite: "/.*.html" // block all html pages + }); + + const notifs: WalletNotification[] = []; + + walletClient.addNotificationListener((x) => { + notifs.push(x); + }); + + const withdrawalRes = await withdrawViaBankV3(t, { + walletClient, + bankClient, + exchange, + amount: "TESTKUDOS:20", + }); + + await withdrawalRes.withdrawalFinishedCond; + + const website = `${paivana.baseUrl}index.html` + + const firstRequest = await harnessHttpLib.fetch(website); + const templateURI = firstRequest.headers.get("paivana"); + t.assertTrue(!!templateURI); + + const uri = Result.unpack(TalerUris.fromString(templateURI)) + t.assertTrue(uri.type === TalerUriAction.PayTemplate) + + const cur_time = Math.floor(Date.now() / 1000);// + 60*60*24; + const nonce = Array.from(new Uint8Array(32)) + .map(b => b.toString(16).padStart(2, '0')).join(''); + + const sha = bytesToString(sha256(stringToBytes(nonce + website + cur_time))) + const hash = base64Url(sha) + const paivanaId = (`${cur_time}-${hash}`) + + + logger.info("PAIVANA TEMPLATE ID::", paivanaId); + const PAIVANA_POLL_BASE = `${website}/.well-known/paivana/sessions/${encodeURIComponent(paivanaId)}`; + + { // Check if we magically have access (guess what, no) + const res = await harnessHttpLib.fetch(`${PAIVANA_POLL_BASE}`); + // const res = await harnessHttpLib.fetch(`${PAIVANA_POLL_BASE}?timeout_ms=1000`); + t.assertTrue(res.status === 402) + } + logger.info("access denied, we need to pay"); + + { // Pay the access to the site + const newTemplate = TalerUris.createTalerPayTemplate( + uri.merchantBaseUrl, + uri.templateId, + { + fulfillmenURL: encodeURIComponent(website), + sessionId: encodeURIComponent(paivanaId) + } + ); + const talerPayTemplateUri = TalerUris.toString(newTemplate); + logger.info("pay template", newTemplate, talerPayTemplateUri); + + const templateStatus = await walletClient.call(WalletApiOperation.PreparePayForTemplate, { talerPayTemplateUri }) + t.assertTrue(templateStatus.status === PreparePayResultType.PaymentPossible) + const talerPayUri = templateStatus.talerUri + logger.info("pay order", talerPayUri); + + const payStatus = await walletClient.call(WalletApiOperation.PreparePayForUri, { talerPayUri }); + t.assertTrue(payStatus.status === PreparePayResultType.PaymentPossible) + logger.info("transaction", payStatus.transactionId); + + const payment = await walletClient.call(WalletApiOperation.ConfirmPay, { + transactionId: payStatus.transactionId, + }); + t.assertTrue(payment.type === ConfirmPayResultType.Done) + logger.info("paid", payment.contractTerms.fulfillment_url); + } + + logger.info("checking paivana state again",); + let order_id; + { // Check if we have access + const res = await harnessHttpLib.fetch(`${PAIVANA_POLL_BASE}?timeout_ms=1000`); + t.assertTrue(res.status === 200) + try { order_id = (await res.json()).order_id ?? undefined; } catch (_) { } + } + t.assertTrue(order_id !== undefined) + logger.info(`---- ORDER ID ${order_id}`); + + const res = await harnessHttpLib.fetch(`${website}/.well-known/paivana`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ order_id, nonce, cur_time, website }), + }); + + console.log(res) + // TODO: the test is incomplete + + // TODO: verify cookie +} + +function base64Url(str: string) { + return Buffer + .from(str, "utf8") + .toString("base64") + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, ''); + +} + +function waitMs(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +runPaivanaTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/test-revocation.ts b/packages/taler-harness/src/integrationtests/test-revocation.ts @@ -36,6 +36,7 @@ import { GlobalTestState, HarnessExchangeBankAccount, MerchantService, + PaivanaService, WalletCli, WalletClient, delayMs, @@ -90,6 +91,10 @@ async function createTestEnvironment( httpPort: 8083, database: db.connStr, }); + const paivana = await PaivanaService.create(t, { + destination: "https://grothoff.org/", + httpPort: 8088 + }) let receiverName = "Exchange"; let exchangeBankUsername = "exchange"; @@ -181,6 +186,7 @@ async function createTestEnvironment( commonDb: db, exchange, merchant, + paivana, walletClient, merchantAdminAccessToken: adminAccessToken, walletService, diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -224,6 +224,7 @@ import { runWithdrawalHugeTest } from "./test-withdrawal-huge.js"; import { runWithdrawalIdempotentTest } from "./test-withdrawal-idempotent.js"; import { runWithdrawalManualTest } from "./test-withdrawal-manual.js"; import { runWithdrawalPrepareTest } from "./test-withdrawal-prepare.js"; +import { runPaivanaTest } from "./test-paivana.js"; /** * Test runner. @@ -432,6 +433,7 @@ const allTests: TestMainFunction[] = [ runWireMetadataTest, runMerchantPaytoReuseTest, runPreparedTransferTest, + runPaivanaTest ]; export interface TestRunSpec { diff --git a/packages/taler-util/src/index.ts b/packages/taler-util/src/index.ts @@ -35,6 +35,8 @@ export * from "./i18n.js"; export * from "./iban.js"; export * from "./invariants.js"; export * from "./kdf.js"; +export {sha256} from "./sha256.js"; + export * from "./libtool-version.js"; export * from "./logging.js"; export * from "./longpool-queue.js"; diff --git a/packages/taler-util/src/talerconfig.ts b/packages/taler-util/src/talerconfig.ts @@ -133,6 +133,13 @@ export const ConfigSources = { baseConfigVarname: "DONAU_BASE_CONFIG", prefixVarname: "DONAU_PREFIX", } satisfies ConfigSource, + ["paivana"]: { + projectName: "paivana", + componentName: "paivana", + installPathBinary: "paivana-config", + baseConfigVarname: "PAIVANA_BASE_CONFIG", + prefixVarname: "PAIVANA_PREFIX", + } satisfies ConfigSource, } satisfies ConfigSourceDef; const defaultConfigSource: ConfigSource = ConfigSources["taler-exchange"]; diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts @@ -296,10 +296,14 @@ export namespace TalerUris { if (p.sourceBaseUrl) result.push(["sourceBaseUrl", p.sourceBaseUrl]); return result; } + case TalerUriAction.PayTemplate: { + if (p.fulfillmentUrl) result.push(["fulfillment_url", p.fulfillmentUrl]); + if (p.sessionId) result.push(["session_id", p.sessionId]); + return result; + } case TalerUriAction.Refund: case TalerUriAction.PayPush: case TalerUriAction.PayPull: - case TalerUriAction.PayTemplate: case TalerUriAction.Restore: case TalerUriAction.DevExperiment: case TalerUriAction.AddExchange: { @@ -347,9 +351,7 @@ export namespace TalerUris { */ switch (p.type) { case TalerUriAction.Withdraw: - return `/${asHost(p.bankIntegrationApiBaseUrl)}${ - p.withdrawalOperationId - }`; + return `/${asHost(p.bankIntegrationApiBaseUrl)}${p.withdrawalOperationId}`; case TalerUriAction.Pay: return `/${asHost(p.merchantBaseUrl)}${p.orderId}/${p.sessionId}`; case TalerUriAction.Refund: @@ -406,47 +408,47 @@ export namespace TalerUris { export type InvalidTargetPathDetail = | { - uriType: TalerUriAction.Pay; - } + uriType: TalerUriAction.Pay; + } | { - uriType: TalerUriAction.Withdraw; - } + uriType: TalerUriAction.Withdraw; + } | { - uriType: TalerUriAction.Refund; - pos: 0; - } + uriType: TalerUriAction.Refund; + pos: 0; + } | { - uriType: TalerUriAction.Refund; - pos: 1; - } + uriType: TalerUriAction.Refund; + pos: 1; + } | { - uriType: TalerUriAction.PayPull; - pos: 0; - } + uriType: TalerUriAction.PayPull; + pos: 0; + } | { - uriType: TalerUriAction.PayPush; - pos: 0; - } + uriType: TalerUriAction.PayPush; + pos: 0; + } | { - uriType: TalerUriAction.PayTemplate; - pos: 0; - } + uriType: TalerUriAction.PayTemplate; + pos: 0; + } | { - uriType: TalerUriAction.WithdrawExchange; - pos: 0; - } + uriType: TalerUriAction.WithdrawExchange; + pos: 0; + } | { - uriType: TalerUriAction.WithdrawExchange; - pos: 1; - } + uriType: TalerUriAction.WithdrawExchange; + pos: 1; + } | { - uriType: TalerUriAction.AddExchange; - pos: 0; - } + uriType: TalerUriAction.AddExchange; + pos: 0; + } | { - uriType: TalerUriAction.AddContact; - pos: 0; - }; + uriType: TalerUriAction.AddContact; + pos: 0; + }; export function fromString( s: string, @@ -457,17 +459,17 @@ export namespace TalerUris { | ResultError<TalerUriParseError.UNSUPPORTED, { uriType: string }> | ResultError<TalerUriParseError.INCOMPLETE, { uriType: string }> | ResultError< - TalerUriParseError.COMPONENTS_LENGTH, - { uriType: TalerUriAction } - > + TalerUriParseError.COMPONENTS_LENGTH, + { uriType: TalerUriAction } + > | ResultError< - TalerUriParseError.INVALID_TARGET_PATH, - InvalidTargetPathDetail - > + TalerUriParseError.INVALID_TARGET_PATH, + InvalidTargetPathDetail + > | ResultError< - TalerUriParseError.INVALID_PARAMETER, - { uriType: TalerUriAction; name: string } - > { + TalerUriParseError.INVALID_PARAMETER, + { uriType: TalerUriAction; name: string } + > { // check prefix let isHttp = false; const prefixCheck = opts.ignoreUppercase ? s.toLowerCase() : s; diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts @@ -113,6 +113,7 @@ export function useComponentState({ def?.amount !== undefined ? Amounts.parseOrThrow(def.amount) : undefined; const defaultSummary = def?.summary; + // we need to get the currency from somewhere const zero = fixedAmount ? Amounts.zeroOfAmount(fixedAmount) : cfg.currency !== undefined