From ff7bcc9bd5eccc2f1187c96eba229c692fe4c1e4 Mon Sep 17 00:00:00 2001 From: davidhayes03 Date: Mon, 2 Feb 2026 14:08:03 +0000 Subject: [PATCH] CODE RUB: Test Nhs Login --- .github/workflows/build.yml | 4 +- ...nDataServices.IDecide.Portal.Client.esproj | 1 + .../tests/helpers/helper.ts | 110 +++++++++++++++++- .../tests/homepageNhsLogin.spec.ts | 99 ++++++++++++++++ .../tests/setup/auth.setup.tffs | 32 +++++ 5 files changed, 243 insertions(+), 3 deletions(-) create mode 100644 LondonDataServices.IDecide.Portal.Client/tests/homepageNhsLogin.spec.ts create mode 100644 LondonDataServices.IDecide.Portal.Client/tests/setup/auth.setup.tffs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 774df9b7..4062cb61 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -51,8 +51,8 @@ jobs: } shell: pwsh env: - NotificationConfigurations__ApiKey: ${{ secrets.NOTIFICATIONCONFIGURATIONS__APIKEY }} - NHSLoginOIDC__privateKeyb64: ${{ secrets.NHSLOGINOIDC__PRIVATEKEYB64 }} + NOTIFICATIONCONFIGURATIONS__APIKEY: ${{ secrets.NOTIFICATIONCONFIGURATIONS__APIKEY }} + NHSLOGINOIDC__PRIVATEKEYB64: ${{ secrets.NHSLOGINOIDC__PRIVATEKEYB64 }} add_tag: name: Tag and Release runs-on: ubuntu-latest diff --git a/LondonDataServices.IDecide.Portal.Client/LondonDataServices.IDecide.Portal.Client.esproj b/LondonDataServices.IDecide.Portal.Client/LondonDataServices.IDecide.Portal.Client.esproj index 80064ad8..8265961c 100644 --- a/LondonDataServices.IDecide.Portal.Client/LondonDataServices.IDecide.Portal.Client.esproj +++ b/LondonDataServices.IDecide.Portal.Client/LondonDataServices.IDecide.Portal.Client.esproj @@ -15,6 +15,7 @@ + diff --git a/LondonDataServices.IDecide.Portal.Client/tests/helpers/helper.ts b/LondonDataServices.IDecide.Portal.Client/tests/helpers/helper.ts index af0f16dc..01e52421 100644 --- a/LondonDataServices.IDecide.Portal.Client/tests/helpers/helper.ts +++ b/LondonDataServices.IDecide.Portal.Client/tests/helpers/helper.ts @@ -2,10 +2,16 @@ export async function clickStartButton(page) { await page.getByTestId('start-login-button').click(); } + export async function clickStartAnotherPersonButton(page) { await page.getByTestId('start-another-person-button').click(); } -export async function fillPoaFields(page: import('@playwright/test').Page, firstName = 'John', surname = 'Doe') { + +export async function fillPoaFields( + page: import('@playwright/test').Page, + firstName = 'John', + surname = 'Doe' +) { await page.locator('#poa-firstname').fill(firstName); await page.locator('#poa-surname').fill(surname); @@ -15,4 +21,106 @@ export async function fillPoaFields(page: import('@playwright/test').Page, first const randomIndex = Math.floor(Math.random() * (options.length - 1)) + 1; const randomValue = await options[randomIndex].getAttribute('value'); await page.locator('#poa-relationship').selectOption(randomValue!); +} + +// Helper function to populate NHS login credentials +export async function fillNhsLoginCredentials( + page: import('@playwright/test').Page, + email = 'testuserlive@demo.signin.nhs.uk', + password = 'Passw0rd$1' +) { + await page.locator('input[type="email"]').fill(email); + await page.locator('input[type="password"]').fill(password); +} + +// Helper function to click the Continue button +export async function clickContinueButton(page: import('@playwright/test').Page) { + await page.getByRole('button', { name: /continue/i }).click(); +} + +// Helper function to fill the OTP code +export async function fillOtpCode( + page: import('@playwright/test').Page, + otpCode = '190696' +) { + await page.locator('#otp-input').fill(otpCode); +} + +// Helper function to check the "Remember this device" checkbox +export async function checkRememberDeviceCheckbox(page: import('@playwright/test').Page) { + await page.locator('#rmd').check(); +} + +// Helper function to handle OTP if prompted and click continue +export async function handleOtpIfPrompted( + page: import('@playwright/test').Page, + otpCode = '190696' +) { + // Wait for navigation after login to settle + await page.waitForLoadState('networkidle'); + + const currentUrl = page.url(); + + // Check if we're locked out due to too many OTP attempts + if (currentUrl.includes('/otp-attempts-exceeded')) { + throw new Error( + 'OTP attempts exceeded. NHS Login has locked you out for 15 minutes. ' + + 'Please wait before running tests again.' + ); + } + + // Check if we're on the OTP page by URL + const isOnOtpPage = currentUrl.includes('/enter-mobile-code'); + + if (isOnOtpPage) { + const otpInput = page.locator('#otp-input'); + const continueButton = page.getByRole('button', { name: /continue/i }); + + // Wait for OTP input to be visible + await otpInput.waitFor({ state: 'visible', timeout: 10000 }); + + // Check for existing error before attempting to fill OTP + const errorAlert = page.locator('div[role="alert"]:has-text("Error")'); + const hasExistingError = await errorAlert.isVisible().catch(() => false); + + if (hasExistingError) { + const errorText = await errorAlert.textContent(); + throw new Error( + `OTP page shows existing error: ${errorText}. ` + + 'Tests aborted to prevent lockout. Please resolve the error before continuing.' + ); + } + + // Fill OTP and remember device + await otpInput.fill(otpCode); + await page.locator('#rmd').check(); + + // Click continue button + await continueButton.click(); + + // Wait for navigation away from OTP page + await page.waitForLoadState('networkidle'); + + const newUrl = page.url(); + + // Check if we got locked out after submission + if (newUrl.includes('/otp-attempts-exceeded')) { + throw new Error( + 'Invalid OTP code caused lockout. NHS Login has locked you out for 15 minutes. ' + + `The OTP code "${otpCode}" appears to be incorrect.` + ); + } + + // Check if we're still on the OTP page with an error + if (newUrl.includes('/enter-mobile-code')) { + const postSubmitError = await errorAlert.isVisible().catch(() => false); + if (postSubmitError) { + const errorText = await errorAlert.textContent(); + throw new Error( + `OTP submission failed: ${errorText}. ` + + 'Tests aborted to prevent further failed attempts and lockout.' + ); + } + } + } } \ No newline at end of file diff --git a/LondonDataServices.IDecide.Portal.Client/tests/homepageNhsLogin.spec.ts b/LondonDataServices.IDecide.Portal.Client/tests/homepageNhsLogin.spec.ts new file mode 100644 index 00000000..97af4853 --- /dev/null +++ b/LondonDataServices.IDecide.Portal.Client/tests/homepageNhsLogin.spec.ts @@ -0,0 +1,99 @@ +import { test, expect } from '@playwright/test'; +import { + clickStartButton, + fillNhsLoginCredentials, + clickContinueButton, + handleOtpIfPrompted +} from './helpers/helper'; + +test.describe('Home Page Nhs Login', () => { + test.beforeEach(async ({ page }) => { + await page.goto('https://localhost:5173/home'); + }); + + test('should display the Start button', async ({ page }) => { + await expect(page.getByTestId('start-login-button')).toBeVisible(); + }); + + test('should navigate to NHS login and complete authentication', async ({ page }) => { + await clickStartButton(page); + await expect(page).toHaveURL('https://access.sandpit.signin.nhs.uk/login'); + + await fillNhsLoginCredentials(page); + await clickContinueButton(page); + + await handleOtpIfPrompted(page); + + // Verify we're back on the app after authentication + await expect(page).toHaveURL(/https:\/\/localhost:5173/); + }); + + test('should display Confirm Details page with user information', async ({ page }) => { + await clickStartButton(page); + await expect(page).toHaveURL('https://access.sandpit.signin.nhs.uk/login'); + + await fillNhsLoginCredentials(page); + await clickContinueButton(page); + + await handleOtpIfPrompted(page); + + // Wait for redirect back to app + await page.waitForURL('https://localhost:5173/nhs-optOut'); + + // Verify the Next button is visible on the Confirm Details page + await expect(page.getByRole('button', { name: /next/i })).toBeVisible(); + + // Verify the summary list contains the expected user details + const summaryList = page.locator('dl.nhsuk-summary-list'); + await expect(summaryList).toBeVisible(); + + // Verify Name field + await expect( + summaryList + .locator('.nhsuk-summary-list__row', { has: page.locator('dt:has-text("Name")') }) + .locator('dd.nhsuk-summary-list__value') + ).toHaveText('Mona, MILLAR'); + + // Verify Email field + await expect( + summaryList + .locator('.nhsuk-summary-list__row', { has: page.locator('dt:has-text("Email")') }) + .locator('dd.nhsuk-summary-list__value') + ).toHaveText('testuserlive@demo.signin.nhs.uk'); + + // Verify Mobile Number field + await expect( + summaryList + .locator( + '.nhsuk-summary-list__row', + { has: page.locator('dt:has-text("Mobile Number")') } + ) + .locator('dd.nhsuk-summary-list__value') + ).toHaveText('+447887510886'); + }); + + test('should navigate to Make your Choice page after clicking Next button', async ({ page }) => { + await clickStartButton(page); + await expect(page).toHaveURL('https://access.sandpit.signin.nhs.uk/login'); + + await fillNhsLoginCredentials(page); + await clickContinueButton(page); + + await handleOtpIfPrompted(page); + + // Wait for redirect back to app + await page.waitForURL('https://localhost:5173/nhs-optOut'); + + // Click the Next button on the Confirm Details page + await page.getByRole('button', { name: /next/i }).click(); + + // Wait for navigation to complete + await page.waitForLoadState('networkidle'); + + // Verify that we're on the Make your Choice page (still same route) + await expect(page).toHaveURL('https://localhost:5173/nhs-optOut'); + + // Verify page content has changed to Make your Choice + await expect(page.locator('h4:has-text("Make your Choice")')).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/LondonDataServices.IDecide.Portal.Client/tests/setup/auth.setup.tffs b/LondonDataServices.IDecide.Portal.Client/tests/setup/auth.setup.tffs new file mode 100644 index 00000000..b7681d8c --- /dev/null +++ b/LondonDataServices.IDecide.Portal.Client/tests/setup/auth.setup.tffs @@ -0,0 +1,32 @@ +import { test as setup, expect } from '@playwright/test'; +import { + clickStartButton, + fillNhsLoginCredentials, + clickContinueButton, + handleOtpIfPrompted +} from '../helpers/helper'; + +const authFile = 'playwright/.auth/user.json'; + +setup('authenticate with NHS Login', async ({ page }) => { + // Navigate to home page + await page.goto('https://localhost:5173/home'); + + // Perform NHS Login authentication + await clickStartButton(page); + await expect(page).toHaveURL('https://access.sandpit.signin.nhs.uk/login'); + + await fillNhsLoginCredentials(page); + await clickContinueButton(page); + + // Handle OTP if prompted (will remember device) + await handleOtpIfPrompted(page); + + // Wait for redirect back to app + await page.waitForURL(/https:\/\/localhost:5173/); + + // Save authenticated state including cookies and localStorage + await page.context().storageState({ path: authFile }); + + console.log('? Authentication completed and saved to', authFile); +}); \ No newline at end of file