From 6f00a0ab93bf969eca016d3863faf9ede360cfd0 Mon Sep 17 00:00:00 2001 From: Raphael Kabo Date: Tue, 5 Dec 2023 16:27:51 +0000 Subject: [PATCH 1/7] update gitignore with cypress env file --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index eedaf810d..041e56516 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ manage-frontend.zip test-report.xml cypress/screenshots +cypress.env.json From 5af362c2f4821f539dac7e5f08dd4cb68f58c859 Mon Sep 17 00:00:00 2001 From: Raphael Kabo Date: Tue, 5 Dec 2023 16:32:51 +0000 Subject: [PATCH 2/7] feat: set up E2E Cypress tests --- .github/workflows/cypress-e2e.yml | 64 +++++++++ .../{cypress.yml => cypress-mocked.yml} | 9 +- cypress.config.ts | 7 +- cypress/cypress-nginx.conf | 131 ++++++++++++++++++ cypress/lib/signInOkta.ts | 24 ++++ cypress/tests/e2e/e2e.cy.ts | 97 +++++++++++++ .../mocked}/parallel-1/accountOverview.cy.ts | 6 +- .../parallel-1/updateContributionAmount.cy.ts | 6 +- .../parallel-1/updatePaymentDetails.cy.ts | 12 +- .../parallel-2/cancelContribution.cy.ts | 6 +- .../mocked}/parallel-2/cancelGW.cy.ts | 6 +- .../parallel-2/cancelSupporterPlus.cy.ts | 6 +- .../mocked}/parallel-2/digisubSave.cy.ts | 6 +- .../mocked}/parallel-2/membershipSave.cy.ts | 8 +- .../mocked}/parallel-2/productSwitch.cy.ts | 8 +- .../mocked}/parallel-2/upgradeSupport.cy.ts | 8 +- .../mocked}/parallel-3/holidayStops.cy.ts | 8 +- .../mocked}/parallel-4/deliveryAddress.cy.ts | 6 +- .../mocked}/parallel-4/deliveryRecords.cy.ts | 14 +- .../mocked}/parallel-5/membership.cy.ts | 6 +- .../mocked}/parallel-5/patron.cy.ts | 6 +- .../parallel-6/emailAndMarketing.cy.ts | 16 +-- .../parallel-6/identitySettingsForm.cy.ts | 4 +- nginx/identity-frontend-CODE-fallback.conf | 88 ++++++++++-- package.json | 10 +- server/log.ts | 3 + server/oktaConfig.ts | 3 + 27 files changed, 475 insertions(+), 93 deletions(-) create mode 100644 .github/workflows/cypress-e2e.yml rename .github/workflows/{cypress.yml => cypress-mocked.yml} (77%) create mode 100644 cypress/cypress-nginx.conf create mode 100644 cypress/lib/signInOkta.ts create mode 100644 cypress/tests/e2e/e2e.cy.ts rename cypress/{e2e => tests/mocked}/parallel-1/accountOverview.cy.ts (81%) rename cypress/{e2e => tests/mocked}/parallel-1/updateContributionAmount.cy.ts (91%) rename cypress/{e2e => tests/mocked}/parallel-1/updatePaymentDetails.cy.ts (95%) rename cypress/{e2e => tests/mocked}/parallel-2/cancelContribution.cy.ts (96%) rename cypress/{e2e => tests/mocked}/parallel-2/cancelGW.cy.ts (93%) rename cypress/{e2e => tests/mocked}/parallel-2/cancelSupporterPlus.cy.ts (92%) rename cypress/{e2e => tests/mocked}/parallel-2/digisubSave.cy.ts (91%) rename cypress/{e2e => tests/mocked}/parallel-2/membershipSave.cy.ts (94%) rename cypress/{e2e => tests/mocked}/parallel-2/productSwitch.cy.ts (95%) rename cypress/{e2e => tests/mocked}/parallel-2/upgradeSupport.cy.ts (88%) rename cypress/{e2e => tests/mocked}/parallel-3/holidayStops.cy.ts (97%) rename cypress/{e2e => tests/mocked}/parallel-4/deliveryAddress.cy.ts (95%) rename cypress/{e2e => tests/mocked}/parallel-4/deliveryRecords.cy.ts (96%) rename cypress/{e2e => tests/mocked}/parallel-5/membership.cy.ts (84%) rename cypress/{e2e => tests/mocked}/parallel-5/patron.cy.ts (87%) rename cypress/{e2e => tests/mocked}/parallel-6/emailAndMarketing.cy.ts (78%) rename cypress/{e2e => tests/mocked}/parallel-6/identitySettingsForm.cy.ts (89%) diff --git a/.github/workflows/cypress-e2e.yml b/.github/workflows/cypress-e2e.yml new file mode 100644 index 000000000..f0b3332ff --- /dev/null +++ b/.github/workflows/cypress-e2e.yml @@ -0,0 +1,64 @@ +name: manage-frontend cypress (E2E) +on: + pull_request: + branches: + - main + workflow_dispatch: + +jobs: + cypress: + name: Manage-frontend Cypress (E2E) + runs-on: ubuntu-latest + + permissions: + id-token: write + contents: read + + steps: + - uses: aws-actions/configure-aws-credentials@v2 + with: + role-to-assume: ${{ secrets.MANAGE_FRONTEND_IAM_ROLE }} + aws-region: eu-west-1 + + - uses: actions/checkout@v4 + - uses: actions/setup-node@v3 + with: + cache: yarn + + - name: Setup OS, Nginx, and Certs + run: | + sudo apt-get update -y + sudo apt-get install -y libnss3-tools + sudo service nginx restart + wget -q https://github.com/FiloSottile/mkcert/releases/download/v1.4.3/mkcert-v1.4.3-linux-amd64 + wget -q https://github.com/guardian/dev-nginx/releases/latest/download/dev-nginx.tar.gz + sudo cp mkcert-v1.4.3-linux-amd64 /usr/local/bin/mkcert + sudo chmod +x /usr/local/bin/mkcert + sudo mkdir -p /usr/local/bin/dev-nginx + sudo tar -xzf dev-nginx.tar.gz -C /usr/local + sudo chmod +x /usr/local/bin/dev-nginx + sudo dev-nginx setup-cert "profile.thegulocal.com" + sudo dev-nginx setup-cert "manage.thegulocal.com" + sudo dev-nginx setup-cert "members-data-api.thegulocal.com" + sudo cp ./cypress/cypress-nginx.conf /etc/nginx/nginx.conf + sudo dev-nginx restart-nginx + + - name: Cypress run + uses: cypress-io/github-action@v6 + env: + CYPRESS_IDAPI_CLIENT_ACCESS_TOKEN: ${{ secrets.IDAPI_CLIENT_ACCESS_TOKEN }} + # Required for the Cypress tests to run as we're unable to verify the created certs + NODE_TLS_REJECT_UNAUTHORIZED: 0 + with: + start: yarn cypress:e2e:server + wait-on: 'https://manage.thegulocal.com' + wait-on-timeout: 30 + quiet: true + browser: chrome + spec: cypress/tests/e2e/*.cy.ts + + - uses: actions/upload-artifact@v3 + if: failure() + with: + name: cypress-screenshots + path: cypress/screenshots diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress-mocked.yml similarity index 77% rename from .github/workflows/cypress.yml rename to .github/workflows/cypress-mocked.yml index 860f2162e..b4f6a41fb 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress-mocked.yml @@ -1,4 +1,4 @@ -name: manage-frontend cypress +name: manage-frontend cypress (mocked) on: push: branches-ignore: @@ -6,7 +6,7 @@ on: jobs: cypress: - name: Manage-frontend Cypress + name: Manage-frontend Cypress (mocked) runs-on: ubuntu-latest permissions: @@ -34,9 +34,10 @@ jobs: env: IDAPI_CLIENT_ACCESS_TOKEN: ${{ secrets.IDAPI_CLIENT_ACCESS_TOKEN }} with: - start: yarn cypress:server + start: yarn cypress:mocked:server wait-on: 'http://localhost:9234, http://localhost:9233' wait-on-timeout: 30 quiet: true browser: chrome - spec: cypress/e2e/parallel-${{ matrix.group }}/*.ts + spec: cypress/tests/mocked/parallel-${{ matrix.group }}/*.cy.ts + config: baseUrl=http://localhost:9234 diff --git a/cypress.config.ts b/cypress.config.ts index 13764d1e9..531d7daa8 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -4,9 +4,7 @@ export default defineConfig({ viewportWidth: 1500, viewportHeight: 860, video: false, - failOnStatusCode: false, chromeWebSecurity: false, - experimentalSessionSupport: true, blockHosts: [ '*ophan.theguardian.com', 'pixel.adsafeprotected.com', @@ -26,11 +24,10 @@ export default defineConfig({ openMode: 0, }, e2e: { - // We've imported your old cypress plugins here. - // You may want to clean this up later by importing these. + specPattern: 'cypress/tests/**/*.cy.{js,jsx,ts,tsx}', setupNodeEvents(on, config) { return require('./cypress/plugins/index.ts')(on, config); }, - baseUrl: 'http://localhost:9234/', + baseUrl: 'https://manage.thegulocal.com', }, }); diff --git a/cypress/cypress-nginx.conf b/cypress/cypress-nginx.conf new file mode 100644 index 000000000..670979c08 --- /dev/null +++ b/cypress/cypress-nginx.conf @@ -0,0 +1,131 @@ +# NGINX Conf file used by Cypress-Nginx Github actions +# so we can run cypress tests against the local nginx server +# with a ssl cert set on the domain + +#user nobody; +worker_processes 1; + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + + sendfile on; + + keepalive_timeout 65; + + # fixes issues for large response headers + proxy_buffer_size 128k; + proxy_buffers 4 256k; + proxy_busy_buffers_size 256k; + + + # manage.thegulocal.com + # ====================== + server { + listen 443 ssl; + server_name manage.thegulocal.com; + proxy_http_version 1.1; # this is essential for chunked responses to work + + ssl_certificate manage.thegulocal.com.crt; + ssl_certificate_key manage.thegulocal.com.key; + ssl_session_timeout 5m; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + + location / { + proxy_pass http://localhost:9234/; + proxy_set_header Host $http_host; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + } + } + + # members-data-api.thegulocal.com + # ====================== + server { + listen 443 ssl; + server_name members-data-api.thegulocal.com; + proxy_http_version 1.1; # this is essential for chunked responses to work + + ssl_certificate members-data-api.thegulocal.com.crt; + ssl_certificate_key members-data-api.thegulocal.com.key; + ssl_session_timeout 5m; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + + location / { + proxy_pass https://members-data-api.code.dev-theguardian.com; + proxy_next_upstream error timeout http_404 non_idempotent; + proxy_set_header "X-Forwarded-Proto" "https"; + proxy_set_header Host members-data-api.code.dev-theguardian.com; + proxy_set_header Accept-Encoding ""; + proxy_hide_header Content-Security-Policy; + + proxy_cookie_domain members-data-api.code.dev-theguardian.com members-data-api.thegulocal.com; + proxy_cookie_domain .code.dev-theguardian.com .thegulocal.com; + + sub_filter_types application/json; + sub_filter_once off; + sub_filter 'members-data-api.code.dev-theguardian.com' 'members-data-api.thegulocal.com'; + } + } + + + # profile.thegulocal.com + # ====================== + server { + listen 443 ssl; + server_name profile.thegulocal.com; + proxy_http_version 1.1; # this is essential for chunked responses to work + + ssl_certificate profile.thegulocal.com.crt; + ssl_certificate_key profile.thegulocal.com.key; + ssl_session_timeout 5m; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; + + # dummy location header for the API + proxy_set_header X-GU-ID-Geolocation ip:$remote_addr,country:GB,city:Leeds; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + location / { + proxy_pass https://profile.code.dev-theguardian.com; + proxy_next_upstream error timeout http_404 non_idempotent; + proxy_set_header "X-Forwarded-Proto" "https"; + proxy_set_header "X-GU-Okta-Env" "profile.code.dev-theguardian.com"; + proxy_set_header Host profile.code.dev-theguardian.com; + proxy_set_header Accept-Encoding ""; + proxy_hide_header Content-Security-Policy; + + proxy_cookie_domain profile.code.dev-theguardian.com profile.thegulocal.com; + proxy_cookie_domain .code.dev-theguardian.com .thegulocal.com; + + sub_filter_types application/json; + sub_filter_once off; + sub_filter 'profile.code.dev-theguardian.com' 'profile.thegulocal.com'; + + ###### + # remove `sid` cookie in requests to Gateway + # save original "Cookie" header value + set $altered_cookie $http_cookie; + # check if the "sid" cookie is present + if ($http_cookie ~ '(.*)(^|;\s)sid=("[^"]*"|[^\s]*[^;]?)(\2|$|;$)(?:;\s)?(.*)') { + # cut "sid" cookie from the string + set $altered_cookie $1$4$5; + } + # hide original "Cookie" header + proxy_hide_header Cookie; + # set "Cookie" header to the new value + proxy_set_header Cookie $altered_cookie; + ###### + } + } +} diff --git a/cypress/lib/signInOkta.ts b/cypress/lib/signInOkta.ts new file mode 100644 index 000000000..163dd9579 --- /dev/null +++ b/cypress/lib/signInOkta.ts @@ -0,0 +1,24 @@ +/** + * Non-mocked sign-in with Gateway using Okta + */ + +export const signInOkta = () => { + // When this function runs, the browser will already be showing the Gateway sign-in page + // because MMA will have redirected to it when it loads the first page of the test. + cy.setCookie('gu-cmp-disabled', 'true', { + domain: '.thegulocal.com', + }); + + // Necessary otherwise we get a 502 error back from MMA for some reason, perhaps a race condition? + // TODO: Make this not suck + cy.wait(1000); + + cy.visit('/'); + cy.createTestUser({ + isUserEmailValidated: true, + })?.then(({ emailAddress, finalPassword }) => { + cy.get('input[name=email]').type(emailAddress); + cy.get('input[name=password]').type(finalPassword); + cy.get('[data-cy="main-form-submit-button"]').click(); + }); +}; diff --git a/cypress/tests/e2e/e2e.cy.ts b/cypress/tests/e2e/e2e.cy.ts new file mode 100644 index 000000000..fadfb959d --- /dev/null +++ b/cypress/tests/e2e/e2e.cy.ts @@ -0,0 +1,97 @@ +import { signInOkta } from '../../lib/signInOkta'; + +describe('E2E with Okta', () => { + beforeEach(() => { + cy.clearAllCookies(); + signInOkta(); + cy.get('h1', { + timeout: 30000, + }).should('contain', 'Account overview'); + }); + + context('account overview tab', () => { + it('should contain an email address', () => { + cy.findByText('Email address', { + timeout: 30000, + }); + }); + }); + + context('profile tab', () => { + it('should contain a username', () => { + cy.visit('/public-settings'); + cy.findByText('Username', { + timeout: 30000, + }); + }); + }); + + context('emails tab', () => { + it('should allow the user to update their email preferences', () => { + cy.visit('/email-prefs'); + cy.findByText('Guardian products and support', { + timeout: 30000, + }).click(); + cy.visit('/email-prefs'); + cy.findByText('Guardian products and support', { + timeout: 30000, + }) + .parents('div') + .get('input[type="checkbox"]') + .should('be.checked'); + }); + + it('should allow the user to unsubscribe from all emails', () => { + cy.visit('/email-prefs'); + cy.findByText('Unsubscribe from all emails', { + timeout: 30000, + }).click(); + cy.visit('/email-prefs'); + cy.get('input[type="checkbox"]', { + timeout: 30000, + }).should('not.be.checked'); + }); + }); + + context('settings tab', () => { + it('should allow the user to update their personal information', () => { + cy.visit('/account-settings'); + cy.findByLabelText('First Name', { + timeout: 30000, + }).clear(); + cy.findByLabelText('Last Name').clear(); + cy.findByLabelText('First Name').type('Test'); + cy.findByLabelText('Last Name').type('User'); + cy.findByText('Save changes').click(); + cy.visit('/account-settings'); + cy.findByLabelText('First Name').should('have.value', 'Test'); + cy.findByLabelText('Last Name').should('have.value', 'User'); + }); + }); + + context('help tab', () => { + it('should allow the user to submit the help centre contact form', () => { + cy.visit('/help'); + cy.findByText( + 'If you still can’t find what you need and want to contact us, check', + { + timeout: 30000, + }, + ) + .parent() + .findByText('here') + .click(); + cy.findByText('Take me to the form').click(); + cy.findByText('Begin form').click(); + cy.findByText('Continue to step 2').click(); + cy.findByLabelText('Full Name').type('Test'); + cy.get('textarea[name="message"]').type('Problem details'); + cy.solveGoogleReCAPTCHA(); + cy.wait(1000); + cy.findByText('Submit').click(); + cy.findByText('Thank you for contacting us', { + timeout: 30000, + }); + }); + }); +}); diff --git a/cypress/e2e/parallel-1/accountOverview.cy.ts b/cypress/tests/mocked/parallel-1/accountOverview.cy.ts similarity index 81% rename from cypress/e2e/parallel-1/accountOverview.cy.ts rename to cypress/tests/mocked/parallel-1/accountOverview.cy.ts index c6425c5ca..37db8d6de 100644 --- a/cypress/e2e/parallel-1/accountOverview.cy.ts +++ b/cypress/tests/mocked/parallel-1/accountOverview.cy.ts @@ -1,6 +1,6 @@ -import { toMembersDataApiResponse } from '../../../client/fixtures/mdapiResponse'; -import { signInAndAcceptCookies } from '../../lib/signInAndAcceptCookies'; -import { singleContributionsAPIResponse } from '../../../client/fixtures/singleContribution'; +import { toMembersDataApiResponse } from '../../../../client/fixtures/mdapiResponse'; +import { signInAndAcceptCookies } from '../../../lib/signInAndAcceptCookies'; +import { singleContributionsAPIResponse } from '../../../../client/fixtures/singleContribution'; describe('single contributions test', () => { beforeEach(() => { diff --git a/cypress/e2e/parallel-1/updateContributionAmount.cy.ts b/cypress/tests/mocked/parallel-1/updateContributionAmount.cy.ts similarity index 91% rename from cypress/e2e/parallel-1/updateContributionAmount.cy.ts rename to cypress/tests/mocked/parallel-1/updateContributionAmount.cy.ts index 8d38a7df4..b9e437243 100644 --- a/cypress/e2e/parallel-1/updateContributionAmount.cy.ts +++ b/cypress/tests/mocked/parallel-1/updateContributionAmount.cy.ts @@ -1,9 +1,9 @@ import { contributionPaidByCard, supporterPlus, -} from '../../../client/fixtures/productBuilder/testProducts'; -import { toMembersDataApiResponse } from '../../../client/fixtures/mdapiResponse'; -import { signInAndAcceptCookies } from '../../lib/signInAndAcceptCookies'; +} from '../../../../client/fixtures/productBuilder/testProducts'; +import { toMembersDataApiResponse } from '../../../../client/fixtures/mdapiResponse'; +import { signInAndAcceptCookies } from '../../../lib/signInAndAcceptCookies'; describe('Update contribution amount', () => { const setSignInStatus = () => { diff --git a/cypress/e2e/parallel-1/updatePaymentDetails.cy.ts b/cypress/tests/mocked/parallel-1/updatePaymentDetails.cy.ts similarity index 95% rename from cypress/e2e/parallel-1/updatePaymentDetails.cy.ts rename to cypress/tests/mocked/parallel-1/updatePaymentDetails.cy.ts index fcedbf3f0..2b9296ac6 100644 --- a/cypress/e2e/parallel-1/updatePaymentDetails.cy.ts +++ b/cypress/tests/mocked/parallel-1/updatePaymentDetails.cy.ts @@ -1,17 +1,17 @@ -import { toMembersDataApiResponse } from '../../../client/fixtures/mdapiResponse'; +import { toMembersDataApiResponse } from '../../../../client/fixtures/mdapiResponse'; import { stripeSetupIntent, executePaymentUpdateResponse, ddPaymentMethod, -} from '../../../client/fixtures/payment'; -import { paymentMethods } from '../../../client/fixtures/stripe'; -import { signInAndAcceptCookies } from '../../lib/signInAndAcceptCookies'; +} from '../../../../client/fixtures/payment'; +import { paymentMethods } from '../../../../client/fixtures/stripe'; +import { signInAndAcceptCookies } from '../../../lib/signInAndAcceptCookies'; import { digitalPackPaidByCardWithPaymentFailure, digitalPackPaidByDirectDebit, guardianWeeklyPaidByCard, -} from '../../../client/fixtures/productBuilder/testProducts'; -import { singleContributionsAPIResponse } from '../../../client/fixtures/singleContribution'; +} from '../../../../client/fixtures/productBuilder/testProducts'; +import { singleContributionsAPIResponse } from '../../../../client/fixtures/singleContribution'; describe('Update payment details', () => { beforeEach(() => { diff --git a/cypress/e2e/parallel-2/cancelContribution.cy.ts b/cypress/tests/mocked/parallel-2/cancelContribution.cy.ts similarity index 96% rename from cypress/e2e/parallel-2/cancelContribution.cy.ts rename to cypress/tests/mocked/parallel-2/cancelContribution.cy.ts index 9f0ae4d6b..99c98c00a 100644 --- a/cypress/e2e/parallel-2/cancelContribution.cy.ts +++ b/cypress/tests/mocked/parallel-2/cancelContribution.cy.ts @@ -1,6 +1,6 @@ -import { contributionPaidByCard } from '../../../client/fixtures/productBuilder/testProducts'; -import { toMembersDataApiResponse } from '../../../client/fixtures/mdapiResponse'; -import { signInAndAcceptCookies } from '../../lib/signInAndAcceptCookies'; +import { contributionPaidByCard } from '../../../../client/fixtures/productBuilder/testProducts'; +import { toMembersDataApiResponse } from '../../../../client/fixtures/mdapiResponse'; +import { signInAndAcceptCookies } from '../../../lib/signInAndAcceptCookies'; describe('Cancel contribution', () => { const setSignInStatus = () => { diff --git a/cypress/e2e/parallel-2/cancelGW.cy.ts b/cypress/tests/mocked/parallel-2/cancelGW.cy.ts similarity index 93% rename from cypress/e2e/parallel-2/cancelGW.cy.ts rename to cypress/tests/mocked/parallel-2/cancelGW.cy.ts index ce8e12be4..bda8b3ec7 100644 --- a/cypress/e2e/parallel-2/cancelGW.cy.ts +++ b/cypress/tests/mocked/parallel-2/cancelGW.cy.ts @@ -1,6 +1,6 @@ -import { guardianWeeklyPaidByCard } from '../../../client/fixtures/productBuilder/testProducts'; -import { toMembersDataApiResponse } from '../../../client/fixtures/mdapiResponse'; -import { signInAndAcceptCookies } from '../../lib/signInAndAcceptCookies'; +import { guardianWeeklyPaidByCard } from '../../../../client/fixtures/productBuilder/testProducts'; +import { toMembersDataApiResponse } from '../../../../client/fixtures/mdapiResponse'; +import { signInAndAcceptCookies } from '../../../lib/signInAndAcceptCookies'; describe('Cancel guardian weekly', () => { beforeEach(() => { diff --git a/cypress/e2e/parallel-2/cancelSupporterPlus.cy.ts b/cypress/tests/mocked/parallel-2/cancelSupporterPlus.cy.ts similarity index 92% rename from cypress/e2e/parallel-2/cancelSupporterPlus.cy.ts rename to cypress/tests/mocked/parallel-2/cancelSupporterPlus.cy.ts index c1279c6bf..cf04bad44 100644 --- a/cypress/e2e/parallel-2/cancelSupporterPlus.cy.ts +++ b/cypress/tests/mocked/parallel-2/cancelSupporterPlus.cy.ts @@ -1,6 +1,6 @@ -import { supporterPlus } from '../../../client/fixtures/productBuilder/testProducts'; -import { toMembersDataApiResponse } from '../../../client/fixtures/mdapiResponse'; -import { signInAndAcceptCookies } from '../../lib/signInAndAcceptCookies'; +import { supporterPlus } from '../../../../client/fixtures/productBuilder/testProducts'; +import { toMembersDataApiResponse } from '../../../../client/fixtures/mdapiResponse'; +import { signInAndAcceptCookies } from '../../../lib/signInAndAcceptCookies'; describe('Cancel Supporter Plus', () => { const setupCancellation = () => { diff --git a/cypress/e2e/parallel-2/digisubSave.cy.ts b/cypress/tests/mocked/parallel-2/digisubSave.cy.ts similarity index 91% rename from cypress/e2e/parallel-2/digisubSave.cy.ts rename to cypress/tests/mocked/parallel-2/digisubSave.cy.ts index 5d18e08b3..5de99ae20 100644 --- a/cypress/e2e/parallel-2/digisubSave.cy.ts +++ b/cypress/tests/mocked/parallel-2/digisubSave.cy.ts @@ -1,6 +1,6 @@ -import { digitalPackPaidByDirectDebit } from '../../../client/fixtures/productBuilder/testProducts'; -import { toMembersDataApiResponse } from '../../../client/fixtures/mdapiResponse'; -import { signInAndAcceptCookies } from '../../lib/signInAndAcceptCookies'; +import { digitalPackPaidByDirectDebit } from '../../../../client/fixtures/productBuilder/testProducts'; +import { toMembersDataApiResponse } from '../../../../client/fixtures/mdapiResponse'; +import { signInAndAcceptCookies } from '../../../lib/signInAndAcceptCookies'; describe('Cancel digi sub', () => { beforeEach(() => { diff --git a/cypress/e2e/parallel-2/membershipSave.cy.ts b/cypress/tests/mocked/parallel-2/membershipSave.cy.ts similarity index 94% rename from cypress/e2e/parallel-2/membershipSave.cy.ts rename to cypress/tests/mocked/parallel-2/membershipSave.cy.ts index cc0a40dea..ca108c877 100644 --- a/cypress/e2e/parallel-2/membershipSave.cy.ts +++ b/cypress/tests/mocked/parallel-2/membershipSave.cy.ts @@ -2,10 +2,10 @@ import { contributionPaidByCard, guardianWeeklyExpiredCard, membershipSupporterWithOldPrice, -} from '../../../client/fixtures/productBuilder/testProducts'; -import { toMembersDataApiResponse } from '../../../client/fixtures/mdapiResponse'; -import { productMovePreviewResponse } from '../../../client/fixtures/productMove'; -import { signInAndAcceptCookies } from '../../lib/signInAndAcceptCookies'; +} from '../../../../client/fixtures/productBuilder/testProducts'; +import { toMembersDataApiResponse } from '../../../../client/fixtures/mdapiResponse'; +import { productMovePreviewResponse } from '../../../../client/fixtures/productMove'; +import { signInAndAcceptCookies } from '../../../lib/signInAndAcceptCookies'; describe('Cancel Membership saves', () => { const setSignInStatus = () => { diff --git a/cypress/e2e/parallel-2/productSwitch.cy.ts b/cypress/tests/mocked/parallel-2/productSwitch.cy.ts similarity index 95% rename from cypress/e2e/parallel-2/productSwitch.cy.ts rename to cypress/tests/mocked/parallel-2/productSwitch.cy.ts index 2b582f17d..84ccfc7ad 100644 --- a/cypress/e2e/parallel-2/productSwitch.cy.ts +++ b/cypress/tests/mocked/parallel-2/productSwitch.cy.ts @@ -1,15 +1,15 @@ -import { toMembersDataApiResponse } from '../../../client/fixtures/mdapiResponse'; -import { signInAndAcceptCookies } from '../../lib/signInAndAcceptCookies'; +import { toMembersDataApiResponse } from '../../../../client/fixtures/mdapiResponse'; +import { signInAndAcceptCookies } from '../../../lib/signInAndAcceptCookies'; import { productMovePreviewResponse, productMoveSuccessfulResponse, -} from '../../../client/fixtures/productMove'; +} from '../../../../client/fixtures/productMove'; import { contributionAboveSupporterPlusThreshold, contributionPaidByPayPalAboveSupporterPlusThreshold, contributionWithPaymentFailure, nonServicedCountryContributor, -} from '../../../client/fixtures/productBuilder/testProducts'; +} from '../../../../client/fixtures/productBuilder/testProducts'; const setSignInStatus = () => { cy.window().then((window) => { diff --git a/cypress/e2e/parallel-2/upgradeSupport.cy.ts b/cypress/tests/mocked/parallel-2/upgradeSupport.cy.ts similarity index 88% rename from cypress/e2e/parallel-2/upgradeSupport.cy.ts rename to cypress/tests/mocked/parallel-2/upgradeSupport.cy.ts index 0f320d85d..8c58e1a3a 100644 --- a/cypress/e2e/parallel-2/upgradeSupport.cy.ts +++ b/cypress/tests/mocked/parallel-2/upgradeSupport.cy.ts @@ -1,10 +1,10 @@ -import { toMembersDataApiResponse } from '../../../client/fixtures/mdapiResponse'; +import { toMembersDataApiResponse } from '../../../../client/fixtures/mdapiResponse'; import { contributionPaidByCard, contributionPaidByPayPal, -} from '../../../client/fixtures/productBuilder/testProducts'; -import { productMovePreviewResponse } from '../../../client/fixtures/productMove'; -import { signInAndAcceptCookies } from '../../lib/signInAndAcceptCookies'; +} from '../../../../client/fixtures/productBuilder/testProducts'; +import { productMovePreviewResponse } from '../../../../client/fixtures/productMove'; +import { signInAndAcceptCookies } from '../../../lib/signInAndAcceptCookies'; describe('upgrade support', () => { beforeEach(() => { diff --git a/cypress/e2e/parallel-3/holidayStops.cy.ts b/cypress/tests/mocked/parallel-3/holidayStops.cy.ts similarity index 97% rename from cypress/e2e/parallel-3/holidayStops.cy.ts rename to cypress/tests/mocked/parallel-3/holidayStops.cy.ts index 4f416e726..e0fe4c467 100644 --- a/cypress/e2e/parallel-3/holidayStops.cy.ts +++ b/cypress/tests/mocked/parallel-3/holidayStops.cy.ts @@ -1,4 +1,4 @@ -import { toMembersDataApiResponse } from '../../../client/fixtures/mdapiResponse'; +import { toMembersDataApiResponse } from '../../../../client/fixtures/mdapiResponse'; import { potentialDeliveries, noPotentialDeliveries, @@ -7,9 +7,9 @@ import { multiplePotentialDeliveries, existingHolidaysFirstIssueDecember, yearSpanningPotentialDeliveries, -} from '../../../client/fixtures/holidays'; -import { guardianWeeklyPaidByCard } from '../../../client/fixtures/productBuilder/testProducts'; -import { signInAndAcceptCookies } from '../../lib/signInAndAcceptCookies'; +} from '../../../../client/fixtures/holidays'; +import { guardianWeeklyPaidByCard } from '../../../../client/fixtures/productBuilder/testProducts'; +import { signInAndAcceptCookies } from '../../../lib/signInAndAcceptCookies'; describe('Holiday stops', () => { beforeEach(() => { diff --git a/cypress/e2e/parallel-4/deliveryAddress.cy.ts b/cypress/tests/mocked/parallel-4/deliveryAddress.cy.ts similarity index 95% rename from cypress/e2e/parallel-4/deliveryAddress.cy.ts rename to cypress/tests/mocked/parallel-4/deliveryAddress.cy.ts index 1512258cb..312752ed8 100644 --- a/cypress/e2e/parallel-4/deliveryAddress.cy.ts +++ b/cypress/tests/mocked/parallel-4/deliveryAddress.cy.ts @@ -2,9 +2,9 @@ import { guardianWeeklyPaidByCard, nationalDelivery, supporterPlus, -} from '../../../client/fixtures/productBuilder/testProducts'; -import { toMembersDataApiResponse } from '../../../client/fixtures/mdapiResponse'; -import { signInAndAcceptCookies } from '../../lib/signInAndAcceptCookies'; +} from '../../../../client/fixtures/productBuilder/testProducts'; +import { toMembersDataApiResponse } from '../../../../client/fixtures/mdapiResponse'; +import { signInAndAcceptCookies } from '../../../lib/signInAndAcceptCookies'; describe('Delivery address', () => { beforeEach(() => { diff --git a/cypress/e2e/parallel-4/deliveryRecords.cy.ts b/cypress/tests/mocked/parallel-4/deliveryRecords.cy.ts similarity index 96% rename from cypress/e2e/parallel-4/deliveryRecords.cy.ts rename to cypress/tests/mocked/parallel-4/deliveryRecords.cy.ts index 73ff6c074..412928ab0 100644 --- a/cypress/e2e/parallel-4/deliveryRecords.cy.ts +++ b/cypress/tests/mocked/parallel-4/deliveryRecords.cy.ts @@ -1,22 +1,22 @@ -import { toMembersDataApiResponse } from '../../../client/fixtures/mdapiResponse'; +import { toMembersDataApiResponse } from '../../../../client/fixtures/mdapiResponse'; import { deliveryRecordsWithNoDeliveries, deliveryRecordsWithDelivery, deliveryRecordsWithReportedProblem, -} from '../../../client/fixtures/deliveryRecords'; -import { signInAndAcceptCookies } from '../../lib/signInAndAcceptCookies'; -import { potentialDeliveries } from '../../../client/fixtures/holidays'; +} from '../../../../client/fixtures/deliveryRecords'; +import { signInAndAcceptCookies } from '../../../lib/signInAndAcceptCookies'; +import { potentialDeliveries } from '../../../../client/fixtures/holidays'; import { dateAddDays, dateString, DATE_FNS_INPUT_FORMAT, -} from '../../../shared/dates'; -import { singleContributionsAPIResponse } from '../../../client/fixtures/singleContribution'; +} from '../../../../shared/dates'; +import { singleContributionsAPIResponse } from '../../../../client/fixtures/singleContribution'; import { guardianWeeklyPaidByCard, homeDelivery, nationalDelivery, -} from '../../../client/fixtures/productBuilder/testProducts'; +} from '../../../../client/fixtures/productBuilder/testProducts'; describe('Delivery records', () => { beforeEach(() => { diff --git a/cypress/e2e/parallel-5/membership.cy.ts b/cypress/tests/mocked/parallel-5/membership.cy.ts similarity index 84% rename from cypress/e2e/parallel-5/membership.cy.ts rename to cypress/tests/mocked/parallel-5/membership.cy.ts index 1494a7114..3bf94df15 100644 --- a/cypress/e2e/parallel-5/membership.cy.ts +++ b/cypress/tests/mocked/parallel-5/membership.cy.ts @@ -1,6 +1,6 @@ -import { membershipSupporter } from '../../../client/fixtures/productBuilder/testProducts'; -import { toMembersDataApiResponse } from '../../../client/fixtures/mdapiResponse'; -import { signInAndAcceptCookies } from '../../lib/signInAndAcceptCookies'; +import { membershipSupporter } from '../../../../client/fixtures/productBuilder/testProducts'; +import { toMembersDataApiResponse } from '../../../../client/fixtures/mdapiResponse'; +import { signInAndAcceptCookies } from '../../../lib/signInAndAcceptCookies'; describe('membership test', () => { beforeEach(() => { diff --git a/cypress/e2e/parallel-5/patron.cy.ts b/cypress/tests/mocked/parallel-5/patron.cy.ts similarity index 87% rename from cypress/e2e/parallel-5/patron.cy.ts rename to cypress/tests/mocked/parallel-5/patron.cy.ts index 5c846dfa9..f70774a95 100644 --- a/cypress/e2e/parallel-5/patron.cy.ts +++ b/cypress/tests/mocked/parallel-5/patron.cy.ts @@ -1,9 +1,9 @@ import { guardianWeeklyPaidByCard, patronDigitalPack, -} from '../../../client/fixtures/productBuilder/testProducts'; -import { toMembersDataApiResponse } from '../../../client/fixtures/mdapiResponse'; -import { signInAndAcceptCookies } from '../../lib/signInAndAcceptCookies'; +} from '../../../../client/fixtures/productBuilder/testProducts'; +import { toMembersDataApiResponse } from '../../../../client/fixtures/mdapiResponse'; +import { signInAndAcceptCookies } from '../../../lib/signInAndAcceptCookies'; describe('patron test', () => { beforeEach(() => { diff --git a/cypress/e2e/parallel-6/emailAndMarketing.cy.ts b/cypress/tests/mocked/parallel-6/emailAndMarketing.cy.ts similarity index 78% rename from cypress/e2e/parallel-6/emailAndMarketing.cy.ts rename to cypress/tests/mocked/parallel-6/emailAndMarketing.cy.ts index 4fecc4759..73939e76f 100644 --- a/cypress/e2e/parallel-6/emailAndMarketing.cy.ts +++ b/cypress/tests/mocked/parallel-6/emailAndMarketing.cy.ts @@ -1,11 +1,11 @@ -import { user as userResponse } from '../../../client/fixtures/user'; -import { toMembersDataApiResponse } from '../../../client/fixtures/mdapiResponse'; -import { singleContributionsAPIResponse } from '../../../client/fixtures/singleContribution'; -import { newsletters } from '../../../client/fixtures/newsletters'; -import { consents } from '../../../client/fixtures/consents'; -import { newsletterSubscriptions } from '../../../client/fixtures/newsletterSubscriptions'; -import { InAppPurchase } from '../../../client/fixtures/inAppPurchase'; -import { signInAndAcceptCookies } from '../../lib/signInAndAcceptCookies'; +import { user as userResponse } from '../../../../client/fixtures/user'; +import { toMembersDataApiResponse } from '../../../../client/fixtures/mdapiResponse'; +import { singleContributionsAPIResponse } from '../../../../client/fixtures/singleContribution'; +import { newsletters } from '../../../../client/fixtures/newsletters'; +import { consents } from '../../../../client/fixtures/consents'; +import { newsletterSubscriptions } from '../../../../client/fixtures/newsletterSubscriptions'; +import { InAppPurchase } from '../../../../client/fixtures/inAppPurchase'; +import { signInAndAcceptCookies } from '../../../lib/signInAndAcceptCookies'; describe('Email and Marketing page', () => { beforeEach(() => { diff --git a/cypress/e2e/parallel-6/identitySettingsForm.cy.ts b/cypress/tests/mocked/parallel-6/identitySettingsForm.cy.ts similarity index 89% rename from cypress/e2e/parallel-6/identitySettingsForm.cy.ts rename to cypress/tests/mocked/parallel-6/identitySettingsForm.cy.ts index 13f0da2aa..39ddc16b7 100644 --- a/cypress/e2e/parallel-6/identitySettingsForm.cy.ts +++ b/cypress/tests/mocked/parallel-6/identitySettingsForm.cy.ts @@ -1,5 +1,5 @@ -import { user as userResponse } from '../../../client/fixtures/user'; -import { signInAndAcceptCookies } from '../../lib/signInAndAcceptCookies'; +import { user as userResponse } from '../../../../client/fixtures/user'; +import { signInAndAcceptCookies } from '../../../lib/signInAndAcceptCookies'; describe('Settings Form', () => { beforeEach(() => { diff --git a/nginx/identity-frontend-CODE-fallback.conf b/nginx/identity-frontend-CODE-fallback.conf index 89c905d82..418e90c58 100644 --- a/nginx/identity-frontend-CODE-fallback.conf +++ b/nginx/identity-frontend-CODE-fallback.conf @@ -1,7 +1,5 @@ upstream identity_services { # avoids naming clash with Identity's own nginx config if already installed - server localhost:9009; # Dotcom Identity Frontend - server localhost:8860; # Identity Frontend - server localhost:8800; # IDAPI ?? + server localhost:8861; # Gateway } server { @@ -16,23 +14,83 @@ server { ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; + # fixes issues for large response headers + proxy_buffer_size 128k; + proxy_buffers 4 256k; + proxy_busy_buffers_size 256k; + large_client_header_buffers 4 32k; + location / { - proxy_pass http://identity_services; - proxy_set_header Host $http_host; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "Upgrade"; + proxy_pass http://identity_services; + proxy_set_header Host $http_host; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header "X-GU-Okta-Env" "profile.code.dev-theguardian.com"; + + ###### + # remove `sid` cookie in requests to Gateway + # save original "Cookie" header value + set $altered_cookie $http_cookie; + # check if the "sid" cookie is present + if ($http_cookie ~ '(.*)(^|;\s)sid=("[^"]*"|[^\s]*[^;]?)(\2|$|;$)(?:;\s)?(.*)') { + # cut "sid" cookie from the string + set $altered_cookie $1$4$5; + } + # hide original "Cookie" header + proxy_hide_header Cookie; + # set "Cookie" header to the new value + proxy_set_header Cookie $altered_cookie; + ###### - proxy_intercept_errors on; + proxy_intercept_errors on; error_page 404 502 503 504 = @fallback; } + # paths to proxy to okta + location ~ ^/(oauth2|api\/v1|login|idp|sso|.well-known\/openid-configuration) { + resolver 8.8.8.8; + proxy_pass https://profile.code.dev-theguardian.com; + proxy_set_header Host profile.code.dev-theguardian.com; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Protocol $scheme; + proxy_set_header Referer https://profile.code.dev-theguardian.com; + # rewrite any domain in the response to our proxy + proxy_set_header Accept-Encoding ""; + sub_filter_types application/json; + sub_filter_once off; + sub_filter 'profile.code.dev-theguardian.com' 'profile.thegulocal.com'; + + ###### + # remove `sid` cookie in requests to Gateway + # save original "Cookie" header value + set $altered_cookie $http_cookie; + # check if the "sid" cookie is present + if ($http_cookie ~ '(.*)(^|;\s)sid=("[^"]*"|[^\s]*[^;]?)(\2|$|;$)(?:;\s)?(.*)') { + # cut "sid" cookie from the string + set $altered_cookie $1$4$5; + } + # hide original "Cookie" header + proxy_hide_header Cookie; + # set "Cookie" header to the new value + proxy_set_header Cookie $altered_cookie; + ###### + + error_page 404 502 503 504 = @fallback; + } + location @fallback { - proxy_pass https://profile.code.dev-theguardian.com; - proxy_set_header Host profile.code.dev-theguardian.com; - proxy_set_header Origin https://profile.code.dev-theguardian.com; - proxy_hide_header Content-Security-Policy; - proxy_cookie_domain profile.code.dev-theguardian.com profile.thegulocal.com; - proxy_cookie_domain .code.dev-theguardian.com .thegulocal.com; + proxy_pass https://profile.code.dev-theguardian.com; + proxy_set_header Host profile.code.dev-theguardian.com; + proxy_set_header Origin https://profile.code.dev-theguardian.com; + proxy_set_header Accept-Encoding ""; + proxy_hide_header Content-Security-Policy; + proxy_cookie_domain profile.code.dev-theguardian.com profile.thegulocal.com; + proxy_cookie_domain .code.dev-theguardian.com .thegulocal.com; + + sub_filter_types application/json; + sub_filter_once off; + sub_filter 'profile.code.dev-theguardian.com' 'profile.thegulocal.com'; } } diff --git a/package.json b/package.json index 3fbc134ed..15cc40e0e 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "scripts": { "start": "nodemon --inspect ./dist/server", + "cypress:start": "RUNNING_IN_CYPRESS=true nodemon --inspect ./dist/server", "type-check": "tsc --skipLibCheck", "lint": "eslint 'client/**' 'server/**' 'shared/**'", "bundle": "copy-node-modules . ./dist && yarn webpack --config webpack.production.js", @@ -20,9 +21,12 @@ "updateSnapshot": "jest -u", "storybook": "storybook dev -p 6006 -s 'static'", "build-storybook": "storybook build -s '.storybook/static'", - "cypress:open": "DEBUG=cypress:* cypress open", - "cypress:run": "cypress run", - "cypress:server": "yarn bundle-dev-server && (yarn bundle-dev-server-cypress -w & yarn start & yarn serve-dev-cypress)", + "cypress:mocked:open": "DEBUG=cypress:* cypress open --config '{\"e2e\":{\"specPattern\":\"cypress/tests/mocked/**/*.cy.{js,jsx,ts,tsx}\"}}' --e2e --browser chrome", + "cypress:mocked:run": "cypress run --config '{\"e2e\":{\"specPattern\":\"cypress/tests/mocked/**/*.cy.{js,jsx,ts,tsx}\"}}' --e2e --browser chrome", + "cypress:mocked:server": "yarn bundle-dev-server && (yarn bundle-dev-server-cypress -w & yarn start & yarn serve-dev-cypress)", + "cypress:e2e:open": "cypress open --config '{\"e2e\":{\"specPattern\":\"cypress/tests/e2e/**/*.cy.{js,jsx,ts,tsx}\"}}' --e2e --browser chrome", + "cypress:e2e:run": "cypress run --config '{\"e2e\":{\"specPattern\":\"cypress/tests/e2e/**/*.cy.{js,jsx,ts,tsx}\"}}' --e2e --browser chrome", + "cypress:e2e:server": "yarn bundle-dev-server && (yarn bundle-dev-server -w & yarn cypress:start & yarn serve-dev-cypress)", "chromatic": "chromatic" }, "lint-staged": { diff --git a/server/log.ts b/server/log.ts index 18b5aece8..bbcb52eea 100644 --- a/server/log.ts +++ b/server/log.ts @@ -21,6 +21,9 @@ interface MetricLoggingFields { } export const putMetric = (fields: MetricLoggingFields) => { + if (process.env.RUNNING_IN_CYPRESS === 'true') { + return; + } const dimensions = { Stage: conf.STAGE, outcome: fields.isOK ? 'SUCCESS' : 'ERROR', diff --git a/server/oktaConfig.ts b/server/oktaConfig.ts index 52d0e624c..5bffbb4e4 100644 --- a/server/oktaConfig.ts +++ b/server/oktaConfig.ts @@ -34,5 +34,8 @@ export const getConfig = async (): Promise => { if (!isValidConfig(config)) { throw new Error('Error loading a valid config'); } + if (process.env.RUNNING_IN_CYPRESS === 'true') { + config.useOkta = true; + } return config; }; From 846975801c325d521b8e91d09664b53ea310c269 Mon Sep 17 00:00:00 2001 From: Raphael Kabo Date: Tue, 5 Dec 2023 16:33:52 +0000 Subject: [PATCH 3/7] feat: add reCAPTCHA Cypress command --- cypress/support/commands.ts | 10 ++++++++++ cypress/support/e2e.ts | 1 + 2 files changed, 11 insertions(+) diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index eefe3a6e7..45ed4da17 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -55,6 +55,16 @@ Cypress.Commands.add('iframeLoaded', { prevSubject: 'element' }, ($iframe) => { }); }); +Cypress.Commands.add('solveGoogleReCAPTCHA', () => { + cy.get('#recaptcha *> iframe').then(($iframe) => { + const $body = $iframe.contents().find('body'); + cy.wrap($body) + .find('.recaptcha-checkbox-border') + .should('be.visible') + .click(); + }); +}); + Cypress.Commands.add('resolve', (name, options = {}) => { const getValue = () => { // @ts-ignore diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index 917f50c76..a588f0278 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -33,6 +33,7 @@ declare global { resolve(name: string): Chainable; getIframeBody(selector: string): Chainable; findByText(text: string): Chainable; + solveGoogleReCAPTCHA(): Chainable; } } } From 1ee4eb2045d5613925c368020ab5835b2aea5dfa Mon Sep 17 00:00:00 2001 From: Raphael Kabo Date: Tue, 5 Dec 2023 16:34:15 +0000 Subject: [PATCH 4/7] docs: for Cypress testing --- docs/07-testing.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 docs/07-testing.md diff --git a/docs/07-testing.md b/docs/07-testing.md new file mode 100644 index 000000000..3444fb402 --- /dev/null +++ b/docs/07-testing.md @@ -0,0 +1,62 @@ +# Cypress testing + +Cypress tests come in two types: mocked and end-to-end (E2E). + +## Mocked tests + +Most of the tests are mocked tests. They mock responses from various APIs and +short-circuit the identity middleware, which prevents the tests from needing to +log a user in. The middleware is short-circuited by setting the value of the +global variable `CYPRESS` to `'SKIP_IDAPI'` in the `bundle-dev-server-cypress` +script in `package.json`. + +The mocked tests live in `cypress/tests/mocked`. + +The mocked tests use the following commands in `package.json`: + +- `yarn cypress:mocked:server`: starts a dev server (essentially the same as the standard `yarn watch` command but with the `CYPRESS` variable set to `'SKIP_IDAPI'`, and doesn't open a new browser tab). +- `yarn cypress:mocked:open`: opens the Cypress test runner in mocked mode. +- `yarn cypress:mocked:run`: runs the Cypress tests headless in mocked mode. + +In CI, the mocked tests are run by the `cypress-mocked.yml` GitHub Actions job. + +## End-to-end tests + +The end-to-end tests live in `cypress/tests/e2e`. They currently comprise a +small suite of basic tests which use CODE IDAPI to generate a test user and CODE +Gateway to sign that user in before performing a few basic functions on the +frontend. + +To run E2E tests locally, it is necessary to download a Cypress env file from S3 +with the following command (after getting fresh tokens from Janus): + +```bash +aws s3 cp --profile membership s3://gu-reader-revenue-private/manage-frontend/CODE/cypress.env.json cypress.env.json +``` + +Also ensure that you have run the setup script in the `nginx` directory, which +sets up the local Nginx proxy to CODE Gateway and IDAPI. If you are also developing +Identity projects such as Gateway, you may have already run the setup script in the +`identity-platform` repo. If this is the case, you will have conflicting server blocks +for the `profile.thegulocal.com` domain. You will need to either: + +- Always run Gateway locally while running MMA E2E tests, and delete the + `identity-frontend-CODE-fallback.conf` file in the Nginx config directory; or +- Comment out the `profile.thegulocal.com` server block in the `identity.conf` + file in the Nginx config directory. + +Remember to restart Nginx after making any changes to the Nginx config files. + +```bash +cd $(dev-nginx locate)/servers +sudo rm identity-frontend-CODE-fallback.conf +# OR +sudo vim identity.conf +sudo nginx -s reload +``` + +The E2E tests use the following commands in `package.json`: + +- `yarn cypress:e2e:server`: starts a dev server without setting the `CYPRESS` variable. +- `yarn cypress:e2e:open`: opens the Cypress test runner in E2E mode. +- `yarn cypress:e2e:run`: runs the Cypress tests headless in E2E mode. From f1335096053e041cf47dd5ed9094e422c834080c Mon Sep 17 00:00:00 2001 From: Raphael Kabo Date: Thu, 7 Dec 2023 17:24:29 +0000 Subject: [PATCH 5/7] remove idapi access token from mocked tests --- .github/workflows/cypress-mocked.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/cypress-mocked.yml b/.github/workflows/cypress-mocked.yml index b4f6a41fb..b0ab5a143 100644 --- a/.github/workflows/cypress-mocked.yml +++ b/.github/workflows/cypress-mocked.yml @@ -31,8 +31,6 @@ jobs: - name: Cypress run uses: cypress-io/github-action@v6 - env: - IDAPI_CLIENT_ACCESS_TOKEN: ${{ secrets.IDAPI_CLIENT_ACCESS_TOKEN }} with: start: yarn cypress:mocked:server wait-on: 'http://localhost:9234, http://localhost:9233' From 8c17c83bec414ce7d879070b91f717898476d78d Mon Sep 17 00:00:00 2001 From: Raphael Kabo Date: Wed, 13 Dec 2023 15:32:52 +0000 Subject: [PATCH 6/7] fix: formatting and explanatory comments --- .github/workflows/cypress-e2e.yml | 4 +- cypress/cypress-nginx.conf | 127 +++++++++++++++--------------- 2 files changed, 68 insertions(+), 63 deletions(-) diff --git a/.github/workflows/cypress-e2e.yml b/.github/workflows/cypress-e2e.yml index f0b3332ff..a369e0795 100644 --- a/.github/workflows/cypress-e2e.yml +++ b/.github/workflows/cypress-e2e.yml @@ -47,7 +47,9 @@ jobs: uses: cypress-io/github-action@v6 env: CYPRESS_IDAPI_CLIENT_ACCESS_TOKEN: ${{ secrets.IDAPI_CLIENT_ACCESS_TOKEN }} - # Required for the Cypress tests to run as we're unable to verify the created certs + # This env variable prevents Node from rejecting self-signed TLS certificates. It's + # required for the Cypress tests to run as we're unable to verify the created certs. + # See: https://nodejs.org/api/cli.html#node_tls_reject_unauthorizedvalue NODE_TLS_REJECT_UNAUTHORIZED: 0 with: start: yarn cypress:e2e:server diff --git a/cypress/cypress-nginx.conf b/cypress/cypress-nginx.conf index 670979c08..c1185a175 100644 --- a/cypress/cypress-nginx.conf +++ b/cypress/cypress-nginx.conf @@ -3,67 +3,69 @@ # with a ssl cert set on the domain #user nobody; -worker_processes 1; +worker_processes 1; events { - worker_connections 1024; + worker_connections 1024; } http { - include mime.types; - default_type application/octet-stream; + include mime.types; + default_type application/octet-stream; - sendfile on; + sendfile on; - keepalive_timeout 65; + # Set to 5 seconds longer than 60 seconds (pretty sure this is a magic numnber). + # This should help prevent timeouts in Cypress requests inside Github Actions. + keepalive_timeout 65; # fixes issues for large response headers - proxy_buffer_size 128k; - proxy_buffers 4 256k; - proxy_busy_buffers_size 256k; + proxy_buffer_size 128k; + proxy_buffers 4 256k; + proxy_busy_buffers_size 256k; # manage.thegulocal.com # ====================== server { - listen 443 ssl; - server_name manage.thegulocal.com; - proxy_http_version 1.1; # this is essential for chunked responses to work + listen 443 ssl; + server_name manage.thegulocal.com; + proxy_http_version 1.1; # this is essential for chunked responses to work - ssl_certificate manage.thegulocal.com.crt; - ssl_certificate_key manage.thegulocal.com.key; - ssl_session_timeout 5m; - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_ciphers HIGH:!aNULL:!MD5; - ssl_prefer_server_ciphers on; + ssl_certificate manage.thegulocal.com.crt; + ssl_certificate_key manage.thegulocal.com.key; + ssl_session_timeout 5m; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; location / { - proxy_pass http://localhost:9234/; - proxy_set_header Host $http_host; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "Upgrade"; + proxy_pass http://localhost:9234/; + proxy_set_header Host $http_host; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; } } # members-data-api.thegulocal.com # ====================== server { - listen 443 ssl; - server_name members-data-api.thegulocal.com; - proxy_http_version 1.1; # this is essential for chunked responses to work + listen 443 ssl; + server_name members-data-api.thegulocal.com; + proxy_http_version 1.1; # this is essential for chunked responses to work - ssl_certificate members-data-api.thegulocal.com.crt; - ssl_certificate_key members-data-api.thegulocal.com.key; - ssl_session_timeout 5m; - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_ciphers HIGH:!aNULL:!MD5; - ssl_prefer_server_ciphers on; + ssl_certificate members-data-api.thegulocal.com.crt; + ssl_certificate_key members-data-api.thegulocal.com.key; + ssl_session_timeout 5m; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; location / { - proxy_pass https://members-data-api.code.dev-theguardian.com; - proxy_next_upstream error timeout http_404 non_idempotent; - proxy_set_header "X-Forwarded-Proto" "https"; + proxy_pass https://members-data-api.code.dev-theguardian.com; + proxy_next_upstream error timeout http_404 non_idempotent; + proxy_set_header "X-Forwarded-Proto" "https"; proxy_set_header Host members-data-api.code.dev-theguardian.com; proxy_set_header Accept-Encoding ""; proxy_hide_header Content-Security-Policy; @@ -81,50 +83,51 @@ http { # profile.thegulocal.com # ====================== server { - listen 443 ssl; - server_name profile.thegulocal.com; - proxy_http_version 1.1; # this is essential for chunked responses to work + listen 443 ssl; + server_name profile.thegulocal.com; + proxy_http_version 1.1; # this is essential for chunked responses to work - ssl_certificate profile.thegulocal.com.crt; - ssl_certificate_key profile.thegulocal.com.key; - ssl_session_timeout 5m; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; - ssl_prefer_server_ciphers on; + ssl_certificate profile.thegulocal.com.crt; + ssl_certificate_key profile.thegulocal.com.key; + ssl_session_timeout 5m; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + ssl_prefer_server_ciphers on; # dummy location header for the API - proxy_set_header X-GU-ID-Geolocation ip:$remote_addr,country:GB,city:Leeds; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-GU-ID-Geolocation ip:$remote_addr,country:GB,city:Leeds; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; location / { - proxy_pass https://profile.code.dev-theguardian.com; - proxy_next_upstream error timeout http_404 non_idempotent; - proxy_set_header "X-Forwarded-Proto" "https"; - proxy_set_header "X-GU-Okta-Env" "profile.code.dev-theguardian.com"; - proxy_set_header Host profile.code.dev-theguardian.com; - proxy_set_header Accept-Encoding ""; - proxy_hide_header Content-Security-Policy; + proxy_pass https://profile.code.dev-theguardian.com; + proxy_next_upstream error timeout http_404 non_idempotent; + proxy_set_header "X-Forwarded-Proto" "https"; + proxy_set_header "X-GU-Okta-Env" "profile.code.dev-theguardian.com"; + proxy_set_header Host profile.code.dev-theguardian.com; + proxy_set_header Accept-Encoding ""; + proxy_hide_header Content-Security-Policy; - proxy_cookie_domain profile.code.dev-theguardian.com profile.thegulocal.com; - proxy_cookie_domain .code.dev-theguardian.com .thegulocal.com; + proxy_cookie_domain profile.code.dev-theguardian.com profile.thegulocal.com; + proxy_cookie_domain .code.dev-theguardian.com .thegulocal.com; - sub_filter_types application/json; - sub_filter_once off; - sub_filter 'profile.code.dev-theguardian.com' 'profile.thegulocal.com'; + sub_filter_types application/json; + sub_filter_once off; + sub_filter 'profile.code.dev-theguardian.com' 'profile.thegulocal.com'; ###### # remove `sid` cookie in requests to Gateway # save original "Cookie" header value - set $altered_cookie $http_cookie; + set $altered_cookie $http_cookie; # check if the "sid" cookie is present + # From: https://stackoverflow.com/a/67627604 if ($http_cookie ~ '(.*)(^|;\s)sid=("[^"]*"|[^\s]*[^;]?)(\2|$|;$)(?:;\s)?(.*)') { # cut "sid" cookie from the string - set $altered_cookie $1$4$5; + set $altered_cookie $1$4$5; } # hide original "Cookie" header - proxy_hide_header Cookie; + proxy_hide_header Cookie; # set "Cookie" header to the new value - proxy_set_header Cookie $altered_cookie; + proxy_set_header Cookie $altered_cookie; ###### } } From 52c60b93e9ba5e25487c046e6bef7b29366b5e5a Mon Sep 17 00:00:00 2001 From: Raphael Kabo Date: Wed, 13 Dec 2023 15:37:39 +0000 Subject: [PATCH 7/7] refactor: move E2E timeout value to central call --- cypress/tests/e2e/e2e.cy.ts | 41 ++++++++++--------------------------- 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/cypress/tests/e2e/e2e.cy.ts b/cypress/tests/e2e/e2e.cy.ts index fadfb959d..a35ea4a01 100644 --- a/cypress/tests/e2e/e2e.cy.ts +++ b/cypress/tests/e2e/e2e.cy.ts @@ -2,40 +2,32 @@ import { signInOkta } from '../../lib/signInOkta'; describe('E2E with Okta', () => { beforeEach(() => { + Cypress.config('defaultCommandTimeout', 30_000); + cy.clearAllCookies(); signInOkta(); - cy.get('h1', { - timeout: 30000, - }).should('contain', 'Account overview'); + cy.get('h1').should('contain', 'Account overview'); }); context('account overview tab', () => { it('should contain an email address', () => { - cy.findByText('Email address', { - timeout: 30000, - }); + cy.findByText('Email address'); }); }); context('profile tab', () => { it('should contain a username', () => { cy.visit('/public-settings'); - cy.findByText('Username', { - timeout: 30000, - }); + cy.findByText('Username'); }); }); context('emails tab', () => { it('should allow the user to update their email preferences', () => { cy.visit('/email-prefs'); - cy.findByText('Guardian products and support', { - timeout: 30000, - }).click(); + cy.findByText('Guardian products and support').click(); cy.visit('/email-prefs'); - cy.findByText('Guardian products and support', { - timeout: 30000, - }) + cy.findByText('Guardian products and support') .parents('div') .get('input[type="checkbox"]') .should('be.checked'); @@ -43,22 +35,16 @@ describe('E2E with Okta', () => { it('should allow the user to unsubscribe from all emails', () => { cy.visit('/email-prefs'); - cy.findByText('Unsubscribe from all emails', { - timeout: 30000, - }).click(); + cy.findByText('Unsubscribe from all emails').click(); cy.visit('/email-prefs'); - cy.get('input[type="checkbox"]', { - timeout: 30000, - }).should('not.be.checked'); + cy.get('input[type="checkbox"]').should('not.be.checked'); }); }); context('settings tab', () => { it('should allow the user to update their personal information', () => { cy.visit('/account-settings'); - cy.findByLabelText('First Name', { - timeout: 30000, - }).clear(); + cy.findByLabelText('First Name').clear(); cy.findByLabelText('Last Name').clear(); cy.findByLabelText('First Name').type('Test'); cy.findByLabelText('Last Name').type('User'); @@ -74,9 +60,6 @@ describe('E2E with Okta', () => { cy.visit('/help'); cy.findByText( 'If you still can’t find what you need and want to contact us, check', - { - timeout: 30000, - }, ) .parent() .findByText('here') @@ -89,9 +72,7 @@ describe('E2E with Okta', () => { cy.solveGoogleReCAPTCHA(); cy.wait(1000); cy.findByText('Submit').click(); - cy.findByText('Thank you for contacting us', { - timeout: 30000, - }); + cy.findByText('Thank you for contacting us'); }); }); });