/*
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
*/
/**
* Imports.
*/
import {
codecForMerchantOrderStatusUnpaid,
ConfirmPayResultType,
j2s,
MerchantApiClient,
PreparePayResultType,
TalerCorebankApiClient,
TalerErrorCode,
TypedTalerErrorDetail,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { URL } from "url";
import { defaultCoinConfig } from "../harness/denomStructures.js";
import {
FaultInjectedExchangeService,
FaultInjectedMerchantService,
} from "../harness/faultInjection.js";
import {
BankService,
ExchangeService,
getTestHarnessPaytoForLabel,
GlobalTestState,
harnessHttpLib,
MerchantService,
setupDb,
} from "../harness/harness.js";
import {
createWalletDaemonWithClient,
FaultyMerchantTestEnvironmentNg,
withdrawViaBankV3,
} from "../harness/helpers.js";
/**
* Run a test case with a simple TESTKUDOS Taler environment, consisting
* of one exchange, one bank and one merchant.
*/
export async function createConfusedMerchantTestkudosEnvironment(
t: GlobalTestState,
): Promise {
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/");
});
let receiverName = "Exchange";
let exchangeBankUsername = "exchange";
let exchangeBankPassword = "mypw";
let exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername);
await exchange.addBankAccount("1", {
accountName: exchangeBankUsername,
accountPassword: exchangeBankPassword,
wireGatewayApiBaseUrl: new URL(
"accounts/exchange/taler-wire-gateway/",
bank.baseUrl,
).href,
accountPaytoUri: exchangePaytoUri,
});
bank.setSuggestedExchange(exchange, exchangePaytoUri);
await bank.start();
await bank.pingUntilAvailable();
const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
auth: {
username: "admin",
password: "adminpw",
},
});
await bankClient.registerAccountExtended({
name: receiverName,
password: exchangeBankPassword,
username: exchangeBankUsername,
is_taler_exchange: true,
payto_uri: exchangePaytoUri,
});
exchange.addOfferedCoins(defaultCoinConfig);
await exchange.start();
await exchange.pingUntilAvailable();
// Confuse the merchant by adding the non-proxied exchange.
merchant.addExchange(exchange);
await merchant.start();
await merchant.pingUntilAvailable();
await merchant.addInstanceWithWireAccount({
id: "default",
name: "Default Instance",
paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
});
await merchant.addInstanceWithWireAccount({
id: "minst1",
name: "minst1",
paytoUris: [getTestHarnessPaytoForLabel("minst1")],
});
console.log("setup done!");
const { walletClient } = await createWalletDaemonWithClient(t, {
name: "default",
});
return {
commonDb: db,
exchange,
merchant,
walletClient,
bankClient,
faultyMerchant,
faultyExchange,
};
}
/**
* Confuse the merchant by having one URL for the same exchange in the config,
* but sending coins from the same exchange with a different URL.
*/
export async function runMerchantExchangeConfusionTest(t: GlobalTestState) {
// Set up test environment
const { walletClient, bankClient, faultyExchange, faultyMerchant } =
await createConfusedMerchantTestkudosEnvironment(t);
// Withdraw digital cash into the wallet.
const wres = await withdrawViaBankV3(t, {
walletClient,
bankClient,
exchange: faultyExchange,
amount: "TESTKUDOS:20",
});
await wres.withdrawalFinishedCond;
/**
* =========================================================================
* Create an order and let the wallet pay under a session ID
*
* We check along the way that the JSON response to /orders/{order_id}
* returns the right thing.
* =========================================================================
*/
const merchant = faultyMerchant;
const merchantClient = new MerchantApiClient(merchant.makeInstanceBaseUrl());
let orderResp = await merchantClient.createOrder({
order: {
summary: "Buy me!",
amount: "TESTKUDOS:5",
fulfillment_url: "https://example.com/article42",
},
});
let orderStatus = await merchantClient.queryPrivateOrderStatus({
orderId: orderResp.order_id,
sessionId: "mysession-one",
});
t.assertTrue(orderStatus.order_status === "unpaid");
t.assertTrue(orderStatus.already_paid_order_id === undefined);
let publicOrderStatusUrl = orderStatus.order_status_url;
let publicOrderStatusResp = await harnessHttpLib.fetch(publicOrderStatusUrl);
if (publicOrderStatusResp.status != 402) {
throw Error(
`expected status 402 (before claiming), but got ${publicOrderStatusResp.status}`,
);
}
let pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
await publicOrderStatusResp.json(),
);
console.log(pubUnpaidStatus);
let preparePayResp = await walletClient.call(
WalletApiOperation.PreparePayForUri,
{
talerPayUri: pubUnpaidStatus.taler_pay_uri,
},
);
t.assertTrue(preparePayResp.status === PreparePayResultType.PaymentPossible);
const proposalId = preparePayResp.proposalId;
const orderUrlWithHash = new URL(publicOrderStatusUrl);
orderUrlWithHash.searchParams.set(
"h_contract",
preparePayResp.contractTermsHash,
);
console.log("requesting", orderUrlWithHash.href);
publicOrderStatusResp = await harnessHttpLib.fetch(orderUrlWithHash.href);
if (publicOrderStatusResp.status != 402) {
throw Error(
`expected status 402 (after claiming), but got ${publicOrderStatusResp.status}`,
);
}
pubUnpaidStatus = codecForMerchantOrderStatusUnpaid().decode(
await publicOrderStatusResp.json(),
);
const confirmPayRes = await walletClient.call(WalletApiOperation.ConfirmPay, {
proposalId: proposalId,
});
t.assertTrue(confirmPayRes.type === ConfirmPayResultType.Pending);
console.log(j2s(confirmPayRes.lastError));
// Merchant should not accept the payment!
// Something is clearly wrong, as the exchange now announces
// its own base URL and something is wrong.
// FIXME: This error code should probably be refined in the future.
t.assertDeepEqual(
confirmPayRes.lastError?.code,
TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR,
);
const err =
confirmPayRes.lastError as TypedTalerErrorDetail;
t.assertDeepEqual(err.httpStatusCode, 400);
}
runMerchantExchangeConfusionTest.suites = ["merchant"];