diff --git a/packages/taler-wallet-cli/src/integrationtests/harness.ts b/packages/taler-wallet-cli/src/integrationtests/harness.ts index 2fe976f9c..fbc67bb5a 100644 --- a/packages/taler-wallet-cli/src/integrationtests/harness.ts +++ b/packages/taler-wallet-cli/src/integrationtests/harness.ts @@ -94,6 +94,7 @@ import { TippingReserveStatus, TipCreateConfirmation, TipCreateRequest, + MerchantInstancesResponse, } from "./merchantApiTypes"; import { ApplyRefundResponse } from "@gnu-taler/taler-wallet-core"; import { PendingOperationsResponse } from "@gnu-taler/taler-wallet-core"; @@ -1171,6 +1172,40 @@ export interface MerchantServiceInterface { readonly name: string; } +export class MerchantApiClient { + constructor( + private baseUrl: string, + private auth: MerchantAuthConfiguration, + ) {} + + async changeAuth(auth: MerchantAuthConfiguration): Promise { + const baseUrl = this.baseUrl; + const url = new URL("private/auth", baseUrl); + await axios.post(url.href, auth, { + headers: this.makeAuthHeader(), + }); + } + + async getInstances(): Promise { + const url = new URL("private/instances", this.baseUrl); + const resp = await axios.get(url.href, { + headers: this.makeAuthHeader(), + }); + return resp.data; + } + + makeAuthHeader(): Record { + switch (this.auth.method) { + case "external": + return {}; + case "token": + return { + Authorization: `Bearer ${this.auth.token}`, + }; + } + } +} + export namespace MerchantPrivateApi { export async function createOrder( merchantService: MerchantServiceInterface, @@ -1407,8 +1442,9 @@ export class MerchantService implements MerchantServiceInterface { } console.log("adding instance"); const url = `http://localhost:${this.merchantConfig.httpPort}/private/instances`; + const auth = instanceConfig.auth ?? { method: "external" }; await axios.post(url, { - auth: { method: "external" }, + auth, payto_uris: instanceConfig.paytoUris, id: instanceConfig.id, name: instanceConfig.name, @@ -1443,8 +1479,13 @@ export class MerchantService implements MerchantServiceInterface { } } +export interface MerchantAuthConfiguration { + method: "external" | "token"; + token?: string; +} + export interface MerchantInstanceConfig { - authToken?: string; + auth?: MerchantAuthConfiguration; id: string; name: string; paytoUris: string[]; diff --git a/packages/taler-wallet-cli/src/integrationtests/merchantApiTypes.ts b/packages/taler-wallet-cli/src/integrationtests/merchantApiTypes.ts index a6356b317..1326d3a53 100644 --- a/packages/taler-wallet-cli/src/integrationtests/merchantApiTypes.ts +++ b/packages/taler-wallet-cli/src/integrationtests/merchantApiTypes.ts @@ -40,6 +40,7 @@ import { AmountString, Timestamp, CoinPublicKeyString, + EddsaPublicKeyString, } from "@gnu-taler/taler-wallet-core"; import { codecForAmountString } from "@gnu-taler/taler-wallet-core/lib/util/amounts"; @@ -294,3 +295,25 @@ export interface TipCreateRequest { // will be included in the tip_token. next_url: string; } + + +export interface MerchantInstancesResponse { + // List of instances that are present in the backend (see Instance) + instances: MerchantInstanceDetail[]; +} + +export interface MerchantInstanceDetail { + // Merchant name corresponding to this instance. + name: string; + + // Merchant instance this response is about ($INSTANCE) + id: string; + + // Public key of the merchant/instance, in Crockford Base32 encoding. + merchant_pub: EddsaPublicKeyString; + + // List of the payment targets supported by this instance. Clients can + // specify the desired payment target in /order requests. Note that + // front-ends do not have to support wallets selecting payment targets. + payment_targets: string[]; +} \ No newline at end of file diff --git a/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances.ts b/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances.ts new file mode 100644 index 000000000..d2e4cd121 --- /dev/null +++ b/packages/taler-wallet-cli/src/integrationtests/test-merchant-instances.ts @@ -0,0 +1,138 @@ +/* + This file is part of GNU Taler + (C) 2021 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 + */ + +/** + * Imports. + */ +import { URL } from "@gnu-taler/taler-wallet-core"; +import axios from "axios"; +import { + ExchangeService, + GlobalTestState, + MerchantApiClient, + MerchantService, + setupDb, +} from "./harness"; + +/** + * Do basic checks on instance management and authentication. + */ +export async function runMerchantInstancesTest(t: GlobalTestState) { + // Set up test environment + + const db = await setupDb(t); + + 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, + }); + + // We add the exchange to the config, but note that the exchange won't be started. + merchant.addExchange(exchange); + + await merchant.start(); + await merchant.pingUntilAvailable(); + + // Base URL for the default instance. + const baseUrl = merchant.makeInstanceBaseUrl(); + + { + const r = await axios.get(new URL("config", baseUrl).href); + console.log(r.data); + t.assertDeepEqual(r.data.currency, "TESTKUDOS"); + } + + // Instances should initially be empty + { + const r = await axios.get(new URL("private/instances", baseUrl).href); + t.assertDeepEqual(r.data.instances, []); + } + + // Add an instance, no auth! + await merchant.addInstance({ + id: "default", + name: "Default Instance", + paytoUris: [`payto://x-taler-bank/merchant-default`], + auth: { + method: "external", + }, + }); + + let merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl(), { + method: "external", + }); + + { + const r = await merchantClient.getInstances(); + t.assertDeepEqual(r.instances.length, 1); + } + + // Check that a "malformed" bearer Authorization header gets ignored + { + const url = merchant.makeInstanceBaseUrl(); + const resp = await axios.get(new URL("private/instances", url).href, { + headers: { + "Authorization": "foo bar-baz", + }, + }); + t.assertDeepEqual(resp.status, 200); + } + + await merchantClient.changeAuth({ + method: "token", + token: "secret-token:foobar", + }); + + // Now this should fail, as we didn't change the auth of the client yet. + const exc = await t.assertThrowsAsync(async () => { + await merchantClient.getInstances(); + }); + + t.assertAxiosError(exc); + t.assertAxiosError(exc.response?.status === 401); + + merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl(), { + method: "token", + token: "secret-token:foobar", + }); + + // With the new client auth settings, request should work again. + await merchantClient.getInstances(); + + // Now, try some variations. + + { + const url = merchant.makeInstanceBaseUrl(); + const resp = await axios.get(new URL("private/instances", url).href, { + headers: { + // Note the spaces + "Authorization": "Bearer secret-token:foobar", + } + }); + t.assertDeepEqual(resp.status, 200); + } +} + +runMerchantInstancesTest.suites = ["merchant"]; diff --git a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts index 252dbafb6..6568e5667 100644 --- a/packages/taler-wallet-cli/src/integrationtests/testrunner.ts +++ b/packages/taler-wallet-cli/src/integrationtests/testrunner.ts @@ -59,6 +59,7 @@ import { runLibeufinRefundTest } from "./test-libeufin-refund"; import { runLibeufinTutorialTest } from "./test-libeufin-tutorial"; import { runDepositTest } from "./test-deposit"; import CancellationToken from "cancellationtoken"; +import { runMerchantInstancesTest } from "./test-merchant-instances"; /** * Test runner. @@ -83,6 +84,7 @@ const allTests: TestMainFunction[] = [ runLibeufinTutorialTest, runLibeufinRefundTest, runMerchantExchangeConfusionTest, + runMerchantInstancesTest, runMerchantLongpollingTest, runMerchantRefundApiTest, runPayAbortTest,