From 2b8848197e783569e4b97f07bdff689ed76de27f Mon Sep 17 00:00:00 2001 From: SHENGXING LU Date: Mon, 3 Aug 2026 20:28:15 +0200 Subject: [PATCH 1/2] redsys testing + bug fixings --- billing-server/server.js | 2 +- charging-docker/docker-compose.yml | 7 +- cypress.config.ts | 4 +- cypress/e2e/01-happy-journey.cy.ts | 4 +- .../04-billing-and-payment-scheduler.cy.ts | 101 +++++++++++---- cypress/e2e/edges/global-states/test-2.cy.ts | 14 ++- cypress/e2e/edges/global-states/test1.cy.ts | 12 +- cypress/e2e/edges/global-states/test2.cy.ts | 12 +- cypress/e2e/edges/global-states/test3.cy.ts | 12 +- cypress/e2e/edges/global-states/test4.cy.ts | 12 +- cypress/e2e/edges/global-states/test5.cy.ts | 12 +- cypress/e2e/edges/global-states/test6.cy.ts | 12 +- cypress/e2e/edges/mod-order.cy.ts | 9 +- cypress/e2e/edges/multi-price-billing.cy.ts | 26 ++-- cypress/e2e/edges/procurement.cy.ts | 4 +- cypress/e2e/edges/transfer-bank.cy.ts | 7 +- cypress/support/commands.ts | 119 +++++++++++++++++- cypress/support/e2e.ts | 30 ++++- cypress/support/form-helpers.ts | 14 ++- cypress/support/global-state-flows.ts | 12 +- docker-speed/Dockerfile.charging | 23 +++- proxy-docker/docker-compose.yml | 1 + reload.sh | 9 ++ 23 files changed, 361 insertions(+), 97 deletions(-) diff --git a/billing-server/server.js b/billing-server/server.js index 002a71c..8e31661 100644 --- a/billing-server/server.js +++ b/billing-server/server.js @@ -160,7 +160,7 @@ app.post('/v1/checkout/sessions', (req, res) => { status: 'open', customer: customerId, payment_intent: paymentIntentId, - url: `http://localhost:${PORT}/stripe-checkout?session_id=${sessionId}`, + url: `http://localhost:${PORT}/stripe/checkin`, line_items: lineItems, } stripeSessions.set(sessionId, session) diff --git a/charging-docker/docker-compose.yml b/charging-docker/docker-compose.yml index 815f1e6..916afbd 100644 --- a/charging-docker/docker-compose.yml +++ b/charging-docker/docker-compose.yml @@ -33,6 +33,7 @@ services: # Service Configuration - BAE_SERVICE_HOST=http://proxy.docker:8004/ - BAE_CB_LOCAL_SITE=http://charging.docker:8006/ + - GUNICORN_CMD_ARGS=--timeout 120 # TMForum APIs - all-in-one (port 8633) - BAE_CB_CATALOG=http://host.docker.internal:8633/tmf-api/productCatalogManagement/v4 @@ -58,7 +59,10 @@ services: - BAE_LP_OAUTH2_CUSTOMER_ROLE=customer - BAE_LP_OAUTH2_ORG_ADMIN_ROLE=orgAdmin # Payment Configuration - - BAE_CB_PAYMENT_METHOD=stripe + - BAE_CB_PAYMENT_METHOD=redsys + - BAE_CB_REDSYS_MERCHANT_CODE=263100000 + - BAE_CB_REDSYS_TERMINAL=068 + - BAE_CB_REDSYS_SECRET_KEY=sq7HjrUOBfKmC576ILgskD5srU870gJ7 - BAE_CB_BILLING_ENGINE=local - BAE_CB_DPAS_CLIENT_API_URL=http://host.docker.internal:4201/api/payment-start - BAE_SERVICE_HOST=http://localhost:4200/ @@ -70,4 +74,3 @@ services: networks: main: external: true - diff --git a/cypress.config.ts b/cypress.config.ts index 077714f..b050b34 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -13,7 +13,9 @@ export default defineConfig({ requestTimeout: 120000, responseTimeout: 120000, env: { - PAYMENT_METHOD: process.env.BAE_CB_PAYMENT_METHOD || 'stripe' + PAYMENT_METHOD: process.env.BAE_CB_PAYMENT_METHOD || 'redsys', + REDSYS_ORIGIN: process.env.BAE_CB_REDSYS_ORIGIN || 'https://sis-t.redsys.es:25443', + REDSYS_AUTH_ORIGIN: process.env.BAE_CB_REDSYS_AUTH_ORIGIN || 'https://sis-d.redsys.es' } } }) diff --git a/cypress/e2e/01-happy-journey.cy.ts b/cypress/e2e/01-happy-journey.cy.ts index 531924f..2c10371 100644 --- a/cypress/e2e/01-happy-journey.cy.ts +++ b/cypress/e2e/01-happy-journey.cy.ts @@ -170,12 +170,14 @@ describe('Happy Journey E2E', { cy.wait('@getBilling') // Wait for the billing address to be processed and selected cy.wait(2000) + cy.deferPaymentRedirect() cy.getBySel('checkout').should('be.visible').should('not.be.disabled').click() cy.wait('@createOrder') - cy.wait('@getOrders') + cy.waitForOrdersBeforePayment() cy.intercept('**/charging/api/orderManagement/orders/confirm/').as('checkin') cy.completePayment() cy.wait('@checkin') + cy.waitForOrdersAfterPayment() cy.wait('@getBilling') // ============================================ diff --git a/cypress/e2e/04-billing-and-payment-scheduler.cy.ts b/cypress/e2e/04-billing-and-payment-scheduler.cy.ts index 8062f25..fc96e3e 100644 --- a/cypress/e2e/04-billing-and-payment-scheduler.cy.ts +++ b/cypress/e2e/04-billing-and-payment-scheduler.cy.ts @@ -16,13 +16,15 @@ import { * * After each billing scheduler run, also triggers the payment scheduler * cron job and verifies that the resulting CustomerBill ends up 'settled' - * once the recurring charge against the stored Stripe payment method succeeds. + * once the recurring charge against the stored payment method succeeds. * * Requires BAE_CB_BILLING_HTTP_ENABLED=true in the charging container. */ const CHARGING_URL = 'http://localhost:8006' const TMF_URL = 'http://localhost:8633' +const SCORPIO_URL = 'http://localhost:1026' const BILLING_SERVER_URL = 'http://localhost:4201' +const IS_REDSYS = Cypress.env('PAYMENT_METHOD') === 'redsys' const runPaymentScheduler = () => { cy.request({ url: `${CHARGING_URL}/charging/api/test/paymentScheduler`, method: 'POST' }).then((res) => { @@ -40,6 +42,65 @@ const expectCustomerBillState = (billId: string, expectedState: string) => { }) } +const patchCustomerBillAmountInScorpio = ( + billId: string, + taxIncludedAmount: { unit: string, value: number | string } +) => { + const value = Number(taxIncludedAmount.value) + + cy.request({ + url: `${SCORPIO_URL}/ngsi-ld/v1/entities/${encodeURIComponent(billId)}/attrs`, + method: 'PATCH', + headers: { 'Content-Type': 'application/ld+json' }, + body: { + taxIncludedAmount: { + type: 'Property', + tmfValue: { type: 'Property', value }, + unit: { type: 'Property', value: taxIncludedAmount.unit }, + value: { tmfValue: value, unit: taxIncludedAmount.unit }, + }, + '@context': ['https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.7.jsonld'], + }, + }).then((res) => { + expect(res.status).to.eq(204) + }) +} + +const expectRecurringPaymentToRetry = ( + billId: string, + stripeStatus: 'processing' | 'requires_payment_method' +) => { + const runAndVerifyRetry = (restorePayment: () => void = () => {}) => { + runPaymentScheduler() + expectCustomerBillState(billId, 'new') + + restorePayment() + runPaymentScheduler() + expectCustomerBillState(billId, 'settled') + } + + if (!IS_REDSYS) { + cy.request(`${BILLING_SERVER_URL}/stripe/set-recurring-status/${stripeStatus}`) + runAndVerifyRetry() + return + } + + cy.request({ + url: `${TMF_URL}/tmf-api/customerBillManagement/v4/customerBill/${billId}`, + method: 'GET', + }).then((res) => { + expect(res.status).to.eq(200) + const originalAmount = res.body.taxIncludedAmount + const rejectedAmount = { + ...originalAmount, + value: `${Math.floor(Number(originalAmount.value))}.96`, + } + + patchCustomerBillAmountInScorpio(billId, rejectedAmount) + runAndVerifyRetry(() => patchCustomerBillAmountInScorpio(billId, originalAmount)) + }) +} + describe('Billing Scheduler Period Coverage', { viewportHeight: 1080, viewportWidth: 1920, @@ -65,6 +126,7 @@ describe('Billing Scheduler Period Coverage', { cy.intercept('GET', '**/ordering/productOrder*').as('getOrders') cy.intercept('GET', '**/account/billingAccount*').as('getBilling') cy.intercept('GET', '**/shoppingCart/item/').as('cartItem') + cy.intercept('GET', '**/paymentInfo').as('getPaymentInfo') // ============================================ // Verify catalog and product spec exist (from happy journey) @@ -139,7 +201,12 @@ describe('Billing Scheduler Period Coverage', { cy.getBySel('savePricePlan').click() cy.getBySel('offerNext').click() - cy.getBySel('procurement').select('automatic') + cy.wait('@getPaymentInfo') + .its('response.body.gatewaysCount') + .should('be.greaterThan', 0) + cy.getBySel('procurement') + .select('automatic') + .should('have.value', 'automatic') cy.getBySel('offerNext').click() cy.getBySel('offerFinish').click() @@ -184,16 +251,18 @@ describe('Billing Scheduler Period Coverage', { cy.wait(2000) cy.wait('@getBilling') cy.wait(2000) + cy.deferPaymentRedirect() cy.getBySel('checkout').should('be.visible').should('not.be.disabled').click() cy.wait('@createOrder') - cy.wait('@getOrders') + cy.waitForOrdersBeforePayment() // ============================================ // Step 4: Complete payment (activation) // ============================================ cy.intercept('**/charging/api/orderManagement/orders/confirm/').as('checkin') - cy.completePayment() + cy.completePayment({ recurring: true }) cy.wait('@checkin') + cy.waitForOrdersAfterPayment() cy.getBySel('ordersTable').should('be.visible') cy.getBySel('ordersTable').find('tbody tr').first().within(() => { @@ -398,18 +467,12 @@ describe('Billing Scheduler Period Coverage', { expect(new Date(acbrs[0].periodCoverage.endDateTime).getTime()).to.equal(week2End.getTime()) // ============================================ - // Payment scheduler: recurring charge comes back pending on the - // first attempt, so the CB must be left untouched ('new'), then - // settles on the next scheduler run once the charge resolves. + // Stripe leaves the first charge pending; Redsys uses a sandbox + // rejection amount. Both must leave the bill 'new' and then settle. // ============================================ const cbId = acbrs[0].bill.id - cy.request(`${BILLING_SERVER_URL}/stripe/set-recurring-status/processing`) - runPaymentScheduler() - expectCustomerBillState(cbId, 'new') - - runPaymentScheduler() - expectCustomerBillState(cbId, 'settled') + expectRecurringPaymentToRetry(cbId, 'processing') }) // — Week 3 — @@ -450,18 +513,12 @@ describe('Billing Scheduler Period Coverage', { expect(new Date(acbrs[0].periodCoverage.endDateTime).getTime()).to.equal(week4End.getTime()) // ============================================ - // Payment scheduler: recurring charge fails on the first attempt, - // so the CB must be left untouched ('new'), then settles on the - // next scheduler run once the charge succeeds. + // Stripe requires a new payment method; Redsys uses a sandbox + // rejection amount. Both must leave the bill 'new' and then settle. // ============================================ const cbId = acbrs[0].bill.id - cy.request(`${BILLING_SERVER_URL}/stripe/set-recurring-status/requires_payment_method`) - runPaymentScheduler() - expectCustomerBillState(cbId, 'new') - - runPaymentScheduler() - expectCustomerBillState(cbId, 'settled') + expectRecurringPaymentToRetry(cbId, 'requires_payment_method') }) // ============================================ diff --git a/cypress/e2e/edges/global-states/test-2.cy.ts b/cypress/e2e/edges/global-states/test-2.cy.ts index 8b51096..3d3c522 100644 --- a/cypress/e2e/edges/global-states/test-2.cy.ts +++ b/cypress/e2e/edges/global-states/test-2.cy.ts @@ -45,7 +45,11 @@ describe('Check order global states - Reverse test (auto and semi failed, iterat cy.intercept('POST', '**/shoppingCart/item/').as('postCart') cy.intercept('POST', '**/ordering/productOrder').as('createOrder') cy.intercept('GET', '**/ordering/productOrder*').as('getOrders') + cy.intercept('GET', '**/ordering/productOrder?*relatedParty.role=seller*').as('getProviderOrders') cy.intercept('GET', '**/account/billingAccount*').as('getBilling') + cy.intercept('GET', '**/catalog/productOffering?*', (request) => { + delete request.headers['if-none-match'] + }) cy.loginAsAdmin() cy.on('uncaught:exception', (err) => { @@ -102,7 +106,7 @@ describe('Check order global states - Reverse test (auto and semi failed, iterat cy.getBySel('shoppingCart').click() cy.getBySel('cartPurchase').click() - cy.intercept('POST', '**/ordering/productOrder').as('createOrder') + cy.deferPaymentRedirect() cy.intercept('GET', '**/ordering/productOrder*').as('getOrders') cy.intercept('GET', '**/account/billingAccount*').as('getBilling') @@ -111,7 +115,7 @@ describe('Check order global states - Reverse test (auto and semi failed, iterat cy.wait(2000) cy.getBySel('checkout').should('be.visible').should('not.be.disabled').click() cy.wait('@createOrder') - cy.wait('@getOrders') + cy.waitForOrdersBeforePayment() // Fail the auto and semi offering cy.intercept('**/charging/api/orderManagement/orders/confirm/').as('checkin') @@ -123,9 +127,9 @@ describe('Check order global states - Reverse test (auto and semi failed, iterat // Navigate to product orders as provider cy.visit('/product-orders') cy.wait('@getOrders') - cy.wait(500) - cy.getBySel('asProviderTab').click() - cy.wait('@getOrders') + cy.getBySel('ordersTable').should('be.visible') + cy.getBySel('asProviderTab').should('be.visible').click() + cy.wait('@getProviderOrders') cy.getBySel('ordersTable').should('be.visible') // Find the most recent order and set auto and semi to failed diff --git a/cypress/e2e/edges/global-states/test1.cy.ts b/cypress/e2e/edges/global-states/test1.cy.ts index b36397f..7da7ab9 100644 --- a/cypress/e2e/edges/global-states/test1.cy.ts +++ b/cypress/e2e/edges/global-states/test1.cy.ts @@ -58,9 +58,9 @@ describe('Check order global states', { cy.changeSessionTo('SELLER ORG') cy.visit('/product-orders') cy.wait('@getOrders') - cy.wait(500) - cy.getBySel('asProviderTab').click() - cy.wait('@getOrders') + cy.getBySel('ordersTable').should('be.visible') + cy.getBySel('asProviderTab').should('be.visible').click() + cy.wait('@getProviderOrders') cy.getBySel('ordersTable').should('be.visible') // check global state cy.getBySel('ordersTable').find('tbody tr').first().within(() => { @@ -121,9 +121,9 @@ describe('Check order global states', { cy.changeSessionTo('SELLER ORG') cy.visit('/product-orders') cy.wait('@getOrders') - cy.wait(500) - cy.getBySel('asProviderTab').click() - cy.wait('@getOrders') + cy.getBySel('ordersTable').should('be.visible') + cy.getBySel('asProviderTab').should('be.visible').click() + cy.wait('@getProviderOrders') cy.getBySel('ordersTable').should('be.visible') // check global state cy.getBySel('ordersTable').find('tbody tr').first().within(() => { diff --git a/cypress/e2e/edges/global-states/test2.cy.ts b/cypress/e2e/edges/global-states/test2.cy.ts index 6685479..616dfab 100644 --- a/cypress/e2e/edges/global-states/test2.cy.ts +++ b/cypress/e2e/edges/global-states/test2.cy.ts @@ -58,9 +58,9 @@ describe('Check order global states', { cy.changeSessionTo('SELLER ORG') cy.visit('/product-orders') cy.wait('@getOrders') - cy.wait(500) - cy.getBySel('asProviderTab').click() - cy.wait('@getOrders') + cy.getBySel('ordersTable').should('be.visible') + cy.getBySel('asProviderTab').should('be.visible').click() + cy.wait('@getProviderOrders') cy.getBySel('ordersTable').should('be.visible') // check global state cy.getBySel('ordersTable').find('tbody tr').first().within(() => { @@ -122,9 +122,9 @@ describe('Check order global states', { cy.changeSessionTo('SELLER ORG') cy.visit('/product-orders') cy.wait('@getOrders') - cy.wait(500) - cy.getBySel('asProviderTab').click() - cy.wait('@getOrders') + cy.getBySel('ordersTable').should('be.visible') + cy.getBySel('asProviderTab').should('be.visible').click() + cy.wait('@getProviderOrders') cy.getBySel('ordersTable').should('be.visible') // check global state cy.getBySel('ordersTable').find('tbody tr').first().within(() => { diff --git a/cypress/e2e/edges/global-states/test3.cy.ts b/cypress/e2e/edges/global-states/test3.cy.ts index 6c23337..e6dc26d 100644 --- a/cypress/e2e/edges/global-states/test3.cy.ts +++ b/cypress/e2e/edges/global-states/test3.cy.ts @@ -58,9 +58,9 @@ describe('Check order global states', { cy.changeSessionTo('SELLER ORG') cy.visit('/product-orders') cy.wait('@getOrders') - cy.wait(500) - cy.getBySel('asProviderTab').click() - cy.wait('@getOrders') + cy.getBySel('ordersTable').should('be.visible') + cy.getBySel('asProviderTab').should('be.visible').click() + cy.wait('@getProviderOrders') cy.getBySel('ordersTable').should('be.visible') // check global state cy.getBySel('ordersTable').find('tbody tr').first().within(() => { @@ -121,9 +121,9 @@ describe('Check order global states', { cy.changeSessionTo('SELLER ORG') cy.visit('/product-orders') cy.wait('@getOrders') - cy.wait(500) - cy.getBySel('asProviderTab').click() - cy.wait('@getOrders') + cy.getBySel('ordersTable').should('be.visible') + cy.getBySel('asProviderTab').should('be.visible').click() + cy.wait('@getProviderOrders') cy.getBySel('ordersTable').should('be.visible') // check global state diff --git a/cypress/e2e/edges/global-states/test4.cy.ts b/cypress/e2e/edges/global-states/test4.cy.ts index 57a1649..2bb148d 100644 --- a/cypress/e2e/edges/global-states/test4.cy.ts +++ b/cypress/e2e/edges/global-states/test4.cy.ts @@ -58,9 +58,9 @@ describe('Check order global states', { cy.changeSessionTo('SELLER ORG') cy.visit('/product-orders') cy.wait('@getOrders') - cy.wait(500) - cy.getBySel('asProviderTab').click() - cy.wait('@getOrders') + cy.getBySel('ordersTable').should('be.visible') + cy.getBySel('asProviderTab').should('be.visible').click() + cy.wait('@getProviderOrders') cy.getBySel('ordersTable').should('be.visible') // check global state cy.getBySel('ordersTable').find('tbody tr').first().within(() => { @@ -122,9 +122,9 @@ describe('Check order global states', { cy.changeSessionTo('SELLER ORG') cy.visit('/product-orders') cy.wait('@getOrders') - cy.wait(500) - cy.getBySel('asProviderTab').click() - cy.wait('@getOrders') + cy.getBySel('ordersTable').should('be.visible') + cy.getBySel('asProviderTab').should('be.visible').click() + cy.wait('@getProviderOrders') cy.getBySel('ordersTable').should('be.visible') // check global state cy.getBySel('ordersTable').find('tbody tr').first().within(() => { diff --git a/cypress/e2e/edges/global-states/test5.cy.ts b/cypress/e2e/edges/global-states/test5.cy.ts index 84d94ad..e5cfe1f 100644 --- a/cypress/e2e/edges/global-states/test5.cy.ts +++ b/cypress/e2e/edges/global-states/test5.cy.ts @@ -58,10 +58,10 @@ describe('Check order global states', { cy.changeSessionTo('SELLER ORG') cy.visit('/product-orders') cy.wait('@getOrders') - cy.wait(500) - cy.getBySel('asProviderTab').click() - cy.wait('@getOrders') cy.getBySel('ordersTable').should('be.visible') + cy.getBySel('asProviderTab').should('be.visible').click() + cy.wait('@getProviderOrders') + cy.getBySel('ordersTable', { timeout: 200000 }).should('be.visible') // check global state cy.getBySel('ordersTable').find('tbody tr').first().within(() => { cy.contains(/inprogress/i) @@ -122,10 +122,10 @@ describe('Check order global states', { cy.changeSessionTo('SELLER ORG') cy.visit('/product-orders') cy.wait('@getOrders') - cy.wait(500) - cy.getBySel('asProviderTab').click() - cy.wait('@getOrders') cy.getBySel('ordersTable').should('be.visible') + cy.getBySel('asProviderTab').should('be.visible').click() + cy.wait('@getProviderOrders') + cy.getBySel('ordersTable', { timeout: 200000 }).should('be.visible') // check global state cy.getBySel('ordersTable').find('tbody tr').first().within(() => { cy.contains(/partial/i) diff --git a/cypress/e2e/edges/global-states/test6.cy.ts b/cypress/e2e/edges/global-states/test6.cy.ts index 8dc85d3..8a205f1 100644 --- a/cypress/e2e/edges/global-states/test6.cy.ts +++ b/cypress/e2e/edges/global-states/test6.cy.ts @@ -58,9 +58,9 @@ describe('Check order global states', { cy.changeSessionTo('SELLER ORG') cy.visit('/product-orders') cy.wait('@getOrders') - cy.wait(500) - cy.getBySel('asProviderTab').click() - cy.wait('@getOrders') + cy.getBySel('ordersTable').should('be.visible') + cy.getBySel('asProviderTab').should('be.visible').click() + cy.wait('@getProviderOrders') cy.getBySel('ordersTable').should('be.visible') // check global state cy.getBySel('ordersTable').find('tbody tr').first().within(() => { @@ -122,9 +122,9 @@ describe('Check order global states', { cy.changeSessionTo('SELLER ORG') cy.visit('/product-orders') cy.wait('@getOrders') - cy.wait(500) - cy.getBySel('asProviderTab').click() - cy.wait('@getOrders') + cy.getBySel('ordersTable').should('be.visible') + cy.getBySel('asProviderTab').should('be.visible').click() + cy.wait('@getProviderOrders') cy.getBySel('ordersTable').should('be.visible') // check global state cy.getBySel('ordersTable').find('tbody tr').first().within(() => { diff --git a/cypress/e2e/edges/mod-order.cy.ts b/cypress/e2e/edges/mod-order.cy.ts index 8fe24ea..931b134 100644 --- a/cypress/e2e/edges/mod-order.cy.ts +++ b/cypress/e2e/edges/mod-order.cy.ts @@ -189,14 +189,16 @@ describe('Product Modification Order E2E', { cy.wait('@getBilling') cy.wait(2000) + cy.deferPaymentRedirect() cy.getBySel('checkout').should('be.visible').should('not.be.disabled').click() cy.wait('@createOrder') - cy.wait('@getOrders') + cy.waitForOrdersBeforePayment() // Confirm payment cy.intercept('**/charging/api/orderManagement/orders/confirm/').as('checkin') - cy.completePayment() + cy.completePayment({ recurring: true }) cy.wait('@checkin') + cy.waitForOrdersAfterPayment() // ============================================ // Verify initial purchase: order completed + invoice @@ -264,6 +266,7 @@ describe('Product Modification Order E2E', { cy.get('.backdrop-blur-sm').should('be.visible').within(() => { cy.get('app-billing-address').first().click() // Click confirm modify button + cy.deferPaymentRedirect() cy.get('button').contains(/confirm|Confirm/i).click() }) cy.wait('@createOrder') @@ -272,7 +275,7 @@ describe('Product Modification Order E2E', { // Verify modification: checkin + order + invoice // ============================================ cy.intercept('**/charging/api/orderManagement/orders/confirm/').as('checkinModify') - cy.completePayment() + cy.completePayment({ recurring: true }) cy.wait('@checkinModify') cy.visit('/product-orders') diff --git a/cypress/e2e/edges/multi-price-billing.cy.ts b/cypress/e2e/edges/multi-price-billing.cy.ts index af0b7ec..a64a341 100644 --- a/cypress/e2e/edges/multi-price-billing.cy.ts +++ b/cypress/e2e/edges/multi-price-billing.cy.ts @@ -3,6 +3,7 @@ import { updateOffering, clickLoadMoreUntilGone, createOffering, + selectProcurementMode, } from '../../support/form-helpers' /** @@ -43,6 +44,7 @@ describe('Multi-Price Component Billing Edge Cases', { cy.intercept('GET', '**/ordering/productOrder*').as('getOrders') cy.intercept('GET', '**/account/billingAccount*').as('getBilling') cy.intercept('GET', '**/shoppingCart/item/').as('cartItem') + cy.intercept('GET', '**/paymentInfo').as('getPaymentInfo') // ============================================ // Verify that catalog and product spec exist (from happy journey test) @@ -112,7 +114,7 @@ describe('Multi-Price Component Billing Edge Cases', { cy.getBySel('offerNext').click() // Step 1.7: Procurement - Set to automatic - cy.getBySel('procurement').select('automatic') + selectProcurementMode('automatic') cy.getBySel('offerNext').click() // Step 1.8: Finish @@ -165,14 +167,16 @@ describe('Multi-Price Component Billing Edge Cases', { cy.wait(2000) cy.wait('@getBilling') cy.wait(2000) + cy.deferPaymentRedirect() cy.getBySel('checkout').should('be.visible').should('not.be.disabled').click() cy.wait('@createOrder') - cy.wait('@getOrders') + cy.waitForOrdersBeforePayment() // Complete payment simulation cy.intercept('**/charging/api/orderManagement/orders/confirm/').as('checkin') - cy.completePayment() + cy.completePayment({ recurring: true }) cy.wait('@checkin') + cy.waitForOrdersAfterPayment() // ============================================ // Step 6: Verify Customer Bill and ACBRs @@ -207,6 +211,7 @@ describe('Multi-Price Component Billing Edge Cases', { cy.intercept('GET', '**/ordering/productOrder*').as('getOrders') cy.intercept('GET', '**/account/billingAccount*').as('getBilling') cy.intercept('GET', '**/shoppingCart/item/').as('cartItem') + cy.intercept('GET', '**/paymentInfo').as('getPaymentInfo') // Verify catalog and product spec exist cy.visit('/my-offerings') @@ -265,7 +270,7 @@ describe('Multi-Price Component Billing Edge Cases', { cy.getBySel('savePricePlan').click() cy.getBySel('offerNext').click() - cy.getBySel('procurement').select('automatic') + selectProcurementMode('automatic') cy.getBySel('offerNext').click() cy.getBySel('offerFinish').click() @@ -274,6 +279,7 @@ describe('Multi-Price Component Billing Edge Cases', { clickLoadMoreUntilGone() updateOffering({ name: offeringName, status: 'launched' }) + cy.visit('/my-offerings') cy.getBySel('offerSection').click() clickLoadMoreUntilGone() cy.getBySel('offers').contains(offeringName).should('be.visible').parent().contains('Launched') @@ -302,13 +308,15 @@ describe('Multi-Price Component Billing Edge Cases', { cy.wait(2000) cy.wait('@getBilling') cy.wait(2000) + cy.deferPaymentRedirect() cy.getBySel('checkout').should('be.visible').should('not.be.disabled').click() cy.wait('@createOrder') - cy.wait('@getOrders') + cy.waitForOrdersBeforePayment() cy.intercept('**/charging/api/orderManagement/orders/confirm/').as('checkin') - cy.completePayment() + cy.completePayment({ recurring: true }) cy.wait('@checkin') + cy.waitForOrdersAfterPayment() cy.getBySel('ordersTable').should('be.visible') cy.getBySel('ordersTable').contains('completed') @@ -404,13 +412,15 @@ describe('Multi-Price Component Billing Edge Cases', { cy.wait(2000) cy.wait('@getBilling') cy.wait(2000) + cy.deferPaymentRedirect() cy.getBySel('checkout').should('be.visible').should('not.be.disabled').click() cy.wait('@createOrder') - cy.wait('@getOrders') + cy.waitForOrdersBeforePayment() cy.intercept('**/charging/api/orderManagement/orders/confirm/').as('checkin') - cy.completePayment() + cy.completePayment({ recurring: true }) cy.wait('@checkin') + cy.waitForOrdersAfterPayment() cy.getBySel('ordersTable').should('be.visible') cy.getBySel('ordersTable').contains('completed') diff --git a/cypress/e2e/edges/procurement.cy.ts b/cypress/e2e/edges/procurement.cy.ts index 074f500..054b719 100644 --- a/cypress/e2e/edges/procurement.cy.ts +++ b/cypress/e2e/edges/procurement.cy.ts @@ -310,9 +310,10 @@ describe('Payment Automatic with Manual Procurement E2E', { cy.wait(2000) cy.wait('@getBilling') cy.wait(2000) + cy.deferPaymentRedirect() cy.getBySel('checkout').should('be.visible').should('not.be.disabled').click() cy.wait('@createOrder') - cy.wait('@getOrders') + cy.waitForOrdersBeforePayment() // ============================================ // Step 6: Verify order is in inProgress state (payment done, waiting for manual procurement) @@ -320,6 +321,7 @@ describe('Payment Automatic with Manual Procurement E2E', { cy.intercept('**/charging/api/orderManagement/orders/confirm/').as('checkin') cy.completePayment() cy.wait('@checkin') + cy.waitForOrdersAfterPayment() cy.getBySel('ordersTable').should('be.visible') // For manual procurement, get the most recent order (first row in tbody) diff --git a/cypress/e2e/edges/transfer-bank.cy.ts b/cypress/e2e/edges/transfer-bank.cy.ts index d70b216..5cc09b2 100644 --- a/cypress/e2e/edges/transfer-bank.cy.ts +++ b/cypress/e2e/edges/transfer-bank.cy.ts @@ -10,8 +10,12 @@ import { * Creates an offering, buys it as buyer, intercepts the order id, * then settles the customer bill directly via API (bypassing billing-server) * and verifies the order completes and product appears in inventory. + * + * This scenario is specific to the DPAS pending-payment flow. */ -describe('Manual Bill Settle Edge Case', { +const describeForDpas = Cypress.env('PAYMENT_METHOD') === 'dpas' ? describe : describe.skip + +describeForDpas('Manual Bill Settle Edge Case', { viewportHeight: 1080, viewportWidth: 1920, }, () => { @@ -148,6 +152,7 @@ describe('Manual Bill Settle Edge Case', { cy.wait(2000) cy.wait('@getBilling') cy.wait(2000) + cy.deferPaymentRedirect() cy.getBySel('checkout').should('be.visible').should('not.be.disabled').click() // Intercept order creation to capture the order id diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 6cf1dea..d02961b 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -62,30 +62,141 @@ Cypress.Commands.add('closeFeedbackModalIfVisible', () => { }) const BILLING_SERVER_URL = 'http://localhost:4201' +const PAYMENT_METHOD = Cypress.env('PAYMENT_METHOD') +const IS_REDSYS = PAYMENT_METHOD === 'redsys' +const REDSYS_ORIGIN = Cypress.env('REDSYS_ORIGIN') +const REDSYS_AUTH_ORIGIN = Cypress.env('REDSYS_AUTH_ORIGIN') +let deferredPaymentUrl: string | undefined // Payment gateway endpoints are namespaced under /stripe for the Stripe client; -// the DPAS client uses the unprefixed endpoints. -const PAYMENT_PREFIX = Cypress.env('PAYMENT_METHOD') === 'dpas' ? '' : '/stripe' +// the DPAS and Redsys clients use the unprefixed endpoints. +const PAYMENT_PREFIX = ['dpas', 'redsys'].includes(PAYMENT_METHOD) ? '' : '/stripe' // Reset the mock billing-server's payment gateway state (Stripe by default) Cypress.Commands.add('clearBilling', () => { + if (IS_REDSYS) { + return + } + cy.request({ url: `${BILLING_SERVER_URL}${PAYMENT_PREFIX}/clear`, method: 'POST' }).then((response) => { expect(response.status).to.eq(200) }) }) +// Keep the browser on the frontend while a test inspects the unpaid order. +// The redirect is resumed by completePayment() or cancelPayment(). +Cypress.Commands.add('deferPaymentRedirect', () => { + deferredPaymentUrl = undefined + + cy.intercept('POST', '**/ordering/productOrder', (request) => { + request.continue((response) => { + const redirectHeader = Object.keys(response.headers).find( + (header) => header.toLowerCase() === 'x-redirect-url' + ) + const redirectUrl = redirectHeader + ? response.headers[redirectHeader] + : undefined + + if (!redirectHeader || typeof redirectUrl !== 'string' || !redirectUrl) { + throw new Error('Payment redirect was not returned by charging') + } + + deferredPaymentUrl = redirectUrl + delete response.headers[redirectHeader] + }) + }).as('createOrder') +}) + +const takeDeferredPaymentUrl = () => { + const paymentUrl = deferredPaymentUrl + deferredPaymentUrl = undefined + return paymentUrl +} + +// The mock gateways redirect through /checkin before the orders page is loaded. +// Redsys must be completed first because its external payment form is already open. +Cypress.Commands.add('waitForOrdersBeforePayment', () => { + if (!IS_REDSYS) { + cy.wait('@getOrders') + } +}) + +Cypress.Commands.add('waitForOrdersAfterPayment', () => { + if (IS_REDSYS) { + cy.wait('@getOrders') + } +}) + // Complete the pending checkout, redirecting back to the order's success url -Cypress.Commands.add('completePayment', () => { - cy.visit(`${BILLING_SERVER_URL}${PAYMENT_PREFIX}/checkin`) +Cypress.Commands.add('completePayment', (options: { recurring?: boolean } = {}) => { + const paymentUrl = takeDeferredPaymentUrl() + + if (IS_REDSYS) { + if (options.recurring) { + cy.origin(REDSYS_AUTH_ORIGIN, () => { + Cypress.on('uncaught:exception', (err) => { + if (err.message.includes('$ is not defined')) { + return false + } + }) + }) + } + + if (paymentUrl) { + cy.visit(paymentUrl) + } + + cy.origin(REDSYS_ORIGIN, () => { + cy.get('#card-number').should('be.visible').type('4548814479727229') + cy.get('#card-expiration').type('1249') + cy.get('#card-cvv').type('123') + cy.get('body').then(($body) => { + if ($body.find('#cardholder-name').length) { + cy.get('#cardholder-name').type('E2E TEST') + } + }) + cy.get('#divImgAceptar').should('be.visible').click() + }) + + if (options.recurring) { + cy.origin(REDSYS_AUTH_ORIGIN, () => { + cy.get('input[name="option"][value="1"]').should('be.visible').check() + cy.get('#boton').should('be.visible').click() + }) + } + + return cy.origin(REDSYS_ORIGIN, () => { + cy.get('input[type="button"][lngid="continuar"]').should('be.visible').click() + }) + } + + cy.visit(paymentUrl || `${BILLING_SERVER_URL}${PAYMENT_PREFIX}/checkin`) }) // Cancel the pending checkout, redirecting back to the order's cancel url Cypress.Commands.add('cancelPayment', () => { + const paymentUrl = takeDeferredPaymentUrl() + + if (IS_REDSYS) { + if (paymentUrl) { + cy.visit(paymentUrl) + } + + return cy.origin(REDSYS_ORIGIN, () => { + cy.get('#divImgCancelar').should('be.visible').click() + cy.get('input[type="button"][lngid="continuar"]').should('be.visible').click() + }) + } + cy.visit(`${BILLING_SERVER_URL}${PAYMENT_PREFIX}/bad-checkin`) }) // Mark the next checkout as left pending instead of completed Cypress.Commands.add('setPaymentPending', () => { + if (IS_REDSYS) { + return + } + cy.request({ url: `${BILLING_SERVER_URL}${PAYMENT_PREFIX}/set-pending`, method: 'GET' }).then((response) => { expect(response.status).to.eq(200) }) diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index 3515f17..3ac716e 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -1,6 +1,12 @@ // Import commands import './commands' +afterEach(() => { + if (Cypress.env('PAYMENT_METHOD') === 'redsys') { + cy.visit('/') + } +}) + // Extend Cypress namespace with custom commands declare global { namespace Cypress { @@ -38,11 +44,31 @@ declare global { */ clearBilling(): Chainable + /** + * Save the next payment redirect so the test can complete or cancel it later. + * @example cy.deferPaymentRedirect() + */ + deferPaymentRedirect(): Chainable + + /** + * Wait for orders before payment when using a mock payment gateway. + * Redsys skips this wait so Cypress can complete its external form first. + * @example cy.waitForOrdersBeforePayment() + */ + waitForOrdersBeforePayment(): Chainable + + /** + * Wait for orders after payment when using Redsys. + * @example cy.waitForOrdersAfterPayment() + */ + waitForOrdersAfterPayment(): Chainable + /** * Complete the pending checkout, redirecting back to the order's success url - * @example cy.completePayment() + * @param options - Set recurring for Redsys payments that require 3DS authentication + * @example cy.completePayment({ recurring: true }) */ - completePayment(): Chainable + completePayment(options?: { recurring?: boolean }): Chainable /** * Cancel the pending checkout, redirecting back to the order's cancel url diff --git a/cypress/support/form-helpers.ts b/cypress/support/form-helpers.ts index 3aca918..06ef7f8 100644 --- a/cypress/support/form-helpers.ts +++ b/cypress/support/form-helpers.ts @@ -59,6 +59,13 @@ export interface UpdateOfferingParams { status: string } +export function selectProcurementMode(procurement: string): void { + cy.wait('@getPaymentInfo') + cy.getBySel('procurement') + .select(procurement) + .should('have.value', procurement) +} + export interface BillingParams { title: string country: string @@ -284,6 +291,7 @@ export function createOffering({ procurement }: OfferingParams): void { cy.intercept('GET', '**/usage/usageSpecification?*').as('usageGET') + cy.intercept('GET', '**/paymentInfo').as('getPaymentInfo') cy.visit('/my-offerings') cy.getBySel('offerSection').click() cy.getBySel('newOffering').click() @@ -347,7 +355,7 @@ export function createOffering({ cy.getBySel('offerNext').click() // Step 7: procurement info - cy.getBySel('procurement').select(procurement) + selectProcurementMode(procurement) cy.getBySel('offerNext').click() // Step 8: Finish @@ -405,12 +413,16 @@ export function clickLoadMoreUntilGone(maxClicks = 10, offering: boolean = false cy.wait(2000) cy.get('body').then($body => { const $btn = $body.find('[data-cy="loadMore"]:visible') + const $loading = $body.find('[data-cy="loadMoreLoading"]:visible') if ($btn.length > 0) { cy.wrap($btn).click() if (offering) { cy.wait('@offeringList') } clickIfExists(remainingClicks - 1) + } else if ($loading.length > 0) { + cy.getBySel('loadMoreLoading', { timeout: 120000 }).should('not.exist') + clickIfExists(remainingClicks) } else if (retries > 0) { // Retry: button might still be loading clickIfExists(remainingClicks, retries - 1) diff --git a/cypress/support/global-state-flows.ts b/cypress/support/global-state-flows.ts index 54f0751..f09583d 100644 --- a/cypress/support/global-state-flows.ts +++ b/cypress/support/global-state-flows.ts @@ -103,7 +103,11 @@ export function setupGlobalStateBeforeEach(params: GlobalStateSetupParams & { au cy.intercept('POST', '**/shoppingCart/item/').as('postCart') cy.intercept('POST', '**/ordering/productOrder').as('createOrder') cy.intercept('GET', '**/ordering/productOrder*').as('getOrders') + cy.intercept('GET', '**/ordering/productOrder?*relatedParty.role=seller*').as('getProviderOrders') cy.intercept('GET', '**/account/billingAccount*').as('getBilling') + cy.intercept('GET', '**/catalog/productOffering?*', (request) => { + delete request.headers['if-none-match'] + }) cy.loginAsAdmin() cy.on('uncaught:exception', (err) => { @@ -162,7 +166,7 @@ export function setupGlobalStateBeforeEach(params: GlobalStateSetupParams & { au cy.getBySel('shoppingCart').click() cy.getBySel('cartPurchase').click() - cy.intercept('POST', '**/ordering/productOrder').as('createOrder') + cy.deferPaymentRedirect() cy.wait(2000) cy.wait('@getBilling') @@ -176,9 +180,9 @@ export function setupGlobalStateBeforeEach(params: GlobalStateSetupParams & { au cy.visit('/product-orders') cy.wait('@getOrders') - cy.getBySel('asProviderTab').click() - cy.wait('@getOrders') - cy.wait('@getOrders') + cy.getBySel('ordersTable').should('be.visible') + cy.getBySel('asProviderTab').should('be.visible').click() + cy.wait('@getProviderOrders') cy.wait(2000) cy.getBySel('ordersTable', { timeout: 60000 }).should('be.visible') diff --git a/docker-speed/Dockerfile.charging b/docker-speed/Dockerfile.charging index 108e56d..d77b0bc 100644 --- a/docker-speed/Dockerfile.charging +++ b/docker-speed/Dockerfile.charging @@ -1,4 +1,4 @@ -FROM ubuntu:20.04 +FROM ubuntu:24.04 LABEL MAINTAINER="Future Internet Consulting and Development Solutions S.L." @@ -12,9 +12,23 @@ WORKDIR /opt/business-ecosystem-charging-backend/ COPY requirements.txt requirements.txt COPY dev-requirements.txt dev-requirements.txt -RUN apt-get update && apt-get -y install python3 python3-pip gcc git wkhtmltopdf xvfb wget cron && \ - pip3 install -r requirements.txt && pip3 install -r dev-requirements.txt && \ - mkdir -p ./src/media/bills ./src/media/assets ./src/plugins +RUN apt-get update && apt-get -y install --no-install-recommends \ + cron \ + gcc \ + git \ + software-properties-common \ + wget \ + wkhtmltopdf \ + xvfb && \ + add-apt-repository -y ppa:deadsnakes/ppa && \ + apt-get update && \ + apt-get -y install --no-install-recommends python3.9 python3.9-distutils && \ + wget -O /tmp/get-pip.py https://bootstrap.pypa.io/pip/3.9/get-pip.py && \ + python3.9 /tmp/get-pip.py && \ + python3.9 -m pip install --no-cache-dir --ignore-installed -r requirements.txt && \ + python3.9 -m pip install --no-cache-dir --ignore-installed -r dev-requirements.txt && \ + mkdir -p ./src/media/bills ./src/media/assets ./src/plugins && \ + rm -rf /tmp/get-pip.py /var/lib/apt/lists/* COPY src/ src/ @@ -23,4 +37,3 @@ COPY ./docker/entrypoint.sh / EXPOSE 8006 ENTRYPOINT ["/entrypoint.sh"] - diff --git a/proxy-docker/docker-compose.yml b/proxy-docker/docker-compose.yml index 64f84c3..5cf2dec 100755 --- a/proxy-docker/docker-compose.yml +++ b/proxy-docker/docker-compose.yml @@ -34,6 +34,7 @@ services: # Service Configuration - BAE_LP_PORT=8004 - BAE_LP_HOST=0.0.0.0 + - BAE_LP_CORS_EXPOSE_REDIRECT=true # Charging Backend Connection - BAE_LP_CHARGING_SERVER=http://charging.docker:8006 diff --git a/reload.sh b/reload.sh index 7ff554b..a42086c 100755 --- a/reload.sh +++ b/reload.sh @@ -19,6 +19,10 @@ cd proxy-docker docker compose down cd .. +cd billing-server +docker compose down +cd .. + cd proxy-repo echo -e "\033[35mrecreating proxy image\033[0m" docker build -t proxy-system-dev -f docker/Dockerfile . @@ -36,3 +40,8 @@ cd proxy-docker docker compose up -d cd .. +# Start Billing Server +echo -e "\033[35mStarting Billing Server...\033[0m" +cd billing-server +docker compose up -d --build +cd .. From 84380a6c04ab194fff8b958efc19c7561145ec39 Mon Sep 17 00:00:00 2001 From: SHENGXING LU Date: Tue, 4 Aug 2026 23:52:48 +0200 Subject: [PATCH 2/2] redsys --- charging-docker/docker-compose.yml | 2 +- cypress/e2e/edges/mod-order.cy.ts | 5 +++-- cypress/e2e/edges/multi-price-billing.cy.ts | 8 ++++++-- cypress/support/form-helpers.ts | 12 ++++++++++-- cypress/support/global-state-flows.ts | 17 ++++++++++++----- 5 files changed, 32 insertions(+), 12 deletions(-) diff --git a/charging-docker/docker-compose.yml b/charging-docker/docker-compose.yml index 916afbd..8b450d1 100644 --- a/charging-docker/docker-compose.yml +++ b/charging-docker/docker-compose.yml @@ -61,7 +61,7 @@ services: # Payment Configuration - BAE_CB_PAYMENT_METHOD=redsys - BAE_CB_REDSYS_MERCHANT_CODE=263100000 - - BAE_CB_REDSYS_TERMINAL=068 + - BAE_CB_REDSYS_TERMINAL=005 - BAE_CB_REDSYS_SECRET_KEY=sq7HjrUOBfKmC576ILgskD5srU870gJ7 - BAE_CB_BILLING_ENGINE=local - BAE_CB_DPAS_CLIENT_API_URL=http://host.docker.internal:4201/api/payment-start diff --git a/cypress/e2e/edges/mod-order.cy.ts b/cypress/e2e/edges/mod-order.cy.ts index 78fb8de..3c7e116 100644 --- a/cypress/e2e/edges/mod-order.cy.ts +++ b/cypress/e2e/edges/mod-order.cy.ts @@ -177,6 +177,7 @@ describe('Product Modification Order E2E', { cy.wait('@getBilling') cy.get('body').then($body => { if ($body.find('[data-cy="billingTitle"]').length > 0) { + cy.intercept('POST', '**/account/billingAccount').as('saveBilling') createCheckoutBilling({ title: 'ModOrder Billing', country: 'ES', @@ -187,11 +188,11 @@ describe('Product Modification Order E2E', { email: 'buyer@test.com', phoneNumber: '600123456' }) - cy.intercept('POST', '**/account/billingAccount').as('saveBilling') + cy.wait('@saveBilling') + cy.wait('@getBilling') } }) - cy.wait('@getBilling') cy.wait(2000) cy.deferPaymentRedirect() cy.getBySel('checkout').should('be.visible').should('not.be.disabled').click() diff --git a/cypress/e2e/edges/multi-price-billing.cy.ts b/cypress/e2e/edges/multi-price-billing.cy.ts index d559a1e..6d024d6 100644 --- a/cypress/e2e/edges/multi-price-billing.cy.ts +++ b/cypress/e2e/edges/multi-price-billing.cy.ts @@ -69,7 +69,9 @@ describe('Multi-Price Component Billing Edge Cases', { // Step 1.1: Basic Info cy.getBySel('offerName').should('be.visible').type(offeringName) cy.getBySel('textArea').type('Offering with recurring and recurring-prepaid components') - cy.getBySel('offerNext').click() + waitForInitialPaginatedList('**/catalog/productSpecification?*', () => { + cy.getBySel('offerNext').click() + }) // Step 1.2: Select Product Spec cy.getBySel('prodSpecs').contains(productSpecName).click() @@ -236,7 +238,9 @@ describe('Multi-Price Component Billing Edge Cases', { cy.getBySel('offerName').should('be.visible').type(offeringName) cy.getBySel('textArea').type('Offering with one-time and recurring-prepaid') - cy.getBySel('offerNext').click() + waitForInitialPaginatedList('**/catalog/productSpecification?*', () => { + cy.getBySel('offerNext').click() + }) cy.getBySel('prodSpecs').contains(productSpecName).click() cy.getBySel('offerNext').click() diff --git a/cypress/support/form-helpers.ts b/cypress/support/form-helpers.ts index 1e8fd55..86be077 100644 --- a/cypress/support/form-helpers.ts +++ b/cypress/support/form-helpers.ts @@ -451,6 +451,7 @@ export function updateOffering({ name, status }: UpdateOfferingParams): void { function setupRequestTracker(apiPattern: string | string[], idleMs = 300) { let pendingRequests = 0 let lastActivity = Date.now() + let seenRequest = false let requestId = 0 const patterns = Array.isArray(apiPattern) ? apiPattern : [apiPattern] @@ -460,6 +461,7 @@ function setupRequestTracker(apiPattern: string | string[], idleMs = 300) { cy.intercept('GET', pattern, (req) => { const id = requestId++ const finished = new Set() + seenRequest = true pendingRequests++ lastActivity = Date.now() @@ -476,7 +478,8 @@ function setupRequestTracker(apiPattern: string | string[], idleMs = 300) { return { waitForIdle: () => { - cy.wrap(null).should(() => { + cy.wrap(null, { timeout: Cypress.config('responseTimeout') }).should(() => { + expect(seenRequest, `${label} has seen requests`).to.eq(true) expect(pendingRequests, `${label} pending requests`).to.eq(0) expect(Date.now() - lastActivity, `${label} idle time`).to.be.greaterThan(idleMs) }) @@ -720,7 +723,9 @@ export function createResourceSpec({ name, description, characteristics = [] }: cy.getBySel('resSpecNext').click() // Step 3: Finish - cy.getBySel('resSpecFinish').should('be.enabled').click() + waitForInitialPaginatedList('**/resource/resourceSpecification?*', () => { + cy.getBySel('resSpecFinish').should('be.enabled').click() + }) // Close feedback modal if it appears cy.closeFeedbackModalIfVisible() @@ -733,6 +738,8 @@ export function createResourceSpec({ name, description, characteristics = [] }: * Update resource spec status */ export function updateResourceSpecStatus({ name, status }: UpdateResourceSpecStatusParams): void { + cy.intercept('PATCH', '**/resource/resourceSpecification/**').as('patchResourceSpec') + cy.getBySel('resSpecTable').contains(name).parents('[data-cy="resSpecRow"]').find('[data-cy="resourceSpecEdit"]').click() if (status === 'launched') { @@ -744,6 +751,7 @@ export function updateResourceSpecStatus({ name, status }: UpdateResourceSpecSta cy.getBySel('resSpecUpdateNext').click() // Go to Summary step cy.getBySel('resourceSpecUpdate').click() + cy.wait('@patchResourceSpec').its('response.statusCode').should('eq', 200) // Close feedback modal if it appears cy.closeFeedbackModalIfVisible() diff --git a/cypress/support/global-state-flows.ts b/cypress/support/global-state-flows.ts index 6637851..041a05b 100644 --- a/cypress/support/global-state-flows.ts +++ b/cypress/support/global-state-flows.ts @@ -156,18 +156,25 @@ export function setupGlobalStateBeforeEach(params: GlobalStateSetupParams & { au cy.wait('@postCart') //MANUAL + const cartReadyTracker = createRequestTracker([ + '**/shoppingCart/item/', + '**/catalog/productOffering/**', + '**/catalog/productSpecification/**', + ], 500) openOfferingDrawer(offeringManualName) // Select the drawer that contains the manual offering name - cy.contains('[data-cy="toCartDrawer"]', `Adding ${offeringManualName} to cart`).within(() => { - cy.contains(HAPPY_JOURNEY.pricePlan.name).click() - cy.getBySel('acceptTermsCheckbox').click() - cy.getBySel('addToCart').click() + cartReadyTracker.waitForAction(() => { + cy.contains('[data-cy="toCartDrawer"]', `Adding ${offeringManualName} to cart`).within(() => { + cy.contains(HAPPY_JOURNEY.pricePlan.name).click() + cy.getBySel('acceptTermsCheckbox').click() + cy.getBySel('addToCart').click() + }) }) cy.wait('@postOrder') cy.wait('@postCart') cy.getBySel('shoppingCart').click() - cy.getBySel('cartPurchase').click() + cy.getBySel('cartPurchase').should('be.visible').should('not.be.disabled').click() cy.deferPaymentRedirect()