From 1bccc40091dce7cbc3d521656a3f3b8f2f95ba56 Mon Sep 17 00:00:00 2001 From: Ervin Maftei Date: Mon, 15 Jun 2026 10:45:01 +0300 Subject: [PATCH] Add workshop Tokenization and PreAuth Playwright flows (Adyen Web v6) - workshop-tokenization-flow.spec.js: zero-auth token create, token payment, cancel, and cancel-then-fail flows - workshop-preauth-flow.spec.js: preauth -> adjust amount -> capture -> refund - workshop-advanced-flow.spec.js: use the v6 Drop-in card-fill helper to match the v6 workshop --- tests/workshop/workshop-advanced-flow.spec.js | 4 +- tests/workshop/workshop-preauth-flow.spec.js | 135 +++++++++++++++++ .../workshop-tokenization-flow.spec.js | 141 ++++++++++++++++++ 3 files changed, 278 insertions(+), 2 deletions(-) create mode 100644 tests/workshop/workshop-preauth-flow.spec.js create mode 100644 tests/workshop/workshop-tokenization-flow.spec.js diff --git a/tests/workshop/workshop-advanced-flow.spec.js b/tests/workshop/workshop-advanced-flow.spec.js index 4d9ac56..1c29a1e 100644 --- a/tests/workshop/workshop-advanced-flow.spec.js +++ b/tests/workshop/workshop-advanced-flow.spec.js @@ -25,8 +25,8 @@ test('Workshop - Advanced Flow Payment', async ({ page }) => { // Wait for load event await page.waitForLoadState('load'); - // Fill card details - await utilities.fillDropinCardDetails(page); + // Fill card details (Adyen.Web v6 Drop-in) + await utilities.fillDropinCardDetailsV6(page); // Click "Pay" button const payButton = page.locator('.adyen-checkout__button__text >> visible=true'); diff --git a/tests/workshop/workshop-preauth-flow.spec.js b/tests/workshop/workshop-preauth-flow.spec.js new file mode 100644 index 0000000..cb0888b --- /dev/null +++ b/tests/workshop/workshop-preauth-flow.spec.js @@ -0,0 +1,135 @@ +// @ts-check +const { test, expect } = require('@playwright/test'); +const utilities = require('../utilities'); +const { v4: uuidv4 } = require('uuid'); + +/** + * Workshop - Pre-Authorisation Flow + * + * Flow: /api/preauthorisation -> /api/modify-amount -> /api/capture -> /api/refund + * + * 1. Navigate to the preauthorisation checkout page + * 2. Select "Credit or debit card" and fill card details + * 3. Submit the pre-auth payment and capture the real pspReference + * 4. Verify success result page + * 5. Adjust the authorisation amount to 66 USD via /api/modify-amount + * 6. Capture the payment via /api/capture + * 7. Refund the payment via /api/refund + * 8. Simulate webhooks (AUTHORISATION, CAPTURE, REFUND) + */ +test('Workshop - PreAuth: Authorise, Adjust, Capture, Refund', async ({ page, request }) => { + // Step 1: Navigate to the preauthorisation checkout + await page.goto('/checkout?type=preauthorisation'); + + // Step 2: Wait for Drop-in to load and select "Credit or debit card" + await page.waitForLoadState('load'); + await expect(page.locator('text="Credit or debit card"')).toBeVisible(); + + const radioButton = await page.getByRole('radio', { name: 'Credit or debit card' }); + await radioButton.click(); + await page.waitForLoadState('load'); + + // Step 3: Fill card details and submit, capturing the real pspReference (Adyen.Web v6 Drop-in) + await utilities.fillDropinCardDetailsV6(page); + + // Intercept the API response to capture pspReference before page navigates + let pspReference; + await page.route('**/api/preauthorisation', async (route) => { + const response = await route.fetch(); + const body = await response.json(); + pspReference = body.pspReference; + console.log("PreAuth pspReference:", pspReference); + await route.fulfill({ response, json: body }); + }); + + const payButton = page.locator('.adyen-checkout__button__text >> visible=true'); + await expect(payButton).toBeVisible(); + await payButton.click(); + + // Step 4: Verify success - the pre-auth payment should be authorised + await expect(page.locator('text="Return Home"')).toBeVisible(); + + // Step 5: Adjust the authorisation amount to 66 USD using real pspReference + const modifyResponse = await request.post('/api/modify-amount', { + data: { + "pspReference": pspReference, + "amount": 6600 + } + }); + expect(modifyResponse.status()).toEqual(200); + console.log("Modify amount response:", JSON.stringify(await modifyResponse.json())); + + // Step 6: Capture the payment for 66 USD + const captureResponse = await request.post('/api/capture', { + data: { + "pspReference": pspReference, + "amount": 6600 + } + }); + expect(captureResponse.status()).toEqual(200); + console.log("Capture response:", JSON.stringify(await captureResponse.json())); + + // Step 7: Refund the captured payment + const refundResponse = await request.post('/api/refund', { + data: { + "pspReference": pspReference, + "amount": 6600 + } + }); + expect(refundResponse.status()).toEqual(200); + console.log("Refund response:", JSON.stringify(await refundResponse.json())); + + // Step 8: Simulate webhooks to verify webhook handling + var authNotification = { + "eventCode": "AUTHORISATION", + "success": "true", + "eventDate": "2026-06-12T12:00:00+01:00", + "merchantAccountCode": "YOUR_MERCHANT_ACCOUNT", + "pspReference": pspReference, + "merchantReference": "preauth-test", + "amount": { "value": 1000, "currency": "USD" } + }; + var hmacSignature = await utilities.calculateHmacSignature(authNotification); + authNotification["additionalData"] = { "hmacSignature": "" + hmacSignature + "" }; + + var webhookResponse = await request.post('/webhooks', { + data: { "live": "false", "notificationItems": [{ "NotificationRequestItem": authNotification }] } + }); + expect([200, 202]).toContain(webhookResponse.status()); + + var captureNotification = { + "eventCode": "CAPTURE", + "success": "true", + "eventDate": "2026-06-12T12:02:00+01:00", + "merchantAccountCode": "YOUR_MERCHANT_ACCOUNT", + "pspReference": uuidv4().replace(/-/g, '').substring(0, 16).toUpperCase(), + "originalReference": pspReference, + "merchantReference": "capture-test", + "amount": { "value": 6600, "currency": "USD" } + }; + hmacSignature = await utilities.calculateHmacSignature(captureNotification); + captureNotification["additionalData"] = { "hmacSignature": "" + hmacSignature + "" }; + + webhookResponse = await request.post('/webhooks', { + data: { "live": "false", "notificationItems": [{ "NotificationRequestItem": captureNotification }] } + }); + expect([200, 202]).toContain(webhookResponse.status()); + + var refundNotification = { + "eventCode": "REFUND", + "success": "true", + "eventDate": "2026-06-12T12:03:00+01:00", + "merchantAccountCode": "YOUR_MERCHANT_ACCOUNT", + "pspReference": uuidv4().replace(/-/g, '').substring(0, 16).toUpperCase(), + "originalReference": pspReference, + "merchantReference": "refund-test", + "amount": { "value": 6600, "currency": "USD" } + }; + hmacSignature = await utilities.calculateHmacSignature(refundNotification); + refundNotification["additionalData"] = { "hmacSignature": "" + hmacSignature + "" }; + + webhookResponse = await request.post('/webhooks', { + data: { "live": "false", "notificationItems": [{ "NotificationRequestItem": refundNotification }] } + }); + expect([200, 202]).toContain(webhookResponse.status()); +}); diff --git a/tests/workshop/workshop-tokenization-flow.spec.js b/tests/workshop/workshop-tokenization-flow.spec.js new file mode 100644 index 0000000..b68091b --- /dev/null +++ b/tests/workshop/workshop-tokenization-flow.spec.js @@ -0,0 +1,141 @@ +// @ts-check +const { test, expect } = require('@playwright/test'); +const utilities = require('../utilities'); +const { v4: uuidv4 } = require('uuid'); + +/** + * Workshop - Tokenization Flows (README_TOKENIZATION.md) + * + * Tests all tokenization flows: + * Flow 1: /api/subscription-create -> /api/subscription-payment + * Flow 2: /api/subscription-create -> /api/subscription-cancel + * Flow 3: /api/subscription-create -> /api/subscription-payment -> /api/subscription-cancel + * Flow 4: /api/subscription-create -> /api/subscription-payment -> /api/subscription-cancel -> /api/subscription-payment (should fail) + * + * Webhooks handled: AUTHORISATION, RECURRING_CONTRACT + */ + +// Helper: perform zero-auth via Drop-in UI and return token info +async function createToken(page) { + await page.goto('/checkout?type=tokenization'); + await page.waitForLoadState('load'); + await expect(page.locator('text="Credit or debit card"')).toBeVisible(); + + const radioButton = await page.getByRole('radio', { name: 'Credit or debit card' }); + await radioButton.click(); + await page.waitForLoadState('load'); + + await utilities.fillDropinCardDetailsV6(page); + + let pspReference; + let recurringDetailRef; + await page.route('**/api/subscription-create', async (route) => { + const response = await route.fetch(); + const body = await response.json(); + pspReference = body.pspReference; + recurringDetailRef = body.additionalData + ? body.additionalData['recurring.recurringDetailReference'] + : null; + console.log("Zero-auth pspReference:", pspReference); + console.log("recurringDetailReference:", recurringDetailRef); + await route.fulfill({ response, json: body }); + }); + + const payButton = page.locator('.adyen-checkout__button__text >> visible=true'); + await expect(payButton).toBeVisible(); + await payButton.click(); + + await expect(page.locator('text="Return Home"')).toBeVisible(); + + return { pspReference, recurringDetailRef }; +} + +// Helper: simulate RECURRING_CONTRACT webhook to store the token on the server +async function simulateRecurringWebhook(request, pspReference, token) { + var notificationRequestItem = { + "eventCode": "RECURRING_CONTRACT", + "success": "true", + "eventDate": "2026-06-12T12:00:00+01:00", + "merchantAccountCode": "YOUR_MERCHANT_ACCOUNT", + "pspReference": pspReference, + "merchantReference": "zero-auth-test", + "amount": { "value": 0, "currency": "EUR" }, + "additionalData": { + "recurring.recurringDetailReference": token, + "recurring.shopperReference": "test-shopper" + } + }; + const hmacSignature = await utilities.calculateHmacSignature(notificationRequestItem); + notificationRequestItem["additionalData"]["hmacSignature"] = "" + hmacSignature + ""; + + const resp = await request.post('/webhooks', { + data: { "live": "false", "notificationItems": [{ "NotificationRequestItem": notificationRequestItem }] } + }); + expect([200, 202]).toContain(resp.status()); +} + +// Flow 1: subscription-create -> subscription-payment +test('Tokenization Flow 1: Create Token → Make Payment', async ({ page, request }) => { + const { pspReference, recurringDetailRef } = await createToken(page); + const token = recurringDetailRef || ('TOKEN_' + uuidv4().substring(0, 8)); + + await simulateRecurringWebhook(request, pspReference, token); + + const payment = await request.post('/api/subscription-payment'); + expect(payment.status()).toEqual(200); + const result = await payment.json(); + console.log("Flow 1 - Payment result:", result.resultCode); + expect(result.resultCode).toBeDefined(); +}); + +// Flow 2: subscription-create -> subscription-cancel +test('Tokenization Flow 2: Create Token → Cancel Subscription', async ({ page, request }) => { + const { pspReference, recurringDetailRef } = await createToken(page); + const token = recurringDetailRef || ('TOKEN_' + uuidv4().substring(0, 8)); + + await simulateRecurringWebhook(request, pspReference, token); + + const cancel = await request.post('/api/subscription-cancel'); + expect(cancel.status()).toEqual(200); + const result = await cancel.text(); + console.log("Flow 2 - Cancel result:", result); +}); + +// Flow 3: subscription-create -> subscription-payment -> subscription-cancel +test('Tokenization Flow 3: Create Token → Payment → Cancel', async ({ page, request }) => { + const { pspReference, recurringDetailRef } = await createToken(page); + const token = recurringDetailRef || ('TOKEN_' + uuidv4().substring(0, 8)); + + await simulateRecurringWebhook(request, pspReference, token); + + const payment = await request.post('/api/subscription-payment'); + expect(payment.status()).toEqual(200); + const payResult = await payment.json(); + console.log("Flow 3 - Payment result:", payResult.resultCode); + expect(payResult.resultCode).toBeDefined(); + + const cancel = await request.post('/api/subscription-cancel'); + expect(cancel.status()).toEqual(200); + console.log("Flow 3 - Cancel result:", await cancel.text()); +}); + +// Flow 4: subscription-create -> subscription-payment -> subscription-cancel -> subscription-payment (should fail) +test('Tokenization Flow 4: Create Token → Payment → Cancel → Payment (should fail)', async ({ page, request }) => { + const { pspReference, recurringDetailRef } = await createToken(page); + const token = recurringDetailRef || ('TOKEN_' + uuidv4().substring(0, 8)); + + await simulateRecurringWebhook(request, pspReference, token); + + const payment1 = await request.post('/api/subscription-payment'); + expect(payment1.status()).toEqual(200); + console.log("Flow 4 - First payment:", (await payment1.json()).resultCode); + + const cancel = await request.post('/api/subscription-cancel'); + expect(cancel.status()).toEqual(200); + console.log("Flow 4 - Cancel:", await cancel.text()); + + // After cancel, subscription-payment should fail (400 = no token available) + const payment2 = await request.post('/api/subscription-payment'); + expect(payment2.status()).toEqual(400); + console.log("Flow 4 - Second payment after cancel returned 400 as expected"); +});