summaryrefslogtreecommitdiff
path: root/packages/taler-harness/src/harness/helpers.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-harness/src/harness/helpers.ts')
-rw-r--r--packages/taler-harness/src/harness/helpers.ts711
1 files changed, 711 insertions, 0 deletions
diff --git a/packages/taler-harness/src/harness/helpers.ts b/packages/taler-harness/src/harness/helpers.ts
new file mode 100644
index 000000000..ea9047d0b
--- /dev/null
+++ b/packages/taler-harness/src/harness/helpers.ts
@@ -0,0 +1,711 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 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/>
+ */
+
+/**
+ * Helpers to create typical test environments.
+ *
+ * @author Florian Dold <dold@taler.net>
+ */
+
+/**
+ * Imports
+ */
+import {
+ AmountString,
+ ConfirmPayResultType,
+ Duration,
+ Logger,
+ MerchantApiClient,
+ MerchantContractTerms,
+ NotificationType,
+ PartialWalletRunConfig,
+ PreparePayResultType,
+ TalerCorebankApiClient,
+ TransactionMajorState,
+ WalletNotification,
+} from "@gnu-taler/taler-util";
+import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "./denomStructures.js";
+import {
+ FaultInjectedExchangeService,
+ FaultInjectedMerchantService,
+} from "./faultInjection.js";
+import {
+ BankService,
+ DbInfo,
+ ExchangeService,
+ ExchangeServiceInterface,
+ FakebankService,
+ GlobalTestState,
+ HarnessExchangeBankAccount,
+ MerchantService,
+ MerchantServiceInterface,
+ WalletCli,
+ WalletClient,
+ WalletService,
+ WithAuthorization,
+ generateRandomPayto,
+ setupDb,
+ setupSharedDb,
+} from "./harness.js";
+
+import * as fs from "fs";
+
+const logger = new Logger("helpers.ts");
+
+/**
+ * @deprecated
+ */
+export interface SimpleTestEnvironment {
+ commonDb: DbInfo;
+ bank: BankService;
+ exchange: ExchangeService;
+ exchangeBankAccount: HarnessExchangeBankAccount;
+ merchant: MerchantService;
+ wallet: WalletCli;
+}
+
+/**
+ * Improved version of the simple test environment,
+ * with the daemonized wallet.
+ */
+export interface SimpleTestEnvironmentNg {
+ commonDb: DbInfo;
+ bank: BankService;
+ exchange: ExchangeService;
+ exchangeBankAccount: HarnessExchangeBankAccount;
+ merchant: MerchantService;
+ walletClient: WalletClient;
+ walletService: WalletService;
+}
+
+export interface EnvOptions {
+ /**
+ * If provided, enable age restrictions with the specified age mask string.
+ */
+ ageMaskSpec?: string;
+
+ mixedAgeRestriction?: boolean;
+
+ additionalExchangeConfig?(e: ExchangeService): void;
+ additionalMerchantConfig?(m: MerchantService): void;
+ additionalBankConfig?(b: BankService): void;
+}
+
+export function getSharedTestDir(): string {
+ return `/tmp/taler-harness@${process.env.USER}`;
+}
+
+export async function useSharedTestkudosEnvironment(t: GlobalTestState) {
+ const coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS"));
+
+ const sharedDir = getSharedTestDir();
+
+ fs.mkdirSync(sharedDir, { recursive: true });
+
+ const db = await setupSharedDb(t);
+
+ let bank: FakebankService;
+
+ const prevSetupDone = fs.existsSync(sharedDir + "/setup-done");
+
+ logger.info(`previous setup done: ${prevSetupDone}`);
+
+ // Wallet has longer startup-time and no dependencies,
+ // so we start it rather early.
+ const walletStartProm = createWalletDaemonWithClient(t, { name: "wallet" });
+
+ if (fs.existsSync(sharedDir + "/bank.conf")) {
+ logger.info("reusing existing bank");
+ bank = BankService.fromExistingConfig(t, {
+ overridePath: sharedDir,
+ });
+ } else {
+ logger.info("creating new bank config");
+ bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ overrideTestDir: sharedDir,
+ });
+ }
+
+ logger.info("setting up exchange");
+
+ const exchangeName = "testexchange-1";
+ const exchangeConfigFilename = sharedDir + `/exchange-${exchangeName}.conf`;
+
+ logger.info(`exchange config filename: ${exchangeConfigFilename}`);
+
+ let exchange: ExchangeService;
+
+ if (fs.existsSync(exchangeConfigFilename)) {
+ logger.info("reusing existing exchange config");
+ exchange = ExchangeService.fromExistingConfig(t, exchangeName, {
+ overridePath: sharedDir,
+ });
+ } else {
+ logger.info("creating new exchange config");
+ exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ overrideTestDir: sharedDir,
+ });
+ }
+
+ logger.info("setting up merchant");
+
+ let merchant: MerchantService;
+ const merchantName = "testmerchant-1";
+ const merchantConfigFilename = sharedDir + `/merchant-${merchantName}}`;
+
+ if (fs.existsSync(merchantConfigFilename)) {
+ merchant = MerchantService.fromExistingConfig(t, merchantName, {
+ overridePath: sharedDir,
+ });
+ } else {
+ merchant = await MerchantService.create(t, {
+ name: "testmerchant-1",
+ currency: "TESTKUDOS",
+ httpPort: 8083,
+ database: db.connStr,
+ overrideTestDir: sharedDir,
+ });
+ }
+
+ logger.info("creating bank account for exchange");
+
+ const exchangeBankAccount = await bank.createExchangeAccount(
+ "myexchange",
+ "x",
+ );
+
+ logger.info("creating exchange bank account");
+ await exchange.addBankAccount("1", exchangeBankAccount);
+
+ bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
+
+ exchange.addCoinConfigList(coinConfig);
+
+ merchant.addExchange(exchange);
+
+ logger.info("basic setup done, starting services");
+
+ if (!prevSetupDone) {
+ // Must be done sequentially, due to a concurrency
+ // issue in the *-dbinit tools.
+ await exchange.dbinit();
+ await merchant.dbinit();
+ }
+
+ const bankStart = async () => {
+ await bank.start();
+ await bank.pingUntilAvailable();
+ };
+
+ const exchangeStart = async () => {
+ await exchange.start({
+ skipDbinit: true,
+ skipKeyup: prevSetupDone,
+ });
+ await exchange.pingUntilAvailable();
+ };
+
+ const merchStart = async () => {
+ await merchant.start({
+ skipDbinit: true,
+ });
+ await merchant.pingUntilAvailable();
+
+ if (!prevSetupDone) {
+ await merchant.addInstanceWithWireAccount({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [generateRandomPayto("merchant-default")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ await merchant.addInstanceWithWireAccount({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [generateRandomPayto("minst1")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+ }
+ };
+
+ await bankStart();
+
+ const res = await Promise.all([
+ exchangeStart(),
+ merchStart(),
+ undefined,
+ walletStartProm,
+ ]);
+
+ const walletClient = res[3].walletClient;
+ const walletService = res[3].walletService;
+
+ fs.writeFileSync(sharedDir + "/setup-done", "OK");
+
+ logger.info("setup done!");
+
+ return {
+ commonDb: db,
+ exchange,
+ merchant,
+ walletClient,
+ walletService,
+ bank,
+ exchangeBankAccount,
+ };
+}
+
+/**
+ * Run a test case with a simple TESTKUDOS Taler environment, consisting
+ * of one exchange, one bank and one merchant.
+ *
+ * V2 uses a daemonized wallet instead of the CLI wallet.
+ */
+export async function createSimpleTestkudosEnvironmentV2(
+ t: GlobalTestState,
+ coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
+ opts: EnvOptions = {},
+): Promise<SimpleTestEnvironmentNg> {
+ const db = await setupDb(t);
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ const merchant = await MerchantService.create(t, {
+ name: "testmerchant-1",
+ currency: "TESTKUDOS",
+ httpPort: 8083,
+ database: db.connStr,
+ });
+
+ const exchangeBankAccount = await bank.createExchangeAccount(
+ "myexchange",
+ "x",
+ );
+ await exchange.addBankAccount("1", exchangeBankAccount);
+
+ bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri);
+
+ if (opts.additionalBankConfig) {
+ opts.additionalBankConfig(bank);
+ }
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ const ageMaskSpec = opts.ageMaskSpec;
+
+ if (ageMaskSpec) {
+ exchange.enableAgeRestrictions(ageMaskSpec);
+ // Enable age restriction for all coins.
+ exchange.addCoinConfigList(
+ coinConfig.map((x) => ({
+ ...x,
+ name: `${x.name}-age`,
+ ageRestricted: true,
+ })),
+ );
+ // For mixed age restrictions, we also offer coins without age restrictions
+ if (opts.mixedAgeRestriction) {
+ exchange.addCoinConfigList(
+ coinConfig.map((x) => ({ ...x, ageRestricted: false })),
+ );
+ }
+ } else {
+ exchange.addCoinConfigList(coinConfig);
+ }
+
+ if (opts.additionalExchangeConfig) {
+ opts.additionalExchangeConfig(exchange);
+ }
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+
+ merchant.addExchange(exchange);
+
+ if (opts.additionalMerchantConfig) {
+ opts.additionalMerchantConfig(merchant);
+ }
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ await merchant.addInstanceWithWireAccount({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [generateRandomPayto("merchant-default")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ await merchant.addInstanceWithWireAccount({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [generateRandomPayto("minst1")],
+ defaultWireTransferDelay: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ const { walletClient, walletService } = await createWalletDaemonWithClient(
+ t,
+ { name: "wallet", persistent: true },
+ );
+
+ console.log("setup done!");
+
+ return {
+ commonDb: db,
+ exchange,
+ merchant,
+ walletClient,
+ walletService,
+ bank,
+ exchangeBankAccount,
+ };
+}
+
+export interface CreateWalletArgs {
+ handleNotification?(wn: WalletNotification): void;
+ name: string;
+ persistent?: boolean;
+ overrideDbPath?: string;
+ config?: PartialWalletRunConfig;
+}
+
+export async function createWalletDaemonWithClient(
+ t: GlobalTestState,
+ args: CreateWalletArgs,
+): Promise<{ walletClient: WalletClient; walletService: WalletService }> {
+ const walletService = new WalletService(t, {
+ name: args.name,
+ useInMemoryDb: !args.persistent,
+ overrideDbPath: args.overrideDbPath,
+ });
+ await walletService.start();
+ await walletService.pingUntilAvailable();
+
+ const observabilityEventFile = t.testDir + `/wallet-${args.name}-notifs.log`;
+
+ const onNotif = (notif: WalletNotification) => {
+ if (observabilityEventFile) {
+ fs.appendFileSync(
+ observabilityEventFile,
+ new Date().toISOString() + " " + JSON.stringify(notif) + "\n",
+ );
+ }
+ if (args.handleNotification) {
+ args.handleNotification(notif);
+ }
+ };
+
+ const walletClient = new WalletClient({
+ name: args.name,
+ unixPath: walletService.socketPath,
+ onNotification: onNotif,
+ });
+ await walletClient.connect();
+ const defaultRunConfig = {
+ testing: {
+ skipDefaults: true,
+ emitObservabilityEvents: !!process.env["TALER_TEST_OBSERVABILITY"],
+ },
+ } satisfies PartialWalletRunConfig;
+ await walletClient.client.call(WalletApiOperation.InitWallet, {
+ config: args.config ?? defaultRunConfig,
+ });
+
+ return { walletClient, walletService };
+}
+
+export interface FaultyMerchantTestEnvironment {
+ commonDb: DbInfo;
+ bank: BankService;
+ exchange: ExchangeService;
+ faultyExchange: FaultInjectedExchangeService;
+ exchangeBankAccount: HarnessExchangeBankAccount;
+ merchant: MerchantService;
+ faultyMerchant: FaultInjectedMerchantService;
+ walletClient: WalletClient;
+}
+
+/**
+ * Run a test case with a simple TESTKUDOS Taler environment, consisting
+ * of one exchange, one bank and one merchant.
+ */
+export async function createFaultInjectedMerchantTestkudosEnvironment(
+ t: GlobalTestState,
+): Promise<FaultyMerchantTestEnvironment> {
+ const db = await setupDb(t);
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ const merchant = await MerchantService.create(t, {
+ name: "testmerchant-1",
+ currency: "TESTKUDOS",
+ httpPort: 8083,
+ database: db.connStr,
+ });
+
+ const faultyMerchant = new FaultInjectedMerchantService(t, merchant, 9083);
+ const faultyExchange = new FaultInjectedExchangeService(t, exchange, 9081);
+
+ // Base URL must contain port that the proxy is listening on.
+ await exchange.modifyConfig(async (config) => {
+ config.setString("exchange", "base_url", "http://localhost:9081/");
+ });
+
+ const exchangeBankAccount = await bank.createExchangeAccount(
+ "myexchange",
+ "x",
+ );
+ exchange.addBankAccount("1", exchangeBankAccount);
+
+ bank.setSuggestedExchange(
+ faultyExchange,
+ exchangeBankAccount.accountPaytoUri,
+ );
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ exchange.addOfferedCoins(defaultCoinConfig);
+
+ await exchange.start();
+ await exchange.pingUntilAvailable();
+
+ merchant.addExchange(faultyExchange);
+
+ await merchant.start();
+ await merchant.pingUntilAvailable();
+
+ await merchant.addInstanceWithWireAccount({
+ id: "default",
+ name: "Default Instance",
+ paytoUris: [generateRandomPayto("merchant-default")],
+ });
+
+ await merchant.addInstanceWithWireAccount({
+ id: "minst1",
+ name: "minst1",
+ paytoUris: [generateRandomPayto("minst1")],
+ });
+
+ console.log("setup done!");
+
+ const { walletClient } = await createWalletDaemonWithClient(t, {
+ name: "default",
+ });
+
+ return {
+ commonDb: db,
+ exchange,
+ merchant,
+ walletClient,
+ bank,
+ exchangeBankAccount,
+ faultyMerchant,
+ faultyExchange,
+ };
+}
+
+export interface WithdrawViaBankResult {
+ withdrawalFinishedCond: Promise<true>;
+}
+
+/**
+ * Withdraw via a bank with the testing API enabled.
+ * Uses the new notification-based mechanism to wait for the
+ * operation to finish.
+ */
+export async function withdrawViaBankV2(
+ t: GlobalTestState,
+ p: {
+ walletClient: WalletClient;
+ bank: BankService;
+ exchange: ExchangeServiceInterface;
+ amount: AmountString | string;
+ restrictAge?: number;
+ },
+): Promise<WithdrawViaBankResult> {
+ const { walletClient: wallet, bank, exchange, amount } = p;
+
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl);
+
+ const user = await bankClient.createRandomBankUser();
+ const wop = await bankClient.createWithdrawalOperation(user.username, amount);
+
+ // Hand it to the wallet
+
+ await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, {
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ restrictAge: p.restrictAge,
+ });
+
+ // Withdraw (AKA select)
+
+ const acceptRes = await wallet.client.call(
+ WalletApiOperation.AcceptBankIntegratedWithdrawal,
+ {
+ exchangeBaseUrl: exchange.baseUrl,
+ talerWithdrawUri: wop.taler_withdraw_uri,
+ restrictAge: p.restrictAge,
+ },
+ );
+
+ const withdrawalFinishedCond = wallet.waitForNotificationCond(
+ (x) =>
+ x.type === NotificationType.TransactionStateTransition &&
+ x.newTxState.major === TransactionMajorState.Done &&
+ x.transactionId === acceptRes.transactionId,
+ );
+
+ // Confirm it
+
+ await bankClient.confirmWithdrawalOperation(user.username, {
+ withdrawalOperationId: wop.withdrawal_id,
+ });
+
+ return {
+ withdrawalFinishedCond,
+ };
+}
+
+export async function applyTimeTravelV2(
+ timetravelOffsetMs: number,
+ s: {
+ exchange?: ExchangeService;
+ merchant?: MerchantService;
+ walletClient?: WalletClient;
+ },
+): Promise<void> {
+ if (s.exchange) {
+ await s.exchange.stop();
+ s.exchange.setTimetravel(timetravelOffsetMs);
+ await s.exchange.start();
+ await s.exchange.pingUntilAvailable();
+ }
+
+ if (s.merchant) {
+ await s.merchant.stop();
+ s.merchant.setTimetravel(timetravelOffsetMs);
+ await s.merchant.start();
+ await s.merchant.pingUntilAvailable();
+ }
+
+ if (s.walletClient) {
+ await s.walletClient.call(WalletApiOperation.TestingSetTimetravel, {
+ offsetMs: timetravelOffsetMs,
+ });
+ }
+}
+
+/**
+ * Make a simple payment and check that it succeeded.
+ */
+export async function makeTestPaymentV2(
+ t: GlobalTestState,
+ args: {
+ merchant: MerchantServiceInterface;
+ walletClient: WalletClient;
+ order: Partial<MerchantContractTerms>;
+ instance?: string;
+ },
+ auth: WithAuthorization = {},
+): Promise<void> {
+ // Set up order.
+
+ const { walletClient, merchant, instance } = args;
+
+ const merchantClient = new MerchantApiClient(
+ merchant.makeInstanceBaseUrl(instance),
+ );
+
+ const orderResp = await merchantClient.createOrder({
+ order: args.order,
+ });
+
+ let orderStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderResp.order_id,
+ });
+
+ t.assertTrue(orderStatus.order_status === "unpaid");
+
+ // Make wallet pay for the order
+
+ const preparePayResult = await walletClient.call(
+ WalletApiOperation.PreparePayForUri,
+ {
+ talerPayUri: orderStatus.taler_pay_uri,
+ },
+ );
+
+ t.assertTrue(
+ preparePayResult.status === PreparePayResultType.PaymentPossible,
+ );
+
+ const r2 = await walletClient.call(WalletApiOperation.ConfirmPay, {
+ transactionId: preparePayResult.transactionId,
+ });
+
+ t.assertTrue(r2.type === ConfirmPayResultType.Done);
+
+ // Check if payment was successful.
+
+ orderStatus = await merchantClient.queryPrivateOrderStatus({
+ orderId: orderResp.order_id,
+ instance,
+ });
+
+ t.assertTrue(orderStatus.order_status === "paid");
+}