commit 2536a6504a299d2310d894c9969b3f5d58299990
parent 22fba35a812d0685adf57f5e15be67f8d99954ae
Author: Florian Dold <florian@dold.me>
Date: Wed, 24 Jun 2026 20:04:17 +0200
harness: extend token families test
Diffstat:
3 files changed, 206 insertions(+), 104 deletions(-)
diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts
@@ -1798,10 +1798,12 @@ playgroundCli
.subcommand("exp", "advanced-tokens-1", {})
.requiredOption("merchantUrl", ["--merchant-url"], clk.STRING)
.requiredOption("merchantPw", ["--merchant-pw"], clk.STRING)
+ .maybeOption("continueFamily", ["--continue"], clk.STRING)
.action(async (args) => {
await runPlaygroundAdvancedTokens1({
merchantPw: args.exp.merchantPw,
merchantUrl: args.exp.merchantUrl,
+ continueFamily: args.exp.continueFamily,
});
});
diff --git a/packages/taler-harness/src/integrationtests/test-merchant-tokenfamilies.ts b/packages/taler-harness/src/integrationtests/test-merchant-tokenfamilies.ts
@@ -20,9 +20,14 @@ NU General Public License along with
import {
AbsoluteTime,
Duration,
+ encodeCrock,
+ getRandomBytes,
+ j2s,
Order,
OrderInputType,
OrderOutputType,
+ OrderVersion,
+ PostOrderRequest,
succeedOrThrow,
TalerMerchantInstanceHttpClient,
TalerProtocolTimestamp,
@@ -33,7 +38,7 @@ import {
applyTimeTravelV2,
createSimpleTestkudosEnvironmentV3,
} from "../harness/environments.js";
-import { GlobalTestState } from "../harness/harness.js";
+import { GlobalTestState, waitMs } from "../harness/harness.js";
export async function runMerchantTokenfamiliesTest(t: GlobalTestState) {
let { merchant, walletClient, merchantAdminAccessToken, exchange } =
@@ -49,7 +54,7 @@ export async function runMerchantTokenfamiliesTest(t: GlobalTestState) {
},
);
- const merchantApi = new TalerMerchantInstanceHttpClient(
+ const merchantClient = new TalerMerchantInstanceHttpClient(
merchant.makeInstanceBaseUrl(),
);
@@ -68,13 +73,13 @@ export async function runMerchantTokenfamiliesTest(t: GlobalTestState) {
),
duration: Duration.toTalerProtocolDuration(Duration.fromSpec({ days: 90 })),
validity_granularity: Duration.toTalerProtocolDuration(
- Duration.fromSpec({ days: 1 }),
+ Duration.fromSpec({ hours: 1 }),
),
};
// setup discount token family
succeedOrThrow(
- await merchantApi.createTokenFamily(
+ await merchantClient.createTokenFamily(
merchantAdminAccessToken,
tokenFamilyJson,
),
@@ -116,13 +121,13 @@ export async function runMerchantTokenfamiliesTest(t: GlobalTestState) {
{
const orderResp = succeedOrThrow(
- await merchantApi.createOrder(merchantAdminAccessToken, {
+ await merchantClient.createOrder(merchantAdminAccessToken, {
order: orderJsonDiscount,
}),
);
let orderStatus = succeedOrThrow(
- await merchantApi.getOrderDetails(
+ await merchantClient.getOrderDetails(
merchantAdminAccessToken,
orderResp.order_id,
),
@@ -141,13 +146,13 @@ export async function runMerchantTokenfamiliesTest(t: GlobalTestState) {
{
const orderResp = succeedOrThrow(
- await merchantApi.createOrder(merchantAdminAccessToken, {
+ await merchantClient.createOrder(merchantAdminAccessToken, {
order: orderJsonDiscount,
}),
);
let orderStatus = succeedOrThrow(
- await merchantApi.getOrderDetails(
+ await merchantClient.getOrderDetails(
merchantAdminAccessToken,
orderResp.order_id,
),
@@ -155,6 +160,82 @@ export async function runMerchantTokenfamiliesTest(t: GlobalTestState) {
t.assertTrue(orderStatus.order_status === "unpaid");
}
+
+ // Now try with a more complex order that has multiple
+ // token families, used multiple times.
+
+ {
+ let suffix: string;
+ suffix = encodeCrock(getRandomBytes(16));
+
+ const slugDiscount1 = `slugdiscount1-${suffix}`;
+ await merchantClient.createTokenFamily(merchantAdminAccessToken, {
+ name: "discount1",
+ slug: slugDiscount1,
+ description: "My Discount 1 (valid two minutes)",
+ duration: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 2 }),
+ ),
+ valid_after: TalerProtocolTimestamp.now(),
+ valid_before: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ years: 1 }),
+ ),
+ ),
+ kind: TokenFamilyKind.Discount,
+ validity_granularity: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+
+ const req: PostOrderRequest = {
+ order: {
+ version: OrderVersion.V1,
+ summary: "Test Payment",
+ choices: [
+ {
+ amount: "TESTKUDOS:1",
+ description: "Article and discount",
+ outputs: [
+ {
+ type: OrderOutputType.Token,
+ token_family_slug: slugDiscount1,
+ },
+ ],
+ },
+ {
+ amount: "TESTKUDOS:1",
+ inputs: [
+ {
+ type: OrderInputType.Token,
+ token_family_slug: slugDiscount1,
+ },
+ ],
+ outputs: [],
+ },
+ ],
+ },
+ };
+
+ for (let i = 0; i < 5; i++) {
+ console.log(`iteration ${i}`);
+ const createResp = succeedOrThrow(
+ await merchantClient.createOrder(merchantAdminAccessToken, req),
+ );
+
+ const st = succeedOrThrow(
+ await merchantClient.getOrderDetails(
+ merchantAdminAccessToken,
+ createResp.order_id,
+ ),
+ );
+
+ console.log(`status: ${j2s(st)}`);
+
+ await waitMs(200);
+ }
+ }
}
runMerchantTokenfamiliesTest.suites = ["merchant"];
diff --git a/packages/taler-harness/src/playground.ts b/packages/taler-harness/src/playground.ts
@@ -23,6 +23,7 @@ import {
OrderInputType,
OrderOutputType,
OrderVersion,
+ PostOrderRequest,
TalerMerchantInstanceHttpClient,
TokenFamilyKind,
encodeCrock,
@@ -121,6 +122,10 @@ export interface PlaygroundAdvancedTokensArgs {
merchantUrl: string;
merchantPw: string;
payDelay?: string;
+ /**
+ * If specified, continue with the given token family suffix.
+ */
+ continueFamily?: string;
}
export async function runPlaygroundAdvancedTokens1(
@@ -141,48 +146,55 @@ export async function runPlaygroundAdvancedTokens1(
),
);
- const rnd = encodeCrock(getRandomBytes(16));
+ let suffix: string;
+ if (args.continueFamily != null) {
+ suffix = args.continueFamily;
+ } else {
+ suffix = encodeCrock(getRandomBytes(16));
+ }
- const slugDiscount1 = `slugdiscount1-${rnd}`;
- const slugSubscription1 = `slugsubscription1-${rnd}`;
+ const slugDiscount1 = `slugdiscount1-${suffix}`;
+ const slugSubscription1 = `slugsubscription1-${suffix}`;
- await merchantClient.createTokenFamily(tok.access_token, {
- name: "discount1",
- slug: slugDiscount1,
- description: "My Discount 1 (valid two minutes)",
- duration: Duration.toTalerProtocolDuration(
- Duration.fromSpec({ minutes: 2 }),
- ),
- valid_before: AbsoluteTime.toProtocolTimestamp(
- AbsoluteTime.addDuration(
- AbsoluteTime.now(),
- Duration.fromSpec({ hours: 1 }),
+ if (args.continueFamily == null) {
+ await merchantClient.createTokenFamily(tok.access_token, {
+ name: "discount1",
+ slug: slugDiscount1,
+ description: "My Discount 1 (valid two minutes)",
+ duration: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 10 }),
),
- ),
- kind: TokenFamilyKind.Discount,
- validity_granularity: Duration.toTalerProtocolDuration(
- Duration.fromSpec({ minutes: 1 }),
- ),
- });
+ valid_before: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ hours: 1 }),
+ ),
+ ),
+ kind: TokenFamilyKind.Discount,
+ validity_granularity: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
- await merchantClient.createTokenFamily(tok.access_token, {
- name: "subscription1",
- slug: slugSubscription1,
- description: "My Subscription 1 (valid one minute)",
- duration: Duration.toTalerProtocolDuration(
- Duration.fromSpec({ minutes: 2 }),
- ),
- valid_before: AbsoluteTime.toProtocolTimestamp(
- AbsoluteTime.addDuration(
- AbsoluteTime.now(),
- Duration.fromSpec({ hours: 1 }),
+ await merchantClient.createTokenFamily(tok.access_token, {
+ name: "subscription1",
+ slug: slugSubscription1,
+ description: "My Subscription 1 (valid one minute)",
+ duration: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 10 }),
),
- ),
- kind: TokenFamilyKind.Subscription,
- validity_granularity: Duration.toTalerProtocolDuration(
- Duration.fromSpec({ minutes: 1 }),
- ),
- });
+ valid_before: AbsoluteTime.toProtocolTimestamp(
+ AbsoluteTime.addDuration(
+ AbsoluteTime.now(),
+ Duration.fromSpec({ hours: 1 }),
+ ),
+ ),
+ kind: TokenFamilyKind.Subscription,
+ validity_granularity: Duration.toTalerProtocolDuration(
+ Duration.fromSpec({ minutes: 1 }),
+ ),
+ });
+ }
const payDeadline = AbsoluteTime.toProtocolTimestamp(
AbsoluteTime.addDuration(
@@ -195,66 +207,68 @@ export async function runPlaygroundAdvancedTokens1(
const articleAmount = `${config.currency}:1` as AmountString;
const articleDiscountedAmount = `${config.currency}:0.5` as AmountString;
+ const req: PostOrderRequest = {
+ order: {
+ pay_deadline: payDeadline,
+ version: OrderVersion.V1,
+ summary: "Test Payment",
+ choices: [
+ {
+ amount: articleAmount,
+ description: "Buy an individual article",
+ },
+ {
+ amount: subscriptionAmount as AmountString,
+ description: "Article and discount",
+ outputs: [
+ {
+ type: OrderOutputType.Token,
+ token_family_slug: slugDiscount1,
+ },
+ ],
+ },
+ {
+ amount: subscriptionAmount as AmountString,
+ description: "Article and subscription",
+ outputs: [
+ {
+ type: OrderOutputType.Token,
+ token_family_slug: slugSubscription1,
+ },
+ ],
+ },
+ {
+ amount: articleDiscountedAmount,
+ inputs: [
+ {
+ type: OrderInputType.Token,
+ token_family_slug: slugDiscount1,
+ },
+ ],
+ outputs: [],
+ },
+ {
+ amount: articleAmount,
+ description: "Article via subscription",
+ inputs: [
+ {
+ type: OrderInputType.Token,
+ token_family_slug: slugSubscription1,
+ },
+ ],
+ outputs: [
+ {
+ type: OrderOutputType.Token,
+ token_family_slug: slugSubscription1,
+ },
+ ],
+ },
+ ],
+ },
+ };
+
const createResp = succeedOrThrow(
- await merchantClient.createOrder(tok.access_token, {
- order: {
- pay_deadline: payDeadline,
- version: OrderVersion.V1,
- summary: "Test Payment",
- choices: [
- {
- amount: articleAmount,
- description: "Buy an individual article",
- },
- {
- amount: subscriptionAmount as AmountString,
- description: "Article and discount",
- outputs: [
- {
- type: OrderOutputType.Token,
- token_family_slug: slugDiscount1,
- },
- ],
- },
- {
- amount: subscriptionAmount as AmountString,
- description: "Article and subscription",
- outputs: [
- {
- type: OrderOutputType.Token,
- token_family_slug: slugSubscription1,
- },
- ],
- },
- {
- amount: articleDiscountedAmount,
- inputs: [
- {
- type: OrderInputType.Token,
- token_family_slug: slugDiscount1,
- },
- ],
- outputs: [],
- },
- {
- amount: articleAmount,
- description: "Article via subscription",
- inputs: [
- {
- type: OrderInputType.Token,
- token_family_slug: slugSubscription1,
- },
- ],
- outputs: [
- {
- type: OrderOutputType.Token,
- token_family_slug: slugSubscription1,
- },
- ],
- },
- ],
- },
- }),
+ await merchantClient.createOrder(tok.access_token, req),
);
const st = succeedOrThrow(
@@ -262,6 +276,11 @@ export async function runPlaygroundAdvancedTokens1(
);
if (st.order_status === "unpaid") {
+ if (args.continueFamily) {
+ console.log(`continued from ${args.continueFamily}`);
+ } else {
+ console.log(`continue with --continue=${suffix}`);
+ }
console.log(st.taler_pay_uri);
}
}