From 04ab9f37801f6a42b85581cc79667239d3fc79e5 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Sat, 11 Feb 2023 14:24:29 +0100 Subject: wallet-core,harness: implement pay templating --- packages/taler-harness/src/harness/harness.ts | 31 +- .../taler-harness/src/harness/merchantApiTypes.ts | 336 --------------------- .../src/integrationtests/test-payment-template.ts | 95 ++++++ .../src/integrationtests/testrunner.ts | 2 + 4 files changed, 121 insertions(+), 343 deletions(-) delete mode 100644 packages/taler-harness/src/harness/merchantApiTypes.ts create mode 100644 packages/taler-harness/src/integrationtests/test-payment-template.ts (limited to 'packages/taler-harness/src') diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts index 4e5d8238c..3659ea538 100644 --- a/packages/taler-harness/src/harness/harness.ts +++ b/packages/taler-harness/src/harness/harness.ts @@ -38,6 +38,7 @@ import { hash, j2s, Logger, + MerchantTemplateAddDetails, parsePaytoUri, stringToBytes, TalerProtocolDuration, @@ -66,15 +67,15 @@ import { CoinConfig } from "./denomStructures.js"; import { LibeufinNexusApi, LibeufinSandboxApi } from "./libeufin-apis.js"; import { codecForMerchantOrderPrivateStatusResponse, - codecForPostOrderResponse, + codecForMerchantPostOrderResponse, MerchantInstancesResponse, MerchantOrderPrivateStatusResponse, - PostOrderRequest, - PostOrderResponse, + MerchantPostOrderRequest, + MerchantPostOrderResponse, TipCreateConfirmation, TipCreateRequest, TippingReserveStatus, -} from "./merchantApiTypes.js"; +} from "@gnu-taler/taler-util"; import { createRemoteWallet, getClientFromRemoteWallet, @@ -1473,15 +1474,31 @@ export namespace MerchantPrivateApi { export async function createOrder( merchantService: MerchantServiceInterface, instanceName: string, - req: PostOrderRequest, + req: MerchantPostOrderRequest, withAuthorization: WithAuthorization = {}, - ): Promise { + ): Promise { const baseUrl = merchantService.makeInstanceBaseUrl(instanceName); let url = new URL("private/orders", baseUrl); const resp = await axios.post(url.href, req, { headers: withAuthorization as Record, }); - return codecForPostOrderResponse().decode(resp.data); + return codecForMerchantPostOrderResponse().decode(resp.data); + } + + export async function createTemplate( + merchantService: MerchantServiceInterface, + instanceName: string, + req: MerchantTemplateAddDetails, + withAuthorization: WithAuthorization = {}, + ) { + const baseUrl = merchantService.makeInstanceBaseUrl(instanceName); + let url = new URL("private/templates", baseUrl); + const resp = await axios.post(url.href, req, { + headers: withAuthorization as Record, + }); + if (resp.status !== 204) { + throw Error("failed to create template"); + } } export async function queryPrivateOrderStatus( diff --git a/packages/taler-harness/src/harness/merchantApiTypes.ts b/packages/taler-harness/src/harness/merchantApiTypes.ts deleted file mode 100644 index 1985e9150..000000000 --- a/packages/taler-harness/src/harness/merchantApiTypes.ts +++ /dev/null @@ -1,336 +0,0 @@ -/* - 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 - */ - -/** - * Test harness for various GNU Taler components. - * Also provides a fault-injection proxy. - * - * @author Florian Dold - */ - -/** - * Imports. - */ -import { - MerchantContractTerms, - Duration, - Codec, - buildCodecForObject, - codecForString, - codecOptional, - codecForConstString, - codecForBoolean, - codecForNumber, - codecForMerchantContractTerms, - codecForAny, - buildCodecForUnion, - AmountString, - AbsoluteTime, - CoinPublicKeyString, - EddsaPublicKeyString, - codecForAmountString, - TalerProtocolDuration, - codecForTimestamp, - TalerProtocolTimestamp, -} from "@gnu-taler/taler-util"; - -export interface PostOrderRequest { - // The order must at least contain the minimal - // order detail, but can override all - order: Partial; - - // if set, the backend will then set the refund deadline to the current - // time plus the specified delay. - refund_delay?: TalerProtocolDuration; - - // specifies the payment target preferred by the client. Can be used - // to select among the various (active) wire methods supported by the instance. - payment_target?: string; - - // FIXME: some fields are missing - - // Should a token for claiming the order be generated? - // False can make sense if the ORDER_ID is sufficiently - // high entropy to prevent adversarial claims (like it is - // if the backend auto-generates one). Default is 'true'. - create_token?: boolean; -} - -export type ClaimToken = string; - -export interface PostOrderResponse { - order_id: string; - token?: ClaimToken; -} - -export const codecForPostOrderResponse = (): Codec => - buildCodecForObject() - .property("order_id", codecForString()) - .property("token", codecOptional(codecForString())) - .build("PostOrderResponse"); - -export const codecForRefundDetails = (): Codec => - buildCodecForObject() - .property("reason", codecForString()) - .property("pending", codecForBoolean()) - .property("amount", codecForString()) - .property("timestamp", codecForTimestamp) - .build("PostOrderResponse"); - -export const codecForCheckPaymentPaidResponse = - (): Codec => - buildCodecForObject() - .property("order_status_url", codecForString()) - .property("order_status", codecForConstString("paid")) - .property("refunded", codecForBoolean()) - .property("wired", codecForBoolean()) - .property("deposit_total", codecForAmountString()) - .property("exchange_ec", codecForNumber()) - .property("exchange_hc", codecForNumber()) - .property("refund_amount", codecForAmountString()) - .property("contract_terms", codecForMerchantContractTerms()) - // FIXME: specify - .property("wire_details", codecForAny()) - .property("wire_reports", codecForAny()) - .property("refund_details", codecForAny()) - .build("CheckPaymentPaidResponse"); - -export const codecForCheckPaymentUnpaidResponse = - (): Codec => - buildCodecForObject() - .property("order_status", codecForConstString("unpaid")) - .property("taler_pay_uri", codecForString()) - .property("order_status_url", codecForString()) - .property("already_paid_order_id", codecOptional(codecForString())) - .build("CheckPaymentPaidResponse"); - -export const codecForCheckPaymentClaimedResponse = - (): Codec => - buildCodecForObject() - .property("order_status", codecForConstString("claimed")) - .property("contract_terms", codecForMerchantContractTerms()) - .build("CheckPaymentClaimedResponse"); - -export const codecForMerchantOrderPrivateStatusResponse = - (): Codec => - buildCodecForUnion() - .discriminateOn("order_status") - .alternative("paid", codecForCheckPaymentPaidResponse()) - .alternative("unpaid", codecForCheckPaymentUnpaidResponse()) - .alternative("claimed", codecForCheckPaymentClaimedResponse()) - .build("MerchantOrderPrivateStatusResponse"); - -export type MerchantOrderPrivateStatusResponse = - | CheckPaymentPaidResponse - | CheckPaymentUnpaidResponse - | CheckPaymentClaimedResponse; - -export interface CheckPaymentClaimedResponse { - // Wallet claimed the order, but didn't pay yet. - order_status: "claimed"; - - contract_terms: MerchantContractTerms; -} - -export interface CheckPaymentPaidResponse { - // did the customer pay for this contract - order_status: "paid"; - - // Was the payment refunded (even partially) - refunded: boolean; - - // Did the exchange wire us the funds - wired: boolean; - - // Total amount the exchange deposited into our bank account - // for this contract, excluding fees. - deposit_total: AmountString; - - // Numeric error code indicating errors the exchange - // encountered tracking the wire transfer for this purchase (before - // we even got to specific coin issues). - // 0 if there were no issues. - exchange_ec: number; - - // HTTP status code returned by the exchange when we asked for - // information to track the wire transfer for this purchase. - // 0 if there were no issues. - exchange_hc: number; - - // Total amount that was refunded, 0 if refunded is false. - refund_amount: AmountString; - - // Contract terms - contract_terms: MerchantContractTerms; - - // Ihe wire transfer status from the exchange for this order if available, otherwise empty array - wire_details: TransactionWireTransfer[]; - - // Reports about trouble obtaining wire transfer details, empty array if no trouble were encountered. - wire_reports: TransactionWireReport[]; - - // The refund details for this order. One entry per - // refunded coin; empty array if there are no refunds. - refund_details: RefundDetails[]; - - order_status_url: string; -} - -export interface CheckPaymentUnpaidResponse { - order_status: "unpaid"; - - // URI that the wallet must process to complete the payment. - taler_pay_uri: string; - - order_status_url: string; - - // Alternative order ID which was paid for already in the same session. - // Only given if the same product was purchased before in the same session. - already_paid_order_id?: string; - - // We do we NOT return the contract terms here because they may not - // exist in case the wallet did not yet claim them. -} - -export interface RefundDetails { - // Reason given for the refund - reason: string; - - // when was the refund approved - timestamp: TalerProtocolTimestamp; - - // has not been taken yet - pending: boolean; - - // Total amount that was refunded (minus a refund fee). - amount: AmountString; -} - -export interface TransactionWireTransfer { - // Responsible exchange - exchange_url: string; - - // 32-byte wire transfer identifier - wtid: string; - - // execution time of the wire transfer - execution_time: AbsoluteTime; - - // Total amount that has been wire transferred - // to the merchant - amount: AmountString; - - // Was this transfer confirmed by the merchant via the - // POST /transfers API, or is it merely claimed by the exchange? - confirmed: boolean; -} - -export interface TransactionWireReport { - // Numerical error code - code: number; - - // Human-readable error description - hint: string; - - // Numerical error code from the exchange. - exchange_ec: number; - - // HTTP status code received from the exchange. - exchange_hc: number; - - // Public key of the coin for which we got the exchange error. - coin_pub: CoinPublicKeyString; -} - -export interface TippingReserveStatus { - // Array of all known reserves (possibly empty!) - reserves: ReserveStatusEntry[]; -} - -export interface ReserveStatusEntry { - // Public key of the reserve - reserve_pub: string; - - // Timestamp when it was established - creation_time: AbsoluteTime; - - // Timestamp when it expires - expiration_time: AbsoluteTime; - - // Initial amount as per reserve creation call - merchant_initial_amount: AmountString; - - // Initial amount as per exchange, 0 if exchange did - // not confirm reserve creation yet. - exchange_initial_amount: AmountString; - - // Amount picked up so far. - pickup_amount: AmountString; - - // Amount approved for tips that exceeds the pickup_amount. - committed_amount: AmountString; - - // Is this reserve active (false if it was deleted but not purged) - active: boolean; -} - -export interface TipCreateConfirmation { - // Unique tip identifier for the tip that was created. - tip_id: string; - - // taler://tip URI for the tip - taler_tip_uri: string; - - // URL that will directly trigger processing - // the tip when the browser is redirected to it - tip_status_url: string; - - // when does the tip expire - tip_expiration: AbsoluteTime; -} - -export interface TipCreateRequest { - // Amount that the customer should be tipped - amount: AmountString; - - // Justification for giving the tip - justification: string; - - // URL that the user should be directed to after tipping, - // 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[]; -} diff --git a/packages/taler-harness/src/integrationtests/test-payment-template.ts b/packages/taler-harness/src/integrationtests/test-payment-template.ts new file mode 100644 index 000000000..41e43e28a --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-payment-template.ts @@ -0,0 +1,95 @@ +/* + 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 { ConfirmPayResultType, Duration, PreparePayResultType, TalerProtocolTimestamp } from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { GlobalTestState, MerchantPrivateApi } from "../harness/harness.js"; +import { + createSimpleTestkudosEnvironment, + withdrawViaBank, + makeTestPayment, +} from "../harness/helpers.js"; + +/** + * Test for taler://payment-template/ URIs + */ +export async function runPaymentTemplateTest(t: GlobalTestState) { + // Set up test environment + + const { wallet, bank, exchange, merchant } = + await createSimpleTestkudosEnvironment(t); + + await MerchantPrivateApi.createTemplate(merchant, "default", { + template_id: "template1", + template_description: "my test template", + template_contract: { + minimum_age: 0, + pay_duration: Duration.toTalerProtocolDuration( + Duration.fromSpec({ + minutes: 2, + }), + ), + summary: "hello, I'm a summary", + }, + }); + + // Withdraw digital cash into the wallet. + + await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" }); + + // Request a template payment + + const preparePayResult = await wallet.client.call( + WalletApiOperation.PreparePayForTemplate, + { + talerPayTemplateUri: `taler+http://pay-template/localhost:${merchant.port}/template1?amount=TESTKUDOS:1`, + templateParams: {}, + }, + ); + + console.log(preparePayResult); + + t.assertTrue( + preparePayResult.status === PreparePayResultType.PaymentPossible, + ); + + // Pay for it + + const r2 = await wallet.client.call(WalletApiOperation.ConfirmPay, { + proposalId: preparePayResult.proposalId, + }); + + t.assertTrue(r2.type === ConfirmPayResultType.Done); + + // Check if payment was successful. + + const orderStatus = await MerchantPrivateApi.queryPrivateOrderStatus( + merchant, + { + orderId: preparePayResult.contractTerms.order_id, + instance: "default", + }, + ); + + t.assertTrue(orderStatus.order_status === "paid"); + + await wallet.runUntilDone(); +} + +runPaymentTemplateTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts index 025f2e514..a20300e02 100644 --- a/packages/taler-harness/src/integrationtests/testrunner.ts +++ b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -100,6 +100,7 @@ import { runKycTest } from "./test-kyc.js"; import { runPaymentAbortTest } from "./test-payment-abort.js"; import { runWithdrawalFeesTest } from "./test-withdrawal-fees.js"; import { runWalletBalanceTest } from "./test-wallet-balance.js"; +import { runPaymentTemplateTest } from "./test-payment-template.js"; /** * Test runner. @@ -163,6 +164,7 @@ const allTests: TestMainFunction[] = [ runPaymentIdempotencyTest, runPaymentMultipleTest, runPaymentTest, + runPaymentTemplateTest, runPaymentAbortTest, runPaymentTransientTest, runPaymentZeroTest, -- cgit v1.2.3