From b7a5c85d0c684bd27cb2df90bd63967c4a95819d Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Tue, 3 Dec 2024 14:24:04 +0000 Subject: [PATCH 01/87] add forgot-your-username-or-password component --- frontend/src/app/app-routing.module.ts | 6 ++ frontend/src/app/app.module.ts | 2 + ...t-your-username-or-password.component.html | 47 ++++++++++++++++ ...t-your-username-or-password.component.scss | 3 + ...our-username-or-password.component.spec.ts | 56 +++++++++++++++++++ ...got-your-username-or-password.component.ts | 28 ++++++++++ 6 files changed, 142 insertions(+) create mode 100644 frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html create mode 100644 frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.scss create mode 100644 frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.spec.ts create mode 100644 frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.ts diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 1be4232f4f..fadb159655 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -29,6 +29,7 @@ import { AdminComponent } from '@features/admin/admin.component'; import { AscWdsCertificateComponent } from '@features/dashboard/asc-wds-certificate/asc-wds-certificate.component'; import { FirstLoginPageComponent } from '@features/first-login-page/first-login-page.component'; import { ForgotYourPasswordComponent } from '@features/forgot-your-password/forgot-your-password.component'; +import { ForgotYourUsernameOrPasswordComponent } from '@features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component'; import { LoginComponent } from '@features/login/login.component'; import { LogoutComponent } from '@features/logout/logout.component'; import { MigratedUserTermsConditionsComponent } from '@features/migrated-user-terms-conditions/migrated-user-terms-conditions.component'; @@ -89,6 +90,11 @@ const routes: Routes = [ component: ForgotYourPasswordComponent, data: { title: 'Forgotten Password' }, }, + { + path: 'forgot-your-username-or-password', + component: ForgotYourUsernameOrPasswordComponent, + data: { title: 'Forgot Your Username Or Password' }, + }, { path: 'reset-password', component: ResetPasswordComponent, diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 3e2d966899..7a0b50b26b 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -92,6 +92,7 @@ import { StaffMismatchBannerComponent } from './features/dashboard/home-tab/staf import { MigratedUserTermsConditionsComponent } from './features/migrated-user-terms-conditions/migrated-user-terms-conditions.component'; import { SatisfactionSurveyComponent } from './features/satisfaction-survey/satisfaction-survey.component'; import { SentryErrorHandler } from './SentryErrorHandler.component'; +import { ForgotYourUsernameOrPasswordComponent } from './features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component'; @NgModule({ declarations: [ @@ -133,6 +134,7 @@ import { SentryErrorHandler } from './SentryErrorHandler.component'; LinkToParentComponent, ParentWorkplaceAccounts, DeleteWorkplaceComponent, + ForgotYourUsernameOrPasswordComponent, ], imports: [ Angulartics2Module.forRoot({ diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html new file mode 100644 index 0000000000..96658d5961 --- /dev/null +++ b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html @@ -0,0 +1,47 @@ +
+
+
+
+ +

Forgot your username or password?

+
+
+
+ + +
+
+ + +
+
+
+ + +

+ Request a link to reset your password + and then come back here to find your username. Alternatively, call the ASC-WDS Support Team on + 0113 241 0969 for help. +

+
+ +
+ + or + Back to sign in +
+
+
+
diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.scss b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.scss new file mode 100644 index 0000000000..218cb11443 --- /dev/null +++ b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.scss @@ -0,0 +1,3 @@ +.govuk-button-group { + gap: 140px; +} diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.spec.ts b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.spec.ts new file mode 100644 index 0000000000..f5f711ee3a --- /dev/null +++ b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.spec.ts @@ -0,0 +1,56 @@ +import { ForgotYourUsernameOrPasswordComponent } from './forgot-your-username-or-password.component'; +import { render, within } from '@testing-library/angular'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { SharedModule } from '@shared/shared.module'; + +fdescribe('ForgotYourUsernameOrPasswordComponent', () => { + const setup = async () => { + const setupTools = await render(ForgotYourUsernameOrPasswordComponent, { + imports: [FormsModule, ReactiveFormsModule, SharedModule], + }); + + const component = setupTools.fixture.componentInstance; + + return { ...setupTools, component }; + }; + + it('should create', async () => { + const { component } = await setup(); + + expect(component).toBeTruthy(); + }); + + describe('rendering', () => { + it('should show a page heading', async () => { + const { getByRole } = await setup(); + + expect(getByRole('heading', { name: 'Forgot your username or password?' })).toBeTruthy(); + }); + + it('should show radio buttons to choose from Username or Password', async () => { + const { getByRole } = await setup(); + + expect(getByRole('radio', { name: 'Username' })).toBeTruthy(); + expect(getByRole('radio', { name: 'Password' })).toBeTruthy(); + }); + + it('it should show an reveal text of "Forgot both?"', async () => { + const { getByTestId } = await setup(); + + const revealText = getByTestId('reveal-text'); + const expectedText = + 'Request a link to reset your password and then come back here to find your username. Alternatively, call the ASC-WDS Support Team on 0113 241 0969 for help.'; + + expect(revealText).toBeTruthy(); + expect(within(revealText).getByText('Forgot both?')).toBeTruthy(); + expect(revealText.textContent).toContain(expectedText); + }); + + it('should show a "Continue" CTA button and a "Back to sign in" link', async () => { + const { getByText, getByRole } = await setup(); + + expect(getByRole('button', { name: 'Continue' })).toBeTruthy(); + expect(getByText('Back to sign in')).toBeTruthy(); + }); + }); +}); diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.ts b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.ts new file mode 100644 index 0000000000..aeec0172c6 --- /dev/null +++ b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.ts @@ -0,0 +1,28 @@ +import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { ErrorDetails } from '@core/model/errorSummary.model'; +import { ErrorSummaryService } from '@core/services/error-summary.service'; + +@Component({ + selector: 'app-forgot-your-username-or-password', + templateUrl: './forgot-your-username-or-password.component.html', + styleUrls: ['./forgot-your-username-or-password.component.scss'], +}) +export class ForgotYourUsernameOrPasswordComponent implements OnInit, AfterViewInit { + @ViewChild('formEl') formEl: ElementRef; + public form: UntypedFormGroup; + public submitted = false; + public formErrorsMap: Array; + + constructor(private formBuilder: UntypedFormBuilder, private errorSummaryService: ErrorSummaryService) {} + + ngOnInit(): void { + this.form = this.formBuilder.group({ + usernameOrPassword: [null, { updateOn: 'submit' }], + }); + } + + ngAfterViewInit(): void {} + + onSubmit(): void {} +} From 0c4156b22d14079fd68de870177abdfc2bc96158 Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Tue, 3 Dec 2024 15:26:37 +0000 Subject: [PATCH 02/87] start adding validation --- ...t-your-username-or-password.component.html | 71 ++++++++++++------- ...t-your-username-or-password.component.scss | 6 ++ ...our-username-or-password.component.spec.ts | 30 +++++++- ...got-your-username-or-password.component.ts | 48 +++++++++++-- 4 files changed, 123 insertions(+), 32 deletions(-) diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html index 96658d5961..f3205e4d52 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html +++ b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html @@ -1,3 +1,11 @@ + + +
@@ -5,37 +13,46 @@

Forgot your username or password?

-
-
- - -
-
- - +
+

+ Error: Select username or password +

+
+
+ + +
+
+ + +
- -

- Request a link to reset your password - and then come back here to find your username. Alternatively, call the ASC-WDS Support Team on - 0113 241 0969 for help. -

-
+
+ +

+ Request a link to reset your password + and then come back here to find your username. Alternatively, call the ASC-WDS Support Team on + 0113 241 0969 for help. +

+
+
diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.scss b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.scss index 218cb11443..f986520861 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.scss +++ b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.scss @@ -1,3 +1,9 @@ +@import 'govuk-frontend/govuk/base'; + .govuk-button-group { gap: 140px; } + +.detail-wrapper { + margin-right: govuk-spacing(-8); +} diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.spec.ts b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.spec.ts index f5f711ee3a..aa3233847d 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.spec.ts +++ b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.spec.ts @@ -2,16 +2,31 @@ import { ForgotYourUsernameOrPasswordComponent } from './forgot-your-username-or import { render, within } from '@testing-library/angular'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { SharedModule } from '@shared/shared.module'; +import userEvent from '@testing-library/user-event'; +import { ActivatedRoute, Router } from '@angular/router'; +import { getTestBed } from '@angular/core/testing'; fdescribe('ForgotYourUsernameOrPasswordComponent', () => { const setup = async () => { const setupTools = await render(ForgotYourUsernameOrPasswordComponent, { imports: [FormsModule, ReactiveFormsModule, SharedModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: {}, + }, + }, + ], }); const component = setupTools.fixture.componentInstance; - return { ...setupTools, component }; + const injector = getTestBed(); + const router = injector.inject(Router) as Router; + const routerSpy = spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); + + return { ...setupTools, component, routerSpy }; }; it('should create', async () => { @@ -53,4 +68,17 @@ fdescribe('ForgotYourUsernameOrPasswordComponent', () => { expect(getByText('Back to sign in')).toBeTruthy(); }); }); + + describe('form submit and navigation', () => { + describe('error', () => { + it('should show an error message on submit if neither of radio buttons were selected', async () => { + const { fixture, getByText, getByRole } = await setup(); + + userEvent.click(getByRole('button', { name: 'Continue' })); + fixture.detectChanges(); + + expect(getByText('There is a problem')).toBeTruthy(); + }); + }); + }); }); diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.ts b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.ts index aeec0172c6..172e79b574 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.ts +++ b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.ts @@ -1,5 +1,5 @@ import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'; -import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { ErrorDetails } from '@core/model/errorSummary.model'; import { ErrorSummaryService } from '@core/services/error-summary.service'; @@ -14,15 +14,55 @@ export class ForgotYourUsernameOrPasswordComponent implements OnInit, AfterViewI public submitted = false; public formErrorsMap: Array; + private nextRoute: string[]; + constructor(private formBuilder: UntypedFormBuilder, private errorSummaryService: ErrorSummaryService) {} ngOnInit(): void { this.form = this.formBuilder.group({ - usernameOrPassword: [null, { updateOn: 'submit' }], + usernameOrPassword: [null, { updateOn: 'submit', validators: Validators.required }], }); + this.setupFormErrorsMap(); + } + + ngAfterViewInit(): void { + this.errorSummaryService.formEl$.next(this.formEl); + } + + public setupFormErrorsMap(): void { + this.formErrorsMap = [ + { + item: 'usernameOrPassword', + type: [ + { + name: 'required', + message: 'Select username or password', + }, + ], + }, + ]; } - ngAfterViewInit(): void {} + get usernameOrPassword(): AbstractControl { + return this.form.get('usernameOrPassword'); + } + + onSubmit(): void { + this.submitted = true; + + switch (this.usernameOrPassword.value) { + case 'password': + this.nextRoute = ['/forgot-your-password']; + this.navigate(); + break; + case 'username': + this.nextRoute = ['/forgot-your-username']; + this.navigate(); + break; + default: + return; + } + } - onSubmit(): void {} + navigate() {} } From 36fee5f4c234a2cf20e2b2a8d7a158d6f41e55fa Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Wed, 4 Dec 2024 09:59:36 +0000 Subject: [PATCH 03/87] add unit tests for navigation --- ...t-your-username-or-password.component.html | 6 +- ...our-username-or-password.component.spec.ts | 61 ++++++++++++++----- ...got-your-username-or-password.component.ts | 27 ++++---- 3 files changed, 64 insertions(+), 30 deletions(-) diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html index f3205e4d52..64299d25ba 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html +++ b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html @@ -9,7 +9,7 @@
-
+

Forgot your username or password?

@@ -57,7 +57,9 @@

Forgot your username or password?

diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.spec.ts b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.spec.ts index aa3233847d..0a1114c9f3 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.spec.ts +++ b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.spec.ts @@ -5,11 +5,12 @@ import { SharedModule } from '@shared/shared.module'; import userEvent from '@testing-library/user-event'; import { ActivatedRoute, Router } from '@angular/router'; import { getTestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; fdescribe('ForgotYourUsernameOrPasswordComponent', () => { const setup = async () => { const setupTools = await render(ForgotYourUsernameOrPasswordComponent, { - imports: [FormsModule, ReactiveFormsModule, SharedModule], + imports: [FormsModule, ReactiveFormsModule, SharedModule, RouterTestingModule], providers: [ { provide: ActivatedRoute, @@ -50,34 +51,64 @@ fdescribe('ForgotYourUsernameOrPasswordComponent', () => { }); it('it should show an reveal text of "Forgot both?"', async () => { - const { getByTestId } = await setup(); + const { getByTestId, getByText } = await setup(); - const revealText = getByTestId('reveal-text'); - const expectedText = - 'Request a link to reset your password and then come back here to find your username. Alternatively, call the ASC-WDS Support Team on 0113 241 0969 for help.'; + const revealTextElement = getByTestId('reveal-text'); + const hiddenText = + 'Request a link to reset your password and then come back here to find your username. ' + + 'Alternatively, call the ASC-WDS Support Team on 0113 241 0969 for help.'; - expect(revealText).toBeTruthy(); - expect(within(revealText).getByText('Forgot both?')).toBeTruthy(); - expect(revealText.textContent).toContain(expectedText); + expect(revealTextElement).toBeTruthy(); + expect(within(revealTextElement).getByText('Forgot both?')).toBeTruthy(); + expect(revealTextElement.textContent).toContain(hiddenText); + + const linkToResetPassword = getByText('Request a link to reset your password'); + expect(linkToResetPassword.getAttribute('href')).toEqual('/forgot-your-password'); }); it('should show a "Continue" CTA button and a "Back to sign in" link', async () => { const { getByText, getByRole } = await setup(); expect(getByRole('button', { name: 'Continue' })).toBeTruthy(); - expect(getByText('Back to sign in')).toBeTruthy(); + + const backToSignIn = getByText('Back to sign in'); + expect(backToSignIn).toBeTruthy(); + expect(backToSignIn.getAttribute('href')).toEqual('/login'); }); }); - describe('form submit and navigation', () => { - describe('error', () => { - it('should show an error message on submit if neither of radio buttons were selected', async () => { - const { fixture, getByText, getByRole } = await setup(); + describe('form submit and validation', () => { + describe('on submit', () => { + it('should nagivate to forgot-your-username page if username was selected', async () => { + const { getByRole, routerSpy } = await setup(); + + userEvent.click(getByRole('radio', { name: 'Username' })); + userEvent.click(getByRole('button', { name: 'Continue' })); + + expect(routerSpy).toHaveBeenCalledWith(['/forgot-your-username']); + }); + + it('should nagivate to forgot-your-password page if password was selected', async () => { + const { getByRole, routerSpy } = await setup(); + userEvent.click(getByRole('radio', { name: 'Password' })); userEvent.click(getByRole('button', { name: 'Continue' })); - fixture.detectChanges(); - expect(getByText('There is a problem')).toBeTruthy(); + expect(routerSpy).toHaveBeenCalledWith(['/forgot-your-password']); + }); + + describe('error', () => { + it('should show an error message if neither of radio buttons were selected', async () => { + const { fixture, getByText, getByRole, getAllByText, routerSpy } = await setup(); + + userEvent.click(getByRole('button', { name: 'Continue' })); + fixture.detectChanges(); + + expect(getByText('There is a problem')).toBeTruthy(); + expect(getAllByText('Select username or password')).toHaveSize(2); + + expect(routerSpy).not.toHaveBeenCalled(); + }); }); }); }); diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.ts b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.ts index 172e79b574..07897ceb97 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.ts +++ b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.ts @@ -1,5 +1,6 @@ import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; import { ErrorDetails } from '@core/model/errorSummary.model'; import { ErrorSummaryService } from '@core/services/error-summary.service'; @@ -16,7 +17,11 @@ export class ForgotYourUsernameOrPasswordComponent implements OnInit, AfterViewI private nextRoute: string[]; - constructor(private formBuilder: UntypedFormBuilder, private errorSummaryService: ErrorSummaryService) {} + constructor( + private router: Router, + private formBuilder: UntypedFormBuilder, + private errorSummaryService: ErrorSummaryService, + ) {} ngOnInit(): void { this.form = this.formBuilder.group({ @@ -43,26 +48,22 @@ export class ForgotYourUsernameOrPasswordComponent implements OnInit, AfterViewI ]; } - get usernameOrPassword(): AbstractControl { - return this.form.get('usernameOrPassword'); - } - onSubmit(): void { this.submitted = true; - switch (this.usernameOrPassword.value) { - case 'password': - this.nextRoute = ['/forgot-your-password']; - this.navigate(); - break; + const selectedOption = this.form.get('usernameOrPassword').value; + + switch (selectedOption) { case 'username': this.nextRoute = ['/forgot-your-username']; - this.navigate(); + break; + case 'password': + this.nextRoute = ['/forgot-your-password']; break; default: return; } - } - navigate() {} + this.router.navigate(this.nextRoute); + } } From f8899825ec9ccc8ece90cee7c66d2ebe35f053a5 Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Wed, 4 Dec 2024 10:43:15 +0000 Subject: [PATCH 04/87] adjust css --- ...forgot-your-username-or-password.component.html | 14 +++++++------- ...forgot-your-username-or-password.component.scss | 13 +++++++++---- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html index 64299d25ba..f0c317eb23 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html +++ b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html @@ -10,22 +10,22 @@
- -

Forgot your username or password?

-
+ +

Forgot your username or password?

+

Error: Select username or password

-
+
@@ -34,9 +34,9 @@

Forgot your username or password?

class="govuk-radios__input" id="forgotPassword" name="usernameOrPassword" + formControlName="usernameOrPassword" type="radio" value="password" - formControlName="usernameOrPassword" />
@@ -57,7 +57,7 @@

Forgot your username or password?

or - Back to sign in
diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.scss b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.scss index f986520861..94bd278b0c 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.scss +++ b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.scss @@ -1,9 +1,14 @@ @import 'govuk-frontend/govuk/base'; -.govuk-button-group { - gap: 140px; +.detail-wrapper, +.govuk-form-group { + @media screen and (min-width: 641px) { + margin-right: govuk-spacing(-8); + } } -.detail-wrapper { - margin-right: govuk-spacing(-8); +.govuk-button-group { + @media screen and (min-width: 641px) { + gap: govuk-spacing(7) + govuk-spacing(9); + } } From f92cbb0e6c30101d3ac57d0facdd03fb7fdfaa21 Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Thu, 5 Dec 2024 09:42:37 +0000 Subject: [PATCH 05/87] amend forgot-your-password component --- .../confirmation/confirmation.component.html | 4 +- .../edit/edit.component.html | 24 +++- .../edit/edit.component.ts | 2 +- .../forgot-your-password.component.spec.ts | 120 ++++++++++++++++++ ...t-your-username-or-password.component.html | 16 ++- ...t-your-username-or-password.component.scss | 6 - ...our-username-or-password.component.spec.ts | 2 +- .../src/assets/scss/components/_buttons.scss | 5 + 8 files changed, 157 insertions(+), 22 deletions(-) create mode 100644 frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts diff --git a/frontend/src/app/features/forgot-your-password/confirmation/confirmation.component.html b/frontend/src/app/features/forgot-your-password/confirmation/confirmation.component.html index ef2cbf6e91..816610577f 100644 --- a/frontend/src/app/features/forgot-your-password/confirmation/confirmation.component.html +++ b/frontend/src/app/features/forgot-your-password/confirmation/confirmation.component.html @@ -1,6 +1,6 @@
-

Reset link sent

+

Password reset link sent

If there's an ASC-WDS account for {{ emailAddress }}, you'll receive an email with a reset password link.

The email should be in your inbox soon, if not, check your spam folder before you @@ -8,7 +8,7 @@

Reset link sent

- Return to sign in + Back to sign in
diff --git a/frontend/src/app/features/forgot-your-password/edit/edit.component.html b/frontend/src/app/features/forgot-your-password/edit/edit.component.html index d849169c39..8bc6518f6d 100644 --- a/frontend/src/app/features/forgot-your-password/edit/edit.component.html +++ b/frontend/src/app/features/forgot-your-password/edit/edit.component.html @@ -10,11 +10,14 @@
- -

Forgot your password?

+ +

Forgot password

-

Enter your username or email address and we will send you a link to reset your password.

+

+ Enter your username or your ASC-WDS email address (this'll be the one you used when you created your account, + unless you've changed it) and we'll send you a link to reset your password. +

Forgot your password? Error: {{ getFormErrorMessage('usernameOrEmail', 'required') }} Forgot your password?
- +
+ + or + Back to sign in +
-

Back to sign in

diff --git a/frontend/src/app/features/forgot-your-password/edit/edit.component.ts b/frontend/src/app/features/forgot-your-password/edit/edit.component.ts index 8dd33b643d..c3b6375446 100644 --- a/frontend/src/app/features/forgot-your-password/edit/edit.component.ts +++ b/frontend/src/app/features/forgot-your-password/edit/edit.component.ts @@ -37,7 +37,7 @@ export class ForgotYourPasswordEditComponent implements OnInit, AfterViewInit { type: [ { name: 'required', - message: 'Enter the username or email address', + message: 'Enter your username or ASC-WDS email address', }, ], }, diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts b/frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts new file mode 100644 index 0000000000..3c3494376f --- /dev/null +++ b/frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts @@ -0,0 +1,120 @@ +import sinon from 'sinon'; +import { AppModule } from 'src/app/app.module'; + +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { getTestBed } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { PasswordResetService } from '@core/services/password-reset.service'; +import { SharedModule } from '@shared/shared.module'; +import { render } from '@testing-library/angular'; +import userEvent from '@testing-library/user-event'; + +import { ForgotYourPasswordComponent } from './forgot-your-password.component'; +import { of } from 'rxjs'; + +describe('ForgotYourPasswordComponent', () => { + const setup = async () => { + const setupTools = await render(ForgotYourPasswordComponent, { + imports: [ + HttpClientTestingModule, + FormsModule, + ReactiveFormsModule, + RouterTestingModule, + SharedModule, + AppModule, + ], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: {}, + }, + }, + ], + }); + + const component = setupTools.fixture.componentInstance; + + const injector = getTestBed(); + const router = injector.inject(Router) as Router; + const routerSpy = spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); + + const passwordResetService = injector.inject(PasswordResetService); + const passwordResetSpy = spyOn(passwordResetService, 'requestPasswordReset').and.returnValue(of(null)); + + return { ...setupTools, component, routerSpy, passwordResetSpy }; + }; + + it('should create', async () => { + const { component } = await setup(); + + expect(component).toBeTruthy(); + }); + + describe('rendering', () => { + it('should show a page heading', async () => { + const { getByRole } = await setup(); + + expect(getByRole('heading', { name: 'Forgot password' })).toBeTruthy(); + }); + + it('should show a textbox to input username or email address', async () => { + const { getByRole } = await setup(); + + expect(getByRole('textbox', { name: 'Username or email address' })).toBeTruthy(); + }); + + it('should show a "Send password reset link" CTA button and a "Back to sign in" link', async () => { + const { getByText, getByRole } = await setup(); + + expect(getByRole('button', { name: 'Send password reset link' })).toBeTruthy(); + + const backToSignIn = getByText('Back to sign in'); + expect(backToSignIn).toBeTruthy(); + expect(backToSignIn.getAttribute('href')).toEqual('/login'); + }); + }); + + describe('form submit and validation', () => { + describe('on submit', () => { + it('should make a request from password reset service', async () => { + const { getByRole, passwordResetSpy } = await setup(); + + userEvent.type(getByRole('textbox'), 'test@example.com'); + userEvent.click(getByRole('button', { name: 'Send password reset link' })); + + expect(passwordResetSpy).toHaveBeenCalledWith('test@example.com'); + }); + + describe('error', () => { + it('should show an error message if no input for textbox', async () => { + const { fixture, getByText, getByRole, getAllByText, passwordResetSpy } = await setup(); + + userEvent.click(getByRole('button', { name: 'Send password reset link' })); + fixture.detectChanges(); + + expect(getByText('There is a problem')).toBeTruthy(); + expect(getAllByText('Enter your username or ASC-WDS email address')).toHaveSize(2); + + expect(passwordResetSpy).not.toHaveBeenCalled(); + }); + }); + }); + }); + + describe('after form submit', async () => { + it('should show a confirmation screen', async () => { + const { fixture, getByRole, getByText } = await setup(); + + userEvent.type(getByRole('textbox'), 'test@example.com'); + userEvent.click(getByRole('button', { name: 'Send password reset link' })); + + fixture.detectChanges(); + + expect(getByRole('heading', { name: 'Password reset link sent' })).toBeTruthy(); + expect(getByText('Back to sign in')).toBeTruthy(); + }); + }); +}); diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html index f0c317eb23..ca03d335db 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html +++ b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html @@ -9,8 +9,8 @@
-
-
+
+

Forgot your username or password?

@@ -41,8 +41,8 @@

Forgot your username or password?

-
-
+ +
@@ -54,10 +54,14 @@

Forgot your username or password?

-
+
or - Back to sign in
diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.scss b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.scss index 94bd278b0c..96f3cdedc6 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.scss +++ b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.scss @@ -6,9 +6,3 @@ margin-right: govuk-spacing(-8); } } - -.govuk-button-group { - @media screen and (min-width: 641px) { - gap: govuk-spacing(7) + govuk-spacing(9); - } -} diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.spec.ts b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.spec.ts index 0a1114c9f3..ed23898474 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.spec.ts +++ b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.spec.ts @@ -7,7 +7,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { getTestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; -fdescribe('ForgotYourUsernameOrPasswordComponent', () => { +describe('ForgotYourUsernameOrPasswordComponent', () => { const setup = async () => { const setupTools = await render(ForgotYourUsernameOrPasswordComponent, { imports: [FormsModule, ReactiveFormsModule, SharedModule, RouterTestingModule], diff --git a/frontend/src/assets/scss/components/_buttons.scss b/frontend/src/assets/scss/components/_buttons.scss index c1cb0cdb5f..be7740796c 100644 --- a/frontend/src/assets/scss/components/_buttons.scss +++ b/frontend/src/assets/scss/components/_buttons.scss @@ -54,6 +54,11 @@ $button-shadow-size: $govuk-border-width-form-element; } } +.govuk-button-group--gap-between { + @extend .govuk-button-group; + column-gap: govuk-spacing(8) * 2; +} + .govuk-button { &.govuk-button--link { background: none; From 83a9d767fa7a8d2581bf04f3a2386806e7371a89 Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Thu, 5 Dec 2024 10:33:55 +0000 Subject: [PATCH 06/87] amend error message retrieval --- .../forgot-your-password/edit/edit.component.html | 4 ++-- .../forgot-your-password/edit/edit.component.ts | 13 ++++++------- .../forgot-your-password.component.spec.ts | 2 +- .../forgot-your-username-or-password.component.html | 2 +- .../forgot-your-username-or-password.component.ts | 5 +++++ 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/features/forgot-your-password/edit/edit.component.html b/frontend/src/app/features/forgot-your-password/edit/edit.component.html index 8bc6518f6d..21b2acbbe0 100644 --- a/frontend/src/app/features/forgot-your-password/edit/edit.component.html +++ b/frontend/src/app/features/forgot-your-password/edit/edit.component.html @@ -27,9 +27,9 @@

Forgot password

- Error: {{ getFormErrorMessage('usernameOrEmail', 'required') }} + Error: {{ getFirstErrorMessage('usernameOrEmail') }} { +fdescribe('ForgotYourPasswordComponent', () => { const setup = async () => { const setupTools = await render(ForgotYourPasswordComponent, { imports: [ diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html index ca03d335db..9d6c11226a 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html +++ b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html @@ -15,7 +15,7 @@

Forgot your username or password?

- Error: Select username or password + Error: {{ getFirstErrorMessage('usernameOrPassword') }}

diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.ts b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.ts index 07897ceb97..b31f69302c 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.ts +++ b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.ts @@ -48,6 +48,11 @@ export class ForgotYourUsernameOrPasswordComponent implements OnInit, AfterViewI ]; } + public getFirstErrorMessage(item: string): string { + const errorType = Object.keys(this.form.get(item).errors)[0]; + return this.errorSummaryService.getFormErrorMessage(item, errorType, this.formErrorsMap); + } + onSubmit(): void { this.submitted = true; From d97fd6a8e4d9cb221dc0c1558f48bd2279ca4594 Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Thu, 5 Dec 2024 10:59:53 +0000 Subject: [PATCH 07/87] adjust css --- .../forgot-your-username-or-password.component.html | 2 +- .../forgot-your-username-or-password.component.scss | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html index 9d6c11226a..8ac71c2e90 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html +++ b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html @@ -12,7 +12,7 @@
-

Forgot your username or password?

+

Forgot your username or password?

Error: {{ getFirstErrorMessage('usernameOrPassword') }} diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.scss b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.scss index 96f3cdedc6..335d671b2b 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.scss +++ b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.scss @@ -1,8 +1,8 @@ @import 'govuk-frontend/govuk/base'; -.detail-wrapper, -.govuk-form-group { - @media screen and (min-width: 641px) { - margin-right: govuk-spacing(-8); - } -} +// .detail-wrapper, +// .govuk-form-group { +// @media screen and (min-width: 641px) { +// margin-right: govuk-spacing(-8); +// } +// } From 127aefd704c454b8c80d6f09a2a094fc63e08cbd Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Thu, 5 Dec 2024 14:25:33 +0000 Subject: [PATCH 08/87] adjust css --- .../forgot-your-password.component.spec.ts | 2 +- .../forgot-your-username-or-password.component.html | 2 +- .../forgot-your-username-or-password.component.scss | 8 -------- .../forgot-your-username-or-password.component.ts | 3 +-- frontend/src/assets/scss/modules/_utils.scss | 6 ++++++ 5 files changed, 9 insertions(+), 12 deletions(-) delete mode 100644 frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.scss diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts b/frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts index 6840a1f001..3c3494376f 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts +++ b/frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts @@ -14,7 +14,7 @@ import userEvent from '@testing-library/user-event'; import { ForgotYourPasswordComponent } from './forgot-your-password.component'; import { of } from 'rxjs'; -fdescribe('ForgotYourPasswordComponent', () => { +describe('ForgotYourPasswordComponent', () => { const setup = async () => { const setupTools = await render(ForgotYourPasswordComponent, { imports: [ diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html index 8ac71c2e90..95e304954e 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html +++ b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html @@ -12,7 +12,7 @@

-

Forgot your username or password?

+

Forgot your username or password?

Error: {{ getFirstErrorMessage('usernameOrPassword') }} diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.scss b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.scss deleted file mode 100644 index 335d671b2b..0000000000 --- a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.scss +++ /dev/null @@ -1,8 +0,0 @@ -@import 'govuk-frontend/govuk/base'; - -// .detail-wrapper, -// .govuk-form-group { -// @media screen and (min-width: 641px) { -// margin-right: govuk-spacing(-8); -// } -// } diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.ts b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.ts index b31f69302c..1897bee9c5 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.ts +++ b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.ts @@ -1,5 +1,5 @@ import { AfterViewInit, Component, ElementRef, OnInit, ViewChild } from '@angular/core'; -import { AbstractControl, UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; import { ErrorDetails } from '@core/model/errorSummary.model'; import { ErrorSummaryService } from '@core/services/error-summary.service'; @@ -7,7 +7,6 @@ import { ErrorSummaryService } from '@core/services/error-summary.service'; @Component({ selector: 'app-forgot-your-username-or-password', templateUrl: './forgot-your-username-or-password.component.html', - styleUrls: ['./forgot-your-username-or-password.component.scss'], }) export class ForgotYourUsernameOrPasswordComponent implements OnInit, AfterViewInit { @ViewChild('formEl') formEl: ElementRef; diff --git a/frontend/src/assets/scss/modules/_utils.scss b/frontend/src/assets/scss/modules/_utils.scss index 9fb486f314..60e64fd01d 100644 --- a/frontend/src/assets/scss/modules/_utils.scss +++ b/frontend/src/assets/scss/modules/_utils.scss @@ -105,6 +105,12 @@ .govuk__nowrap { white-space: nowrap; } +.govuk__nowrap-responsive { + @media screen and (min-width: 641px) { + white-space: nowrap; + } +} + .govuk-util__vertical-align-top { vertical-align: top; } From fb4f23821de68ecaef3cd8106165046ca50f9ed2 Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Thu, 5 Dec 2024 14:54:29 +0000 Subject: [PATCH 09/87] fix an issue about unit test config --- .../forgot-your-password.component.spec.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts b/frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts index 3c3494376f..d7a238a1d3 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts +++ b/frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts @@ -1,6 +1,3 @@ -import sinon from 'sinon'; -import { AppModule } from 'src/app/app.module'; - import { HttpClientTestingModule } from '@angular/common/http/testing'; import { getTestBed } from '@angular/core/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @@ -13,18 +10,14 @@ import userEvent from '@testing-library/user-event'; import { ForgotYourPasswordComponent } from './forgot-your-password.component'; import { of } from 'rxjs'; +import { ForgotYourPasswordEditComponent } from './edit/edit.component'; +import { ForgotYourPasswordConfirmationComponent } from './confirmation/confirmation.component'; describe('ForgotYourPasswordComponent', () => { const setup = async () => { const setupTools = await render(ForgotYourPasswordComponent, { - imports: [ - HttpClientTestingModule, - FormsModule, - ReactiveFormsModule, - RouterTestingModule, - SharedModule, - AppModule, - ], + imports: [HttpClientTestingModule, FormsModule, ReactiveFormsModule, RouterTestingModule, SharedModule], + declarations: [ForgotYourPasswordEditComponent, ForgotYourPasswordConfirmationComponent], providers: [ { provide: ActivatedRoute, From 0b53bcb2744ca2260dd14e81de1371649bd03b1e Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Thu, 5 Dec 2024 14:55:23 +0000 Subject: [PATCH 10/87] sort import --- .../forgot-your-password.component.spec.ts | 7 ++++--- ...forgot-your-username-or-password.component.spec.ts | 11 ++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts b/frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts index d7a238a1d3..17d808fd01 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts +++ b/frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts @@ -1,3 +1,5 @@ +import { of } from 'rxjs'; + import { HttpClientTestingModule } from '@angular/common/http/testing'; import { getTestBed } from '@angular/core/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @@ -8,10 +10,9 @@ import { SharedModule } from '@shared/shared.module'; import { render } from '@testing-library/angular'; import userEvent from '@testing-library/user-event'; -import { ForgotYourPasswordComponent } from './forgot-your-password.component'; -import { of } from 'rxjs'; -import { ForgotYourPasswordEditComponent } from './edit/edit.component'; import { ForgotYourPasswordConfirmationComponent } from './confirmation/confirmation.component'; +import { ForgotYourPasswordEditComponent } from './edit/edit.component'; +import { ForgotYourPasswordComponent } from './forgot-your-password.component'; describe('ForgotYourPasswordComponent', () => { const setup = async () => { diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.spec.ts b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.spec.ts index ed23898474..3edc33bf13 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.spec.ts +++ b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.spec.ts @@ -1,11 +1,12 @@ -import { ForgotYourUsernameOrPasswordComponent } from './forgot-your-username-or-password.component'; -import { render, within } from '@testing-library/angular'; +import { getTestBed } from '@angular/core/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { SharedModule } from '@shared/shared.module'; -import userEvent from '@testing-library/user-event'; import { ActivatedRoute, Router } from '@angular/router'; -import { getTestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; +import { SharedModule } from '@shared/shared.module'; +import { render, within } from '@testing-library/angular'; +import userEvent from '@testing-library/user-event'; + +import { ForgotYourUsernameOrPasswordComponent } from './forgot-your-username-or-password.component'; describe('ForgotYourUsernameOrPasswordComponent', () => { const setup = async () => { From dc1c277909636b064f284d59bac05288f9a1065d Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Thu, 5 Dec 2024 16:49:52 +0000 Subject: [PATCH 11/87] update test content and css --- .../confirmation/confirmation.component.html | 9 ++++++--- .../forgot-your-password.component.spec.ts | 4 ++++ .../forgot-your-username-or-password.component.html | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/features/forgot-your-password/confirmation/confirmation.component.html b/frontend/src/app/features/forgot-your-password/confirmation/confirmation.component.html index 816610577f..349fb4a4f3 100644 --- a/frontend/src/app/features/forgot-your-password/confirmation/confirmation.component.html +++ b/frontend/src/app/features/forgot-your-password/confirmation/confirmation.component.html @@ -1,9 +1,12 @@

-

Password reset link sent

-

If there's an ASC-WDS account for {{ emailAddress }}, you'll receive an email with a reset password link.

+

Password reset link sent

- The email should be in your inbox soon, if not, check your spam folder before you + If there's an ASC-WDS account for {{ emailAddress }}, you'll get an email soon, with a link to reset your + password. +

+

+ The email should go to your inbox, but if not then check your spam folder before you contact us for help.

diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts b/frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts index 17d808fd01..9e1fadeb1f 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts +++ b/frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts @@ -107,7 +107,11 @@ describe('ForgotYourPasswordComponent', () => { fixture.detectChanges(); + const expectedMessage = + "If there's an ASC-WDS account for test@example.com, you'll get an email soon, with a link to reset your password."; + expect(getByRole('heading', { name: 'Password reset link sent' })).toBeTruthy(); + expect(getByText(expectedMessage)).toBeTruthy(); expect(getByText('Back to sign in')).toBeTruthy(); }); }); diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html index 95e304954e..60ede96d56 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html +++ b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html @@ -44,7 +44,7 @@

Forgot your usernam

-
+

Request a link to reset your password From 65be38a6a2058331784d6d1070eeaeddb9376aba Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Fri, 6 Dec 2024 09:31:51 +0000 Subject: [PATCH 12/87] update confirmation screen message --- .../confirmation/confirmation.component.html | 4 ++-- .../forgot-your-password.component.spec.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/features/forgot-your-password/confirmation/confirmation.component.html b/frontend/src/app/features/forgot-your-password/confirmation/confirmation.component.html index 349fb4a4f3..c3384951d6 100644 --- a/frontend/src/app/features/forgot-your-password/confirmation/confirmation.component.html +++ b/frontend/src/app/features/forgot-your-password/confirmation/confirmation.component.html @@ -1,8 +1,8 @@

-
+

Password reset link sent

- If there's an ASC-WDS account for {{ emailAddress }}, you'll get an email soon, with a link to reset your + If there's an ASC-WDS account for {{ emailAddress }} you'll get an email soon, with a link to reset your password.

diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts b/frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts index 9e1fadeb1f..65b506db4c 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts +++ b/frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts @@ -100,7 +100,7 @@ describe('ForgotYourPasswordComponent', () => { describe('after form submit', async () => { it('should show a confirmation screen', async () => { - const { fixture, getByRole, getByText } = await setup(); + const { fixture, getByRole, getByText, getByTestId } = await setup(); userEvent.type(getByRole('textbox'), 'test@example.com'); userEvent.click(getByRole('button', { name: 'Send password reset link' })); @@ -108,10 +108,10 @@ describe('ForgotYourPasswordComponent', () => { fixture.detectChanges(); const expectedMessage = - "If there's an ASC-WDS account for test@example.com, you'll get an email soon, with a link to reset your password."; + "If there's an ASC-WDS account for test@example.com you'll get an email soon, with a link to reset your password."; expect(getByRole('heading', { name: 'Password reset link sent' })).toBeTruthy(); - expect(getByText(expectedMessage)).toBeTruthy(); + expect(getByTestId('confirmation-message').textContent).toContain(expectedMessage); expect(getByText('Back to sign in')).toBeTruthy(); }); }); From f0c4baa3aa02ea48380323f762c2504492b5e119 Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Fri, 6 Dec 2024 09:46:57 +0000 Subject: [PATCH 13/87] add nowrap class to tel number --- .../confirmation/confirmation.component.html | 4 ++-- .../forgot-your-username-or-password.component.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/features/forgot-your-password/confirmation/confirmation.component.html b/frontend/src/app/features/forgot-your-password/confirmation/confirmation.component.html index c3384951d6..7fc8ae5f89 100644 --- a/frontend/src/app/features/forgot-your-password/confirmation/confirmation.component.html +++ b/frontend/src/app/features/forgot-your-password/confirmation/confirmation.component.html @@ -2,8 +2,8 @@

Password reset link sent

- If there's an ASC-WDS account for {{ emailAddress }} you'll get an email soon, with a link to reset your - password. + If there's an ASC-WDS account for {{ emailAddress }} you'll get an email soon, with a link to + reset your password.

The email should go to your inbox, but if not then check your spam folder before you diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html index 60ede96d56..ad8b8894b6 100644 --- a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html +++ b/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html @@ -49,7 +49,7 @@

Forgot your usernam

Request a link to reset your password and then come back here to find your username. Alternatively, call the ASC-WDS Support Team on - 0113 241 0969 for help. + 0113 241 0969 for help.

From c5fc166211bf0e4cea2de936c84b5fe83894c75e Mon Sep 17 00:00:00 2001 From: Sabrina Date: Fri, 6 Dec 2024 11:41:35 +0000 Subject: [PATCH 14/87] Add validation check for at signs in username --- .../features/login/login.component.spec.ts | 117 ++++++++++++++++-- .../src/app/features/login/login.component.ts | 51 +++++++- 2 files changed, 157 insertions(+), 11 deletions(-) diff --git a/frontend/src/app/features/login/login.component.spec.ts b/frontend/src/app/features/login/login.component.spec.ts index d41d0a7544..c89fea2f91 100644 --- a/frontend/src/app/features/login/login.component.spec.ts +++ b/frontend/src/app/features/login/login.component.spec.ts @@ -9,17 +9,19 @@ import { MockAuthService } from '@core/test-utils/MockAuthService'; import { MockUserService } from '@core/test-utils/MockUserService'; import { FeatureFlagsService } from '@shared/services/feature-flags.service'; import { SharedModule } from '@shared/shared.module'; -import { render } from '@testing-library/angular'; +import { fireEvent, getByTestId, render, within } from '@testing-library/angular'; import { throwError } from 'rxjs'; import { LoginComponent } from './login.component'; +import { ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms'; -describe('LoginComponent', () => { +fdescribe('LoginComponent', () => { async function setup(isAdmin = false, employerTypeSet = true, isAuthenticated = true) { - const { fixture, getAllByText, getByText, queryByText, getByTestId } = await render(LoginComponent, { - imports: [SharedModule, RouterModule, RouterTestingModule, HttpClientTestingModule], + const setupTools = await render(LoginComponent, { + imports: [SharedModule, RouterModule, RouterTestingModule, HttpClientTestingModule, ReactiveFormsModule], providers: [ FeatureFlagsService, + UntypedFormBuilder, { provide: AuthService, useFactory: MockAuthService.factory(true, isAdmin, employerTypeSet), @@ -53,14 +55,13 @@ describe('LoginComponent', () => { authSpy = spyOn(authService, 'authenticate').and.returnValue(throwError(mockErrorResponse)); } + const fixture = setupTools.fixture; const component = fixture.componentInstance; + return { component, fixture, - getAllByText, - getByText, - queryByText, - getByTestId, + ...setupTools, spy, authSpy, }; @@ -71,6 +72,85 @@ describe('LoginComponent', () => { expect(component).toBeTruthy(); }); + describe('username', () => { + it('should show the username hint', async () => { + const { component, getByTestId } = await setup(); + + const usernameHint = getByTestId('username-hint'); + const hintText = 'You cannot use an email address to sign in'; + + expect(within(usernameHint).getByText(hintText)).toBeTruthy(); + }); + }); + + describe('password', () => { + it('should show the password as password field when show password is false', async () => { + const { component, fixture, getByTestId } = await setup(); + + component.showPassword = false; + + fixture.detectChanges(); + + const passwordInput = getByTestId('password'); + + expect(passwordInput.getAttribute('type')).toEqual('password'); + }); + + it('should show the password as text field when show password is true', async () => { + const { component, fixture, getByTestId } = await setup(); + + component.showPassword = true; + + fixture.detectChanges(); + + const passwordInput = getByTestId('password'); + + expect(passwordInput.getAttribute('type')).toEqual('text'); + }); + + it("should initially show 'Show password' text for the password toggle", async () => { + const { component, getByTestId } = await setup(); + + const passwordToggle = getByTestId('password-toggle'); + const toggleText = 'Show password'; + + expect(within(passwordToggle).getByText(toggleText)).toBeTruthy(); + }); + + it("should show 'Hide password' text for the password toggle when 'Show password' is clicked", async () => { + const { component, fixture, getByTestId, getByText } = await setup(); + + const passwordToggle = getByTestId('password-toggle'); + const showToggleText = 'Show password'; + const hideToggleText = 'Hide password'; + + fireEvent.click(getByText(showToggleText)); + fixture.detectChanges(); + + expect(within(passwordToggle).getByText(hideToggleText)).toBeTruthy(); + }); + }); + + it('should show the link to forgot username or password', async () => { + const { component, getByTestId } = await setup(); + + const forgotUsernamePasswordText = 'Forgot your username or password?'; + const forgotUsernamePasswordLink = getByTestId('forgot-username-password'); + + expect(within(forgotUsernamePasswordLink).getByText(forgotUsernamePasswordText)).toBeTruthy(); + expect(forgotUsernamePasswordLink.getAttribute('href')).toEqual('/forgot-your-username-or-password'); + }); + + it('should show the link to create an account', async () => { + const { component, getByTestId } = await setup(); + + const createAccountText = 'Create an account'; + const createAccountLink = getByTestId('create-account'); + + expect(within(createAccountLink).getByText(createAccountText)).toBeTruthy(); + expect(createAccountLink.getAttribute('href')).toEqual('/registration/create-account'); + }); + it('should send you to dashboard on login as user', async () => { const { component, fixture, spy, authSpy } = await setup(); @@ -165,5 +245,26 @@ describe('LoginComponent', () => { fixture.detectChanges(); expect(getAllByText('Your username or your password is incorrect')).toBeTruthy(); }); + + it('should not let you sign in with a username with special characters', async () => { + const { component, fixture, getAllByText, getByTestId } = await setup(); + + const signInButton = within(getByTestId('signinButton')).getByText('Sign in'); + const form = component.form; + + component.form.markAsDirty(); + form.controls['username'].setValue('username@123.com'); + form.controls['username'].markAsDirty(); + component.form.get('password').setValue('1'); + component.form.get('password').markAsDirty(); + + fireEvent.click(signInButton); + fixture.detectChanges(); + + expect(form.invalid).toBeTruthy(); + expect( + getAllByText("You've entered an @ symbol (remember, your username cannot be an email address)"), + ).toBeTruthy(); + }); }); }); diff --git a/frontend/src/app/features/login/login.component.ts b/frontend/src/app/features/login/login.component.ts index e0e48e232c..a5cfd1f773 100644 --- a/frontend/src/app/features/login/login.component.ts +++ b/frontend/src/app/features/login/login.component.ts @@ -1,6 +1,13 @@ import { HttpErrorResponse } from '@angular/common/http'; import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'; -import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { + AbstractControl, + UntypedFormBuilder, + UntypedFormGroup, + ValidationErrors, + ValidatorFn, + Validators, +} from '@angular/forms'; import { Router } from '@angular/router'; import { ErrorDefinition, ErrorDetails } from '@core/model/errorSummary.model'; import { AuthService } from '@core/services/auth.service'; @@ -14,6 +21,7 @@ import { Subscription } from 'rxjs'; @Component({ selector: 'app-login', templateUrl: './login.component.html', + styleUrls: ['./login.component.scss'], }) export class LoginComponent implements OnInit, OnDestroy, AfterViewInit { @ViewChild('formEl') formEl: ElementRef; @@ -23,6 +31,8 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewInit { public formErrorsMap: Array; public serverErrorsMap: Array; public serverError: string; + public showPassword: boolean = false; + public regex = "^[A-Za-z0-9._'%+-]*$"; constructor( private idleService: IdleService, @@ -36,8 +46,20 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewInit { ngOnInit() { this.form = this.formBuilder.group({ - username: [null, { validators: [Validators.required], updateOn: 'submit' }], - password: [null, { validators: [Validators.required], updateOn: 'submit' }], + username: [ + null, + { + validators: [Validators.required, this.checkAtSignInUsername()], + updateOn: 'submit', + }, + ], + password: [ + null, + { + validators: [Validators.required], + updateOn: 'submit', + }, + ], }); this.setupFormErrorsMap(); @@ -52,6 +74,20 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewInit { this.subscriptions.unsubscribe(); } + public checkAtSignInUsername(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const value = control.value; + + if (!value) { + return null; + } + + const userNameHasAtSign = /@/.test(value); + + return userNameHasAtSign ? { isUsernameNotEmail: true } : null; + }; + } + public setupFormErrorsMap(): void { this.formErrorsMap = [ { @@ -61,6 +97,10 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewInit { name: 'required', message: 'Enter your username', }, + { + name: 'isUsernameNotEmail', + message: "You've entered an @ symbol (remember, your username cannot be an email address)", + }, ], }, { @@ -154,4 +194,9 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewInit { ), ); } + + public setShowPassword(event: Event): void { + event.preventDefault(); + this.showPassword = !this.showPassword; + } } From 53c8e2458465c5f844856ef5efad07b426658f88 Mon Sep 17 00:00:00 2001 From: Sabrina Date: Fri, 6 Dec 2024 12:59:38 +0000 Subject: [PATCH 15/87] Add password toggle and showadditional error message --- .../app/features/login/login.component.html | 40 ++++++++++++++++--- .../app/features/login/login.component.scss | 5 +++ .../features/login/login.component.spec.ts | 20 +++++----- .../src/app/features/login/login.component.ts | 8 ++-- 4 files changed, 53 insertions(+), 20 deletions(-) create mode 100644 frontend/src/app/features/login/login.component.scss diff --git a/frontend/src/app/features/login/login.component.html b/frontend/src/app/features/login/login.component.html index 4a0117e46d..16db339ac8 100644 --- a/frontend/src/app/features/login/login.component.html +++ b/frontend/src/app/features/login/login.component.html @@ -17,14 +17,25 @@

Sign in

[class.govuk-form-group--error]="(form.get('username').errors || serverError) && submitted" > +
+ You cannot use an email address to sign in +
+ - Error: - {{ getFormErrorMessage('username', 'required') }} + + Error: + {{ getFormErrorMessage('username', 'required') }} + + + + Error: + {{ getFormErrorMessage('username', 'atSignInUsername') }} + Sign in > Error: {{ getFormErrorMessage('password', 'required') }} + + {{ showPassword ? 'Hide' : 'Show' }} password
- + diff --git a/frontend/src/app/features/login/login.component.scss b/frontend/src/app/features/login/login.component.scss new file mode 100644 index 0000000000..faf12e9c51 --- /dev/null +++ b/frontend/src/app/features/login/login.component.scss @@ -0,0 +1,5 @@ +@import 'govuk-frontend/govuk/base'; + +.asc-colour-black { + color: $govuk-text-colour; +} diff --git a/frontend/src/app/features/login/login.component.spec.ts b/frontend/src/app/features/login/login.component.spec.ts index c89fea2f91..4fe0de1d70 100644 --- a/frontend/src/app/features/login/login.component.spec.ts +++ b/frontend/src/app/features/login/login.component.spec.ts @@ -9,13 +9,13 @@ import { MockAuthService } from '@core/test-utils/MockAuthService'; import { MockUserService } from '@core/test-utils/MockUserService'; import { FeatureFlagsService } from '@shared/services/feature-flags.service'; import { SharedModule } from '@shared/shared.module'; -import { fireEvent, getByTestId, render, within } from '@testing-library/angular'; +import { fireEvent, render, within } from '@testing-library/angular'; import { throwError } from 'rxjs'; import { LoginComponent } from './login.component'; import { ReactiveFormsModule, UntypedFormBuilder } from '@angular/forms'; -fdescribe('LoginComponent', () => { +describe('LoginComponent', () => { async function setup(isAdmin = false, employerTypeSet = true, isAuthenticated = true) { const setupTools = await render(LoginComponent, { imports: [SharedModule, RouterModule, RouterTestingModule, HttpClientTestingModule, ReactiveFormsModule], @@ -40,7 +40,7 @@ fdescribe('LoginComponent', () => { spy.and.returnValue(Promise.resolve(true)); const authService = injector.inject(AuthService) as AuthService; - let authSpy; + let authSpy: any; if (isAuthenticated) { authSpy = spyOn(authService, 'authenticate'); authSpy.and.callThrough(); @@ -74,7 +74,7 @@ fdescribe('LoginComponent', () => { describe('username', () => { it('should show the username hint', async () => { - const { component, getByTestId } = await setup(); + const { getByTestId } = await setup(); const usernameHint = getByTestId('username-hint'); const hintText = 'You cannot use an email address to sign in'; @@ -109,7 +109,7 @@ fdescribe('LoginComponent', () => { }); it("should initially show 'Show password' text for the password toggle", async () => { - const { component, getByTestId } = await setup(); + const { getByTestId } = await setup(); const passwordToggle = getByTestId('password-toggle'); const toggleText = 'Show password'; @@ -118,7 +118,7 @@ fdescribe('LoginComponent', () => { }); it("should show 'Hide password' text for the password toggle when 'Show password' is clicked", async () => { - const { component, fixture, getByTestId, getByText } = await setup(); + const { fixture, getByTestId, getByText } = await setup(); const passwordToggle = getByTestId('password-toggle'); const showToggleText = 'Show password'; @@ -132,7 +132,7 @@ fdescribe('LoginComponent', () => { }); it('should show the link to forgot username or password', async () => { - const { component, getByTestId } = await setup(); + const { getByTestId } = await setup(); const forgotUsernamePasswordText = 'Forgot your username or password?'; const forgotUsernamePasswordLink = getByTestId('forgot-username-password'); @@ -142,7 +142,7 @@ fdescribe('LoginComponent', () => { }); it('should show the link to create an account', async () => { - const { component, getByTestId } = await setup(); + const { getByTestId } = await setup(); const createAccountText = 'Create an account'; const createAccountLink = getByTestId('create-account'); @@ -263,8 +263,8 @@ fdescribe('LoginComponent', () => { expect(form.invalid).toBeTruthy(); expect( - getAllByText("You've entered an @ symbol (remember, your username cannot be an email address)"), - ).toBeTruthy(); + getAllByText("You've entered an @ symbol (remember, your username cannot be an email address)").length, + ).toBe(2); }); }); }); diff --git a/frontend/src/app/features/login/login.component.ts b/frontend/src/app/features/login/login.component.ts index a5cfd1f773..d820415974 100644 --- a/frontend/src/app/features/login/login.component.ts +++ b/frontend/src/app/features/login/login.component.ts @@ -49,7 +49,7 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewInit { username: [ null, { - validators: [Validators.required, this.checkAtSignInUsername()], + validators: [Validators.required, this.checkUsernameForAtSign()], updateOn: 'submit', }, ], @@ -74,7 +74,7 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewInit { this.subscriptions.unsubscribe(); } - public checkAtSignInUsername(): ValidatorFn { + public checkUsernameForAtSign(): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { const value = control.value; @@ -84,7 +84,7 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewInit { const userNameHasAtSign = /@/.test(value); - return userNameHasAtSign ? { isUsernameNotEmail: true } : null; + return userNameHasAtSign ? { atSignInUsername: true } : null; }; } @@ -98,7 +98,7 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewInit { message: 'Enter your username', }, { - name: 'isUsernameNotEmail', + name: 'atSignInUsername', message: "You've entered an @ symbol (remember, your username cannot be an email address)", }, ], From d546629e7d1f628562eca672bf3770a9cd10a932 Mon Sep 17 00:00:00 2001 From: Sabrina Date: Mon, 9 Dec 2024 09:33:51 +0000 Subject: [PATCH 16/87] Create new page to display username --- frontend/src/app/app-routing.module.ts | 6 +++++ frontend/src/app/app.module.ts | 2 ++ .../found-username.component.html | 0 .../found-username.component.spec.ts | 23 +++++++++++++++++++ .../found-username.component.ts | 11 +++++++++ 5 files changed, 42 insertions(+) create mode 100644 frontend/src/app/features/found-username/found-username.component.html create mode 100644 frontend/src/app/features/found-username/found-username.component.spec.ts create mode 100644 frontend/src/app/features/found-username/found-username.component.ts diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index fadb159655..e675fabe9f 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -30,6 +30,7 @@ import { AscWdsCertificateComponent } from '@features/dashboard/asc-wds-certific import { FirstLoginPageComponent } from '@features/first-login-page/first-login-page.component'; import { ForgotYourPasswordComponent } from '@features/forgot-your-password/forgot-your-password.component'; import { ForgotYourUsernameOrPasswordComponent } from '@features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component'; +import { FoundUsernameComponent } from '@features/found-username/found-username.component'; import { LoginComponent } from '@features/login/login.component'; import { LogoutComponent } from '@features/logout/logout.component'; import { MigratedUserTermsConditionsComponent } from '@features/migrated-user-terms-conditions/migrated-user-terms-conditions.component'; @@ -105,6 +106,11 @@ const routes: Routes = [ component: SatisfactionSurveyComponent, data: { title: 'Satisfaction Survey' }, }, + { + path: 'username-found', + component: FoundUsernameComponent, + data: { title: 'Username Found' }, + }, ], }, { diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 7a0b50b26b..13d4b92418 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -93,6 +93,7 @@ import { MigratedUserTermsConditionsComponent } from './features/migrated-user-t import { SatisfactionSurveyComponent } from './features/satisfaction-survey/satisfaction-survey.component'; import { SentryErrorHandler } from './SentryErrorHandler.component'; import { ForgotYourUsernameOrPasswordComponent } from './features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component'; +import { FoundUsernameComponent } from '@features/found-username/found-username.component'; @NgModule({ declarations: [ @@ -135,6 +136,7 @@ import { ForgotYourUsernameOrPasswordComponent } from './features/forgot-your-pa ParentWorkplaceAccounts, DeleteWorkplaceComponent, ForgotYourUsernameOrPasswordComponent, + FoundUsernameComponent, ], imports: [ Angulartics2Module.forRoot({ diff --git a/frontend/src/app/features/found-username/found-username.component.html b/frontend/src/app/features/found-username/found-username.component.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/app/features/found-username/found-username.component.spec.ts b/frontend/src/app/features/found-username/found-username.component.spec.ts new file mode 100644 index 0000000000..bd1584e7cd --- /dev/null +++ b/frontend/src/app/features/found-username/found-username.component.spec.ts @@ -0,0 +1,23 @@ +import { render } from '@testing-library/angular'; +import { FoundUsernameComponent } from './found-username.component'; + +fdescribe('FoundUsernameComponent', () => { + const setup = async () => { + const setupTools = await render(FoundUsernameComponent, { + imports: [], + declarations: [], + providers: [], + componentProperties: {}, + }); + + const component = setupTools.fixture.componentInstance; + + return { ...setupTools, component }; + }; + + it('should create', async () => { + const { component } = await setup(); + + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/features/found-username/found-username.component.ts b/frontend/src/app/features/found-username/found-username.component.ts new file mode 100644 index 0000000000..66427b79e2 --- /dev/null +++ b/frontend/src/app/features/found-username/found-username.component.ts @@ -0,0 +1,11 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-found-username', + templateUrl: './found-username.component.html', +}) +export class FoundUsernameComponent implements OnInit { + constructor() {} + + ngOnInit(): void {} +} From fed95edafa366652e502063ba22a9900a97d0807 Mon Sep 17 00:00:00 2001 From: Sabrina Date: Mon, 9 Dec 2024 09:44:22 +0000 Subject: [PATCH 17/87] Remove unused regex --- .../src/app/features/login/login.component.spec.ts | 11 ++++------- frontend/src/app/features/login/login.component.ts | 1 - 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/features/login/login.component.spec.ts b/frontend/src/app/features/login/login.component.spec.ts index 4fe0de1d70..4bf0ff064c 100644 --- a/frontend/src/app/features/login/login.component.spec.ts +++ b/frontend/src/app/features/login/login.component.spec.ts @@ -85,11 +85,7 @@ describe('LoginComponent', () => { describe('password', () => { it('should show the password as password field when show password is false', async () => { - const { component, fixture, getByTestId } = await setup(); - - component.showPassword = false; - - fixture.detectChanges(); + const { getByTestId } = await setup(); const passwordInput = getByTestId('password'); @@ -97,10 +93,11 @@ describe('LoginComponent', () => { }); it('should show the password as text field when show password is true', async () => { - const { component, fixture, getByTestId } = await setup(); + const { fixture, getByTestId, getByText } = await setup(); - component.showPassword = true; + const showToggleText = 'Show password'; + fireEvent.click(getByText(showToggleText)); fixture.detectChanges(); const passwordInput = getByTestId('password'); diff --git a/frontend/src/app/features/login/login.component.ts b/frontend/src/app/features/login/login.component.ts index d820415974..902e06924b 100644 --- a/frontend/src/app/features/login/login.component.ts +++ b/frontend/src/app/features/login/login.component.ts @@ -32,7 +32,6 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewInit { public serverErrorsMap: Array; public serverError: string; public showPassword: boolean = false; - public regex = "^[A-Za-z0-9._'%+-]*$"; constructor( private idleService: IdleService, From 545e7c945f23c73489d68d49ff0e29b1ab5b4828 Mon Sep 17 00:00:00 2001 From: Sabrina Date: Mon, 9 Dec 2024 09:59:26 +0000 Subject: [PATCH 18/87] Update test names --- frontend/src/app/features/login/login.component.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/features/login/login.component.spec.ts b/frontend/src/app/features/login/login.component.spec.ts index 4bf0ff064c..fb46fc7e26 100644 --- a/frontend/src/app/features/login/login.component.spec.ts +++ b/frontend/src/app/features/login/login.component.spec.ts @@ -84,7 +84,7 @@ describe('LoginComponent', () => { }); describe('password', () => { - it('should show the password as password field when show password is false', async () => { + it('should set the password as password field (to hide input) on page load', async () => { const { getByTestId } = await setup(); const passwordInput = getByTestId('password'); @@ -92,7 +92,7 @@ describe('LoginComponent', () => { expect(passwordInput.getAttribute('type')).toEqual('password'); }); - it('should show the password as text field when show password is true', async () => { + it("should show the password as text field after user clicks 'Show password'", async () => { const { fixture, getByTestId, getByText } = await setup(); const showToggleText = 'Show password'; From 9e946b6204795f489fc208e58939c7e74ceaba97 Mon Sep 17 00:00:00 2001 From: Sabrina Date: Tue, 10 Dec 2024 17:10:25 +0000 Subject: [PATCH 19/87] Add panel to display --- .../found-username.component.html | 29 ++++++ .../found-username.component.spec.ts | 93 +++++++++++++++++-- .../found-username.component.ts | 22 ++++- 3 files changed, 134 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/features/found-username/found-username.component.html b/frontend/src/app/features/found-username/found-username.component.html index e69de29bb2..5540793d37 100644 --- a/frontend/src/app/features/found-username/found-username.component.html +++ b/frontend/src/app/features/found-username/found-username.component.html @@ -0,0 +1,29 @@ + +
+

+ We’ve found your username +

+
+ Your username is +
{{ username }}
+
+
+ +
+ +
+
+ + + + diff --git a/frontend/src/app/features/found-username/found-username.component.spec.ts b/frontend/src/app/features/found-username/found-username.component.spec.ts index bd1584e7cd..20bd56224b 100644 --- a/frontend/src/app/features/found-username/found-username.component.spec.ts +++ b/frontend/src/app/features/found-username/found-username.component.spec.ts @@ -1,23 +1,100 @@ -import { render } from '@testing-library/angular'; +import { fireEvent, render, within } from '@testing-library/angular'; import { FoundUsernameComponent } from './found-username.component'; +import { getTestBed } from '@angular/core/testing'; +import { Router, RouterModule } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { PageNotFoundComponent } from '@core/components/error/page-not-found/page-not-found.component'; -fdescribe('FoundUsernameComponent', () => { - const setup = async () => { +describe('FoundUsernameComponent', () => { + const setup = async (overrides: any = {}) => { const setupTools = await render(FoundUsernameComponent, { - imports: [], - declarations: [], + imports: [RouterModule, RouterTestingModule], + declarations: [PageNotFoundComponent], providers: [], - componentProperties: {}, + componentProperties: { + username: overrides.username, + }, }); const component = setupTools.fixture.componentInstance; - return { ...setupTools, component }; + const injector = getTestBed(); + const router = injector.inject(Router) as Router; + + const routerSpy = spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); + + return { ...setupTools, component, routerSpy }; }; it('should create', async () => { - const { component } = await setup(); + const overrides = { + username: 'Bighitterhank1000', + }; + + const { component } = await setup(overrides); expect(component).toBeTruthy(); }); + + it('should show the panel', async () => { + const overrides = { + username: 'Bighitterhank1000', + }; + + const { getByTestId } = await setup(overrides); + + const panel = getByTestId('panel'); + + expect(panel).toBeTruthy(); + expect(within(panel).getByText('We’ve found your username')); + }); + + it('should show the username', async () => { + const overrides = { + username: 'Bighitterhank1000', + }; + + const { getByTestId } = await setup(overrides); + + const panel = getByTestId('panel'); + expect(within(panel).getByText('Your username is')); + expect(within(panel).getByText('Bighitterhank1000')); + }); + + it('should show a button to return to the sign in page', async () => { + const overrides = { + username: 'Bighitterhank1000', + }; + + const { getByText } = await setup(overrides); + + const buttonText = getByText('Back to sign in'); + + expect(buttonText).toBeTruthy(); + }); + + it('should go back to the sign in page when the button is clicked', async () => { + const overrides = { + username: 'Bighitterhank1000', + }; + + const { fixture, getByText, routerSpy } = await setup(overrides); + + const buttonText = getByText('Back to sign in'); + + fireEvent.click(buttonText); + fixture.detectChanges(); + + expect(routerSpy).toHaveBeenCalledWith(['/login']); + }); + + it('should show page not found if user lands on page and no username was found', async () => { + const overrides = { + username: null, + }; + + const { getByTestId } = await setup(overrides); + + expect(getByTestId('not-found')).toBeTruthy(); + }); }); diff --git a/frontend/src/app/features/found-username/found-username.component.ts b/frontend/src/app/features/found-username/found-username.component.ts index 66427b79e2..81215c5df7 100644 --- a/frontend/src/app/features/found-username/found-username.component.ts +++ b/frontend/src/app/features/found-username/found-username.component.ts @@ -1,11 +1,29 @@ import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; @Component({ selector: 'app-found-username', templateUrl: './found-username.component.html', }) export class FoundUsernameComponent implements OnInit { - constructor() {} + public username: string = 'Bighitterhank1000'; + public isUsernameFound: boolean; - ngOnInit(): void {} + constructor(private router: Router) {} + + ngOnInit(): void { + this.getFoundUsername(); + } + + public getFoundUsername(): void { + //get username from FindUsernameService + //this.username = this.findUsernameService.usernameFound + + this.isUsernameFound = this.username !== null ? true : false; + } + + public backToSignInPage(event: Event): void { + event.preventDefault(); + this.router.navigate(['/login']); + } } From e219b66e37090023ffe8d5aa1ede0ad82aeea080 Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Fri, 6 Dec 2024 16:23:10 +0000 Subject: [PATCH 20/87] amend folder structure, add forgot-your-username component --- frontend/src/app/app-routing.module.ts | 4 +- frontend/src/app/app.module.ts | 21 +++++----- .../confirmation/confirmation.component.html | 0 .../confirmation/confirmation.component.ts | 0 .../edit/edit.component.html | 0 .../edit/edit.component.ts | 0 .../forgot-your-password.component.html | 0 .../forgot-your-password.component.spec.ts | 0 .../forgot-your-password.component.ts | 0 ...t-your-username-or-password.component.html | 0 ...our-username-or-password.component.spec.ts | 0 ...got-your-username-or-password.component.ts | 0 .../forgot-your-username.component.html | 1 + .../forgot-your-username.component.scss | 0 .../forgot-your-username.component.spec.ts | 39 +++++++++++++++++++ .../forgot-your-username.component.ts | 10 +++++ 16 files changed, 64 insertions(+), 11 deletions(-) rename frontend/src/app/features/{ => forgot-your-username-or-password}/forgot-your-password/confirmation/confirmation.component.html (100%) rename frontend/src/app/features/{ => forgot-your-username-or-password}/forgot-your-password/confirmation/confirmation.component.ts (100%) rename frontend/src/app/features/{ => forgot-your-username-or-password}/forgot-your-password/edit/edit.component.html (100%) rename frontend/src/app/features/{ => forgot-your-username-or-password}/forgot-your-password/edit/edit.component.ts (100%) rename frontend/src/app/features/{ => forgot-your-username-or-password}/forgot-your-password/forgot-your-password.component.html (100%) rename frontend/src/app/features/{ => forgot-your-username-or-password}/forgot-your-password/forgot-your-password.component.spec.ts (100%) rename frontend/src/app/features/{ => forgot-your-username-or-password}/forgot-your-password/forgot-your-password.component.ts (100%) rename frontend/src/app/features/{forgot-your-password => }/forgot-your-username-or-password/forgot-your-username-or-password.component.html (100%) rename frontend/src/app/features/{forgot-your-password => }/forgot-your-username-or-password/forgot-your-username-or-password.component.spec.ts (100%) rename frontend/src/app/features/{forgot-your-password => }/forgot-your-username-or-password/forgot-your-username-or-password.component.ts (100%) create mode 100644 frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.html create mode 100644 frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.scss create mode 100644 frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts create mode 100644 frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.ts diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index fadb159655..9f13cfcd57 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -28,8 +28,8 @@ import { WorkplaceResolver } from '@core/resolvers/workplace.resolver'; import { AdminComponent } from '@features/admin/admin.component'; import { AscWdsCertificateComponent } from '@features/dashboard/asc-wds-certificate/asc-wds-certificate.component'; import { FirstLoginPageComponent } from '@features/first-login-page/first-login-page.component'; -import { ForgotYourPasswordComponent } from '@features/forgot-your-password/forgot-your-password.component'; -import { ForgotYourUsernameOrPasswordComponent } from '@features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component'; +import { ForgotYourPasswordComponent } from '@features/forgot-your-username-or-password/forgot-your-password/forgot-your-password.component'; +import { ForgotYourUsernameOrPasswordComponent } from '@features/forgot-your-username-or-password/forgot-your-username-or-password.component'; import { LoginComponent } from '@features/login/login.component'; import { LogoutComponent } from '@features/logout/logout.component'; import { MigratedUserTermsConditionsComponent } from '@features/migrated-user-terms-conditions/migrated-user-terms-conditions.component'; diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 7a0b50b26b..315bdf9124 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -1,3 +1,6 @@ +import { Angulartics2Module } from 'angulartics2'; +import { HighchartsChartModule } from 'highcharts-angular'; + import { CommonModule } from '@angular/common'; import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from '@angular/common/http'; import { ErrorHandler, NgModule } from '@angular/core'; @@ -57,13 +60,17 @@ import { AscWdsCertificateComponent } from '@features/dashboard/asc-wds-certific import { DashboardHeaderComponent } from '@features/dashboard/dashboard-header/dashboard-header.component'; import { DashboardComponent } from '@features/dashboard/dashboard.component'; import { HomeTabComponent } from '@features/dashboard/home-tab/home-tab.component'; +import { StaffMismatchBannerComponent } from '@features/dashboard/home-tab/staff-mismatch-banner/staff-mismatch-banner.component'; import { FirstLoginPageComponent } from '@features/first-login-page/first-login-page.component'; import { FirstLoginWizardComponent } from '@features/first-login-wizard/first-login-wizard.component'; -import { ForgotYourPasswordConfirmationComponent } from '@features/forgot-your-password/confirmation/confirmation.component'; -import { ForgotYourPasswordEditComponent } from '@features/forgot-your-password/edit/edit.component'; -import { ForgotYourPasswordComponent } from '@features/forgot-your-password/forgot-your-password.component'; +import { ForgotYourPasswordConfirmationComponent } from '@features/forgot-your-username-or-password/forgot-your-password/confirmation/confirmation.component'; +import { ForgotYourPasswordEditComponent } from '@features/forgot-your-username-or-password/forgot-your-password/edit/edit.component'; +import { ForgotYourPasswordComponent } from '@features/forgot-your-username-or-password/forgot-your-password/forgot-your-password.component'; +import { ForgotYourUsernameOrPasswordComponent } from '@features/forgot-your-username-or-password/forgot-your-username-or-password.component'; +import { ForgotYourUsernameComponent } from '@features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component'; import { LoginComponent } from '@features/login/login.component'; import { LogoutComponent } from '@features/logout/logout.component'; +import { MigratedUserTermsConditionsComponent } from '@features/migrated-user-terms-conditions/migrated-user-terms-conditions.component'; import { BecomeAParentComponent } from '@features/new-dashboard/become-a-parent/become-a-parent.component'; import { DashboardWrapperComponent } from '@features/new-dashboard/dashboard-wrapper.component'; import { NewDashboardComponent } from '@features/new-dashboard/dashboard/dashboard.component'; @@ -79,20 +86,15 @@ import { NewWorkplaceTabComponent } from '@features/new-dashboard/workplace-tab/ import { ResetPasswordConfirmationComponent } from '@features/reset-password/confirmation/confirmation.component'; import { ResetPasswordEditComponent } from '@features/reset-password/edit/edit.component'; import { ResetPasswordComponent } from '@features/reset-password/reset-password.component'; +import { SatisfactionSurveyComponent } from '@features/satisfaction-survey/satisfaction-survey.component'; import { BenchmarksModule } from '@shared/components/benchmarks-tab/benchmarks.module'; import { DataAreaTabModule } from '@shared/components/data-area-tab/data-area-tab.module'; import { FeatureFlagsService } from '@shared/services/feature-flags.service'; import { SharedModule } from '@shared/shared.module'; -import { Angulartics2Module } from 'angulartics2'; -import { HighchartsChartModule } from 'highcharts-angular'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; -import { StaffMismatchBannerComponent } from './features/dashboard/home-tab/staff-mismatch-banner/staff-mismatch-banner.component'; -import { MigratedUserTermsConditionsComponent } from './features/migrated-user-terms-conditions/migrated-user-terms-conditions.component'; -import { SatisfactionSurveyComponent } from './features/satisfaction-survey/satisfaction-survey.component'; import { SentryErrorHandler } from './SentryErrorHandler.component'; -import { ForgotYourUsernameOrPasswordComponent } from './features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component'; @NgModule({ declarations: [ @@ -135,6 +137,7 @@ import { ForgotYourUsernameOrPasswordComponent } from './features/forgot-your-pa ParentWorkplaceAccounts, DeleteWorkplaceComponent, ForgotYourUsernameOrPasswordComponent, + ForgotYourUsernameComponent, ], imports: [ Angulartics2Module.forRoot({ diff --git a/frontend/src/app/features/forgot-your-password/confirmation/confirmation.component.html b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-password/confirmation/confirmation.component.html similarity index 100% rename from frontend/src/app/features/forgot-your-password/confirmation/confirmation.component.html rename to frontend/src/app/features/forgot-your-username-or-password/forgot-your-password/confirmation/confirmation.component.html diff --git a/frontend/src/app/features/forgot-your-password/confirmation/confirmation.component.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-password/confirmation/confirmation.component.ts similarity index 100% rename from frontend/src/app/features/forgot-your-password/confirmation/confirmation.component.ts rename to frontend/src/app/features/forgot-your-username-or-password/forgot-your-password/confirmation/confirmation.component.ts diff --git a/frontend/src/app/features/forgot-your-password/edit/edit.component.html b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-password/edit/edit.component.html similarity index 100% rename from frontend/src/app/features/forgot-your-password/edit/edit.component.html rename to frontend/src/app/features/forgot-your-username-or-password/forgot-your-password/edit/edit.component.html diff --git a/frontend/src/app/features/forgot-your-password/edit/edit.component.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-password/edit/edit.component.ts similarity index 100% rename from frontend/src/app/features/forgot-your-password/edit/edit.component.ts rename to frontend/src/app/features/forgot-your-username-or-password/forgot-your-password/edit/edit.component.ts diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-password.component.html b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-password/forgot-your-password.component.html similarity index 100% rename from frontend/src/app/features/forgot-your-password/forgot-your-password.component.html rename to frontend/src/app/features/forgot-your-username-or-password/forgot-your-password/forgot-your-password.component.html diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-password/forgot-your-password.component.spec.ts similarity index 100% rename from frontend/src/app/features/forgot-your-password/forgot-your-password.component.spec.ts rename to frontend/src/app/features/forgot-your-username-or-password/forgot-your-password/forgot-your-password.component.spec.ts diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-password.component.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-password/forgot-your-password.component.ts similarity index 100% rename from frontend/src/app/features/forgot-your-password/forgot-your-password.component.ts rename to frontend/src/app/features/forgot-your-username-or-password/forgot-your-password/forgot-your-password.component.ts diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username-or-password.component.html similarity index 100% rename from frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.html rename to frontend/src/app/features/forgot-your-username-or-password/forgot-your-username-or-password.component.html diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.spec.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username-or-password.component.spec.ts similarity index 100% rename from frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.spec.ts rename to frontend/src/app/features/forgot-your-username-or-password/forgot-your-username-or-password.component.spec.ts diff --git a/frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username-or-password.component.ts similarity index 100% rename from frontend/src/app/features/forgot-your-password/forgot-your-username-or-password/forgot-your-username-or-password.component.ts rename to frontend/src/app/features/forgot-your-username-or-password/forgot-your-username-or-password.component.ts diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.html b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.html new file mode 100644 index 0000000000..2a4bba413a --- /dev/null +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.html @@ -0,0 +1 @@ +

forgot-your-username works!

diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.scss b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts new file mode 100644 index 0000000000..ae29218d5a --- /dev/null +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts @@ -0,0 +1,39 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { getTestBed } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { SharedModule } from '@shared/shared.module'; +import { render } from '@testing-library/angular'; + +import { ForgotYourUsernameComponent } from './forgot-your-username.component'; + +describe('ForgotYourUsernameComponent', () => { + const setup = async () => { + const setupTools = await render(ForgotYourUsernameComponent, { + imports: [HttpClientTestingModule, FormsModule, ReactiveFormsModule, RouterTestingModule, SharedModule], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: {}, + }, + }, + ], + }); + + const component = setupTools.fixture.componentInstance; + + const injector = getTestBed(); + const router = injector.inject(Router) as Router; + const routerSpy = spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); + + return { ...setupTools, component, routerSpy }; + }; + + it('should create', async () => { + const { component } = await setup(); + + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.ts new file mode 100644 index 0000000000..678cf00edc --- /dev/null +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-forgot-your-username', + templateUrl: './forgot-your-username.component.html', + styleUrls: ['./forgot-your-username.component.scss'] +}) +export class ForgotYourUsernameComponent { + +} From d5372fbe109401c31a91c5bfa5a3340ba09cef29 Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Fri, 6 Dec 2024 16:56:47 +0000 Subject: [PATCH 21/87] add find-account child component --- frontend/src/app/app.module.ts | 2 ++ .../find-account/find-account.component.html | 1 + .../find-account/find-account.component.scss | 0 .../find-account/find-account.component.ts | 10 ++++++++++ .../forgot-your-username.component.html | 3 ++- .../forgot-your-username.component.spec.ts | 8 +++++++- 6 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html create mode 100644 frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.scss create mode 100644 frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 315bdf9124..3a4e00f9a4 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -95,6 +95,7 @@ import { SharedModule } from '@shared/shared.module'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { SentryErrorHandler } from './SentryErrorHandler.component'; +import { FindAccountComponent } from './features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component'; @NgModule({ declarations: [ @@ -138,6 +139,7 @@ import { SentryErrorHandler } from './SentryErrorHandler.component'; DeleteWorkplaceComponent, ForgotYourUsernameOrPasswordComponent, ForgotYourUsernameComponent, + FindAccountComponent, ], imports: [ Angulartics2Module.forRoot({ diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html new file mode 100644 index 0000000000..f2ee9a95e1 --- /dev/null +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html @@ -0,0 +1 @@ +

find-account works!

diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.scss b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts new file mode 100644 index 0000000000..2058e329eb --- /dev/null +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-find-account', + templateUrl: './find-account.component.html', + styleUrls: ['./find-account.component.scss'] +}) +export class FindAccountComponent { + +} diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.html b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.html index 2a4bba413a..765df93338 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.html +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.html @@ -1 +1,2 @@ -

forgot-your-username works!

+

Forgot username

+ \ No newline at end of file diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts index ae29218d5a..aa06c01bf1 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts @@ -8,7 +8,7 @@ import { render } from '@testing-library/angular'; import { ForgotYourUsernameComponent } from './forgot-your-username.component'; -describe('ForgotYourUsernameComponent', () => { +fdescribe('ForgotYourUsernameComponent', () => { const setup = async () => { const setupTools = await render(ForgotYourUsernameComponent, { imports: [HttpClientTestingModule, FormsModule, ReactiveFormsModule, RouterTestingModule, SharedModule], @@ -36,4 +36,10 @@ describe('ForgotYourUsernameComponent', () => { expect(component).toBeTruthy(); }); + + it('should show a page heading', async () => { + const { getByRole } = await setup(); + + expect(getByRole('heading', { name: 'Forgot username' })).toBeTruthy(); + }); }); From aea6c7b007cf38da89360dad3ecf2b3b7b70e732 Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Mon, 9 Dec 2024 16:32:48 +0000 Subject: [PATCH 22/87] implement submit and validation logic for findAccount form --- frontend/src/app/app-routing.module.ts | 6 ++ frontend/src/app/app.module.ts | 4 +- .../services/find-username.service.spec.ts | 16 ++++ .../core/services/find-username.service.ts | 58 +++++++++++++ .../test-utils/MockFindUsernameService.ts | 20 +++++ .../find-account/find-account.component.html | 36 +++++++- .../find-account/find-account.component.ts | 79 +++++++++++++++++- .../find-username.component.html | 1 + .../find-username.component.scss | 0 .../find-username.component.spec.ts | 23 +++++ .../find-username/find-username.component.ts | 15 ++++ .../forgot-your-username.component.html | 10 ++- .../forgot-your-username.component.spec.ts | 83 ++++++++++++++++++- .../forgot-your-username.component.ts | 31 ++++++- 14 files changed, 372 insertions(+), 10 deletions(-) create mode 100644 frontend/src/app/core/services/find-username.service.spec.ts create mode 100644 frontend/src/app/core/services/find-username.service.ts create mode 100644 frontend/src/app/core/test-utils/MockFindUsernameService.ts create mode 100644 frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.html create mode 100644 frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.scss create mode 100644 frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.spec.ts create mode 100644 frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.ts diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 9f13cfcd57..315a0d94dd 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -30,6 +30,7 @@ import { AscWdsCertificateComponent } from '@features/dashboard/asc-wds-certific import { FirstLoginPageComponent } from '@features/first-login-page/first-login-page.component'; import { ForgotYourPasswordComponent } from '@features/forgot-your-username-or-password/forgot-your-password/forgot-your-password.component'; import { ForgotYourUsernameOrPasswordComponent } from '@features/forgot-your-username-or-password/forgot-your-username-or-password.component'; +import { ForgotYourUsernameComponent } from '@features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component'; import { LoginComponent } from '@features/login/login.component'; import { LogoutComponent } from '@features/logout/logout.component'; import { MigratedUserTermsConditionsComponent } from '@features/migrated-user-terms-conditions/migrated-user-terms-conditions.component'; @@ -95,6 +96,11 @@ const routes: Routes = [ component: ForgotYourUsernameOrPasswordComponent, data: { title: 'Forgot Your Username Or Password' }, }, + { + path: 'forgot-your-username', + component: ForgotYourUsernameComponent, + data: { title: 'Forgot Your Username' }, + }, { path: 'reset-password', component: ResetPasswordComponent, diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 3a4e00f9a4..3e5cf82cda 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -67,6 +67,8 @@ import { ForgotYourPasswordConfirmationComponent } from '@features/forgot-your-u import { ForgotYourPasswordEditComponent } from '@features/forgot-your-username-or-password/forgot-your-password/edit/edit.component'; import { ForgotYourPasswordComponent } from '@features/forgot-your-username-or-password/forgot-your-password/forgot-your-password.component'; import { ForgotYourUsernameOrPasswordComponent } from '@features/forgot-your-username-or-password/forgot-your-username-or-password.component'; +import { FindAccountComponent } from '@features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component'; +import { FindUsernameComponent } from '@features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component'; import { ForgotYourUsernameComponent } from '@features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component'; import { LoginComponent } from '@features/login/login.component'; import { LogoutComponent } from '@features/logout/logout.component'; @@ -95,7 +97,6 @@ import { SharedModule } from '@shared/shared.module'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { SentryErrorHandler } from './SentryErrorHandler.component'; -import { FindAccountComponent } from './features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component'; @NgModule({ declarations: [ @@ -140,6 +141,7 @@ import { FindAccountComponent } from './features/forgot-your-username-or-passwor ForgotYourUsernameOrPasswordComponent, ForgotYourUsernameComponent, FindAccountComponent, + FindUsernameComponent, ], imports: [ Angulartics2Module.forRoot({ diff --git a/frontend/src/app/core/services/find-username.service.spec.ts b/frontend/src/app/core/services/find-username.service.spec.ts new file mode 100644 index 0000000000..7685d34bc6 --- /dev/null +++ b/frontend/src/app/core/services/find-username.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { FindUsernameService } from './find-username.service'; + +describe('FindUsernameService', () => { + let service: FindUsernameService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(FindUsernameService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/core/services/find-username.service.ts b/frontend/src/app/core/services/find-username.service.ts new file mode 100644 index 0000000000..d2c94d49a1 --- /dev/null +++ b/frontend/src/app/core/services/find-username.service.ts @@ -0,0 +1,58 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { environment } from 'src/environments/environment'; + +export interface FindAccountRequest { + name: string; + workplaceIdOrPostcode: string; + email: string; +} + +interface AccountFound { + accountFound: true; + accountUid: string; +} +interface AccountNotFound { + accountFound: false; + remainingAttempts: number; +} + +export type FindAccountResponse = AccountFound | AccountNotFound; + +@Injectable({ + providedIn: 'root', +}) +export class FindUsernameService { + private _accountUid$: BehaviorSubject = new BehaviorSubject(null); + public findAccountRemainingAttempts: number = null; + public findUsernameRemainingAttempts: number = null; + + constructor(private http: HttpClient) {} + + postFindUserAccount(params: FindAccountRequest): Observable { + return this.http.post( + `${environment.appRunnerEndpoint}/api/registration/findUserAccount`, + params, + ) as Observable; + } + + findUserAccount(params: FindAccountRequest): void { + this.postFindUserAccount(params).subscribe((res) => { + if (res.accountFound === true) { + this._accountUid$.next(res.accountUid); + } else { + this.findAccountRemainingAttempts = res.remainingAttempts; + } + }); + } + + public get accountUid$() { + return this._accountUid$.asObservable(); + } + + public get accountUid() { + return this._accountUid$.value; + } +} diff --git a/frontend/src/app/core/test-utils/MockFindUsernameService.ts b/frontend/src/app/core/test-utils/MockFindUsernameService.ts new file mode 100644 index 0000000000..deb41e6de3 --- /dev/null +++ b/frontend/src/app/core/test-utils/MockFindUsernameService.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import { FindAccountRequest, FindAccountResponse, FindUsernameService } from '@core/services/find-username.service'; +import { of } from 'rxjs'; + +@Injectable() +export class MockFindUsernameService extends FindUsernameService { + postFindUserAccount(params: FindAccountRequest): ReturnType { + if (params.name === 'non-exist-username') { + return of({ + accountFound: false, + remainingAttempts: 4, + }); + } + + return of({ + accountFound: true, + accountUid: 'mock-user-uid', + }); + } +} diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html index f2ee9a95e1..922fa52845 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html @@ -1 +1,35 @@ -

find-account works!

+

+ Enter your name, workplace ID or postcode, and your ASC-WDS email address (this'll be the one you used when you + created your account, unless you've changed it). +

+
+ +
+ + + Error: {{ getFirstErrorMessage(field.id) }} + + +
+
+ +

Account found

+ +
+ + or + Back to sign in + +
+
diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts index 2058e329eb..163cba2930 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts @@ -1,10 +1,85 @@ -import { Component } from '@angular/core'; +import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { ErrorDetails } from '@core/model/errorSummary.model'; +import { ErrorSummaryService } from '@core/services/error-summary.service'; +import { lowerFirst } from 'lodash'; +import { FindUsernameService } from '../../../../core/services/find-username.service'; +import { Subscription } from 'rxjs'; + +const Fields = [ + { id: 'name', label: 'Name', maxlength: 120 }, + { id: 'workplaceIdOrPostcode', label: 'Workplace ID or postcode', maxlength: 8 }, + { id: 'email', label: 'Email address', maxlength: 120 }, +]; @Component({ selector: 'app-find-account', templateUrl: './find-account.component.html', - styleUrls: ['./find-account.component.scss'] + styleUrls: ['./find-account.component.scss'], }) export class FindAccountComponent { + @ViewChild('formEl') formEl: ElementRef; + public form: UntypedFormGroup; + public submitted = false; + public formErrorsMap: Array; + public formFields = Fields; + + @Input() public serverError: string; + @Input() public accountFound: false; + @Output() setCurrentForm = new EventEmitter(); + + constructor( + private FormBuilder: UntypedFormBuilder, + private errorSummaryService: ErrorSummaryService, + private findUsernameService: FindUsernameService, + ) {} + + ngOnInit() { + const formElements = {}; + Fields.forEach((field) => { + formElements[field.id] = [ + '', + { validators: [Validators.required, Validators.maxLength(field.maxlength)], updateOn: 'submit' }, + ]; + }); + + this.form = this.FormBuilder.group(formElements); + this.setupFormErrorsMap(); + this.setCurrentForm.emit(this); + } + + ngAfterViewInit() { + this.errorSummaryService.formEl$.next(this.formEl); + } + + public setupFormErrorsMap(): void { + this.formErrorsMap = Fields.map((field) => { + return { + item: field.id, + type: [ + { + name: 'required', + message: `Enter your ${lowerFirst(field.label)}`, + }, + { + name: 'maxlength', + message: `Your ${lowerFirst(field.label)} must be ${field.maxlength} characters or fewer`, + }, + ], + }; + }); + } + + public getFirstErrorMessage(item: string): string { + const errorType = Object.keys(this.form.get(item).errors)[0]; + return this.errorSummaryService.getFormErrorMessage(item, errorType, this.formErrorsMap); + } + + onSubmit() { + this.submitted = true; + if (this.form.valid) { + this.findUsernameService.findUserAccount(this.form.value); + } + } } diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.html b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.html new file mode 100644 index 0000000000..e102330d81 --- /dev/null +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.html @@ -0,0 +1 @@ +

find-username works!

diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.scss b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.spec.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.spec.ts new file mode 100644 index 0000000000..4a56f743f4 --- /dev/null +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FindUsernameComponent } from './find-username.component'; + +describe('FindUsernameComponent', () => { + let component: FindUsernameComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FindUsernameComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(FindUsernameComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.ts new file mode 100644 index 0000000000..f2496189e2 --- /dev/null +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.ts @@ -0,0 +1,15 @@ +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { UntypedFormGroup } from '@angular/forms'; +import { ErrorDetails } from '@core/model/errorSummary.model'; + +@Component({ + selector: 'app-find-username', + templateUrl: './find-username.component.html', + styleUrls: ['./find-username.component.scss'], +}) +export class FindUsernameComponent { + @ViewChild('formEl') formEl: ElementRef; + public form: UntypedFormGroup; + public submitted = false; + public formErrorsMap: Array; +} diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.html b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.html index 765df93338..bf6c671c42 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.html +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.html @@ -1,2 +1,10 @@ + + +

Forgot username

- \ No newline at end of file + \ No newline at end of file diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts index aa06c01bf1..aefd204deb 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts @@ -4,14 +4,19 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { SharedModule } from '@shared/shared.module'; -import { render } from '@testing-library/angular'; +import { render, screen } from '@testing-library/angular'; +import userEvent from '@testing-library/user-event'; +import { FindUsernameService } from '../../../core/services/find-username.service'; +import { FindAccountComponent } from './find-account/find-account.component'; import { ForgotYourUsernameComponent } from './forgot-your-username.component'; +import { MockFindUsernameService } from '@core/test-utils/MockFindUsernameService'; fdescribe('ForgotYourUsernameComponent', () => { const setup = async () => { const setupTools = await render(ForgotYourUsernameComponent, { imports: [HttpClientTestingModule, FormsModule, ReactiveFormsModule, RouterTestingModule, SharedModule], + declarations: [FindAccountComponent], providers: [ { provide: ActivatedRoute, @@ -19,6 +24,10 @@ fdescribe('ForgotYourUsernameComponent', () => { snapshot: {}, }, }, + { + provide: FindUsernameService, + useClass: MockFindUsernameService, + }, ], }); @@ -28,7 +37,17 @@ fdescribe('ForgotYourUsernameComponent', () => { const router = injector.inject(Router) as Router; const routerSpy = spyOn(router, 'navigate').and.returnValue(Promise.resolve(true)); - return { ...setupTools, component, routerSpy }; + const findUsernameService = injector.inject(FindUsernameService) as FindUsernameService; + + return { ...setupTools, component, routerSpy, findUsernameService }; + }; + + const fillInAndSubmitForm = async (name: string, workplaceIdOrPostcode: string, email: string) => { + userEvent.type(screen.getByRole('textbox', { name: 'Name' }), name); + userEvent.type(screen.getByRole('textbox', { name: 'Workplace ID or postcode' }), workplaceIdOrPostcode); + userEvent.type(screen.getByRole('textbox', { name: 'Email address' }), email); + + userEvent.click(screen.getByRole('button', { name: 'Find account' })); }; it('should create', async () => { @@ -42,4 +61,64 @@ fdescribe('ForgotYourUsernameComponent', () => { expect(getByRole('heading', { name: 'Forgot username' })).toBeTruthy(); }); + + describe('find account', () => { + it('should show text inputs for "Name", "Workplace ID or postcode" and "Email address"', async () => { + const { getByRole } = await setup(); + + expect(getByRole('textbox', { name: 'Name' })).toBeTruthy(); + expect(getByRole('textbox', { name: 'Workplace ID or postcode' })).toBeTruthy(); + expect(getByRole('textbox', { name: 'Email address' })).toBeTruthy(); + }); + + it('should show a "Find account" CTA button and a "Back to sign in" link', async () => { + const { getByRole, getByText } = await setup(); + + expect(getByRole('button', { name: 'Find account' })).toBeTruthy(); + + const backToSignIn = getByText('Back to sign in'); + expect(backToSignIn).toBeTruthy(); + expect(backToSignIn.getAttribute('href')).toEqual('/login'); + }); + + describe('submit and validation', () => { + it('should show an error message if any of the text input is blank', async () => { + const { fixture, getByRole, getByText, getAllByText } = await setup(); + + userEvent.click(getByRole('button', { name: 'Find account' })); + fixture.detectChanges(); + + expect(getByText('There is a problem')).toBeTruthy(); + + expect(getAllByText('Enter your name')).toHaveSize(2); + expect(getAllByText('Enter your workplace ID or postcode')).toHaveSize(2); + expect(getAllByText('Enter your email address')).toHaveSize(2); + }); + + it('should call forgetUsernameService findUserAccount() on submit', async () => { + const { fixture, findUsernameService } = await setup(); + + spyOn(findUsernameService, 'findUserAccount'); + + await fillInAndSubmitForm('Test User', 'A1234567', 'test@example.com'); + fixture.detectChanges(); + + expect(findUsernameService.findUserAccount).toHaveBeenCalledWith({ + name: 'Test User', + workplaceIdOrPostcode: 'A1234567', + email: 'test@example.com', + }); + }); + + it('should show "Account found" and stop showing "Find account" button when an account is found', async () => { + const { fixture, getByText, queryByRole, findUsernameService } = await setup(); + + await fillInAndSubmitForm('Test User', 'A1234567', 'test@example.com'); + fixture.detectChanges(); + + expect(getByText('Account found')).toBeTruthy(); + expect(queryByRole('button', { name: 'Find account' })).toBeFalsy(); + }); + }); + }); }); diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.ts index 678cf00edc..63024660f7 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.ts +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.ts @@ -1,10 +1,35 @@ -import { Component } from '@angular/core'; +import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { FormGroupDirective, UntypedFormGroup } from '@angular/forms'; +import { ErrorDetails } from '@core/model/errorSummary.model'; +import { BehaviorSubject, Subscription } from 'rxjs'; +import { FindAccountComponent } from './find-account/find-account.component'; +import { FindUsernameComponent } from './find-username/find-username.component'; +import { FindUsernameService } from '../../../core/services/find-username.service'; @Component({ selector: 'app-forgot-your-username', templateUrl: './forgot-your-username.component.html', - styleUrls: ['./forgot-your-username.component.scss'] + styleUrls: ['./forgot-your-username.component.scss'], }) -export class ForgotYourUsernameComponent { +export class ForgotYourUsernameComponent implements OnInit { + public currentForm: FindAccountComponent | FindUsernameComponent; + public formErrorsMap: Array; + public accountUid: string; + private subscriptions = new Subscription(); + constructor(private cd: ChangeDetectorRef, private findUsernameService: FindUsernameService) {} + + ngOnInit(): void { + this.subscriptions.add( + this.findUsernameService.accountUid$.subscribe((accountUid) => { + this.accountUid = accountUid; + }), + ); + } + + public setCurrentForm(childForm: FindAccountComponent | FindUsernameComponent): void { + this.currentForm = childForm; + this.formErrorsMap = childForm.formErrorsMap; + this.cd.detectChanges(); + } } From c5fa77d77d7e5c481ea31c7119ae8acbf300b85c Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Tue, 10 Dec 2024 10:54:42 +0000 Subject: [PATCH 23/87] amend the handling of findUserAccount response --- .../core/services/find-username.service.ts | 37 ++------ .../test-utils/MockFindUsernameService.ts | 7 +- .../find-account/find-account.component.html | 88 ++++++++++++------- .../find-account/find-account.component.ts | 53 ++++++++--- .../forgot-your-username.component.html | 3 +- .../forgot-your-username.component.spec.ts | 36 ++++++-- .../forgot-your-username.component.ts | 14 +-- 7 files changed, 143 insertions(+), 95 deletions(-) diff --git a/frontend/src/app/core/services/find-username.service.ts b/frontend/src/app/core/services/find-username.service.ts index d2c94d49a1..da25f4d5aa 100644 --- a/frontend/src/app/core/services/find-username.service.ts +++ b/frontend/src/app/core/services/find-username.service.ts @@ -1,8 +1,8 @@ +import { Observable } from 'rxjs'; +import { environment } from 'src/environments/environment'; + import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { BehaviorSubject, Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; -import { environment } from 'src/environments/environment'; export interface FindAccountRequest { name: string; @@ -10,49 +10,30 @@ export interface FindAccountRequest { email: string; } -interface AccountFound { +export interface AccountFound { accountFound: true; accountUid: string; + securityQuestion: string; } interface AccountNotFound { accountFound: false; remainingAttempts: number; } -export type FindAccountResponse = AccountFound | AccountNotFound; +export type FindUserAccountResponse = AccountFound | AccountNotFound; @Injectable({ providedIn: 'root', }) export class FindUsernameService { - private _accountUid$: BehaviorSubject = new BehaviorSubject(null); - public findAccountRemainingAttempts: number = null; - public findUsernameRemainingAttempts: number = null; + public usernameFound: string = null; constructor(private http: HttpClient) {} - postFindUserAccount(params: FindAccountRequest): Observable { + findUserAccount(params: FindAccountRequest): Observable { return this.http.post( `${environment.appRunnerEndpoint}/api/registration/findUserAccount`, params, - ) as Observable; - } - - findUserAccount(params: FindAccountRequest): void { - this.postFindUserAccount(params).subscribe((res) => { - if (res.accountFound === true) { - this._accountUid$.next(res.accountUid); - } else { - this.findAccountRemainingAttempts = res.remainingAttempts; - } - }); - } - - public get accountUid$() { - return this._accountUid$.asObservable(); - } - - public get accountUid() { - return this._accountUid$.value; + ) as Observable; } } diff --git a/frontend/src/app/core/test-utils/MockFindUsernameService.ts b/frontend/src/app/core/test-utils/MockFindUsernameService.ts index deb41e6de3..9d318ccbd3 100644 --- a/frontend/src/app/core/test-utils/MockFindUsernameService.ts +++ b/frontend/src/app/core/test-utils/MockFindUsernameService.ts @@ -1,11 +1,11 @@ import { Injectable } from '@angular/core'; -import { FindAccountRequest, FindAccountResponse, FindUsernameService } from '@core/services/find-username.service'; +import { FindAccountRequest, FindUsernameService } from '@core/services/find-username.service'; import { of } from 'rxjs'; @Injectable() export class MockFindUsernameService extends FindUsernameService { - postFindUserAccount(params: FindAccountRequest): ReturnType { - if (params.name === 'non-exist-username') { + findUserAccount(params: FindAccountRequest): ReturnType { + if (params.name === 'non-exist user') { return of({ accountFound: false, remainingAttempts: 4, @@ -15,6 +15,7 @@ export class MockFindUsernameService extends FindUsernameService { return of({ accountFound: true, accountUid: 'mock-user-uid', + securityQuestion: 'What is your favourite colour?', }); } } diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html index 922fa52845..66af2e61a1 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html @@ -1,35 +1,59 @@ -

- Enter your name, workplace ID or postcode, and your ASC-WDS email address (this'll be the one you used when you - created your account, unless you've changed it). -

-
- -
- - - Error: {{ getFirstErrorMessage(field.id) }} - - -
-
+
+
+

+ Enter your name, workplace ID or postcode, and your ASC-WDS email address (this'll be the one you used when you + created your account, unless you've changed it). +

+ + +
+ + + Error: {{ getFirstErrorMessage(field.id) }} + + +
+
-

Account found

+ + +

Account found

+
+ +
+

Account not found

+

+ Some or all of the information you entered does not match that which we have for your account. +

+

+ You've {{ remainingAttempts }} more chances to enter the same information that we have. +

+

+ Make sure the details you entered are correct or call the ASC-WDS Support Team on + 0113 241 0969 for help. +

+
+
+
-
- - or - Back to sign in - +
+ + or + Back to sign in + +
+
- +
diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts index 163cba2930..f7bfff3c7b 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts @@ -1,12 +1,13 @@ -import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; +import { lowerFirst } from 'lodash'; +import { Subscription } from 'rxjs'; + +import { Component, ElementRef, EventEmitter, Output, ViewChild } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { ErrorDetails } from '@core/model/errorSummary.model'; import { ErrorSummaryService } from '@core/services/error-summary.service'; -import { lowerFirst } from 'lodash'; -import { FindUsernameService } from '../../../../core/services/find-username.service'; -import { Subscription } from 'rxjs'; +import { AccountFound, FindUserAccountResponse, FindUsernameService } from '@core/services/find-username.service'; -const Fields = [ +const InputFields = [ { id: 'name', label: 'Name', maxlength: 120 }, { id: 'workplaceIdOrPostcode', label: 'Workplace ID or postcode', maxlength: 8 }, { id: 'email', label: 'Email address', maxlength: 120 }, @@ -20,13 +21,17 @@ const Fields = [ export class FindAccountComponent { @ViewChild('formEl') formEl: ElementRef; public form: UntypedFormGroup; - public submitted = false; public formErrorsMap: Array; - public formFields = Fields; + public formFields = InputFields; + + public submitted = false; + public accountFound: boolean; + public remainingAttempts: number; + + private subscriptions = new Subscription(); - @Input() public serverError: string; - @Input() public accountFound: false; @Output() setCurrentForm = new EventEmitter(); + @Output() accountFoundEvent = new EventEmitter(); constructor( private FormBuilder: UntypedFormBuilder, @@ -36,7 +41,7 @@ export class FindAccountComponent { ngOnInit() { const formElements = {}; - Fields.forEach((field) => { + InputFields.forEach((field) => { formElements[field.id] = [ '', { validators: [Validators.required, Validators.maxLength(field.maxlength)], updateOn: 'submit' }, @@ -53,7 +58,7 @@ export class FindAccountComponent { } public setupFormErrorsMap(): void { - this.formErrorsMap = Fields.map((field) => { + this.formErrorsMap = InputFields.map((field) => { return { item: field.id, type: [ @@ -75,11 +80,31 @@ export class FindAccountComponent { return this.errorSummaryService.getFormErrorMessage(item, errorType, this.formErrorsMap); } - onSubmit() { + public handleFindUserAccountResponse(response: FindUserAccountResponse): void { + switch (response?.accountFound) { + case true: + this.accountFound = true; + // emit info to parent + break; + case false: + this.accountFound = false; + this.remainingAttempts = response.remainingAttempts; + // to navigate to error page when remaining attempt = 0 + break; + } + } + + public onSubmit() { this.submitted = true; - if (this.form.valid) { - this.findUsernameService.findUserAccount(this.form.value); + if (!this.form.valid) { + return; } + + this.subscriptions.add( + this.findUsernameService + .findUserAccount(this.form.value) + .subscribe((response) => this.handleFindUserAccountResponse(response)), + ); } } diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.html b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.html index bf6c671c42..4716707b3d 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.html +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.html @@ -6,5 +6,6 @@ > +

Forgot username

- \ No newline at end of file + diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts index aefd204deb..44010988c4 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts @@ -4,7 +4,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { SharedModule } from '@shared/shared.module'; -import { render, screen } from '@testing-library/angular'; +import { render, screen, within } from '@testing-library/angular'; import userEvent from '@testing-library/user-event'; import { FindUsernameService } from '../../../core/services/find-username.service'; @@ -62,7 +62,7 @@ fdescribe('ForgotYourUsernameComponent', () => { expect(getByRole('heading', { name: 'Forgot username' })).toBeTruthy(); }); - describe('find account', () => { + describe('Find account', () => { it('should show text inputs for "Name", "Workplace ID or postcode" and "Email address"', async () => { const { getByRole } = await setup(); @@ -81,9 +81,11 @@ fdescribe('ForgotYourUsernameComponent', () => { expect(backToSignIn.getAttribute('href')).toEqual('/login'); }); - describe('submit and validation', () => { + describe('submit form and validation', () => { it('should show an error message if any of the text input is blank', async () => { - const { fixture, getByRole, getByText, getAllByText } = await setup(); + const { fixture, getByRole, getByText, getAllByText, findUsernameService } = await setup(); + + spyOn(findUsernameService, 'findUserAccount').and.callThrough(); userEvent.click(getByRole('button', { name: 'Find account' })); fixture.detectChanges(); @@ -93,12 +95,14 @@ fdescribe('ForgotYourUsernameComponent', () => { expect(getAllByText('Enter your name')).toHaveSize(2); expect(getAllByText('Enter your workplace ID or postcode')).toHaveSize(2); expect(getAllByText('Enter your email address')).toHaveSize(2); + + expect(findUsernameService.findUserAccount).not.toHaveBeenCalled(); }); it('should call forgetUsernameService findUserAccount() on submit', async () => { const { fixture, findUsernameService } = await setup(); - spyOn(findUsernameService, 'findUserAccount'); + spyOn(findUsernameService, 'findUserAccount').and.callThrough(); await fillInAndSubmitForm('Test User', 'A1234567', 'test@example.com'); fixture.detectChanges(); @@ -111,7 +115,7 @@ fdescribe('ForgotYourUsernameComponent', () => { }); it('should show "Account found" and stop showing "Find account" button when an account is found', async () => { - const { fixture, getByText, queryByRole, findUsernameService } = await setup(); + const { fixture, getByText, queryByRole } = await setup(); await fillInAndSubmitForm('Test User', 'A1234567', 'test@example.com'); fixture.detectChanges(); @@ -119,6 +123,26 @@ fdescribe('ForgotYourUsernameComponent', () => { expect(getByText('Account found')).toBeTruthy(); expect(queryByRole('button', { name: 'Find account' })).toBeFalsy(); }); + + it('should show a "Account not found" message and still showing "Find account" button when an account is not found', async () => { + const { fixture, getByText, getByRole } = await setup(); + fixture.autoDetectChanges(); + + await fillInAndSubmitForm('non-exist user', 'A1234567', 'test@example.com'); + + const expectedText = [ + 'Some or all of the information you entered does not match that which we have for your account.', + "You've 4 more chances to enter the same information that we have.", + 'Make sure the details you entered are correct or call the ASC-WDS Support Team on 0113 241 0969 for help.', + ]; + + expect(getByText('Account not found')).toBeTruthy(); + expect(getByRole('button', { name: 'Find account' })).toBeTruthy(); + + const accountNotFoundMessage = getByText('Account not found').parentElement; + + expectedText.forEach((text) => expect(accountNotFoundMessage.innerText).toContain(text)); + }); }); }); }); diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.ts index 63024660f7..8c47f01e63 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.ts +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.ts @@ -1,10 +1,8 @@ import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; -import { FormGroupDirective, UntypedFormGroup } from '@angular/forms'; import { ErrorDetails } from '@core/model/errorSummary.model'; -import { BehaviorSubject, Subscription } from 'rxjs'; +import { Subscription } from 'rxjs'; import { FindAccountComponent } from './find-account/find-account.component'; import { FindUsernameComponent } from './find-username/find-username.component'; -import { FindUsernameService } from '../../../core/services/find-username.service'; @Component({ selector: 'app-forgot-your-username', @@ -17,15 +15,9 @@ export class ForgotYourUsernameComponent implements OnInit { public accountUid: string; private subscriptions = new Subscription(); - constructor(private cd: ChangeDetectorRef, private findUsernameService: FindUsernameService) {} + constructor(private cd: ChangeDetectorRef) {} - ngOnInit(): void { - this.subscriptions.add( - this.findUsernameService.accountUid$.subscribe((accountUid) => { - this.accountUid = accountUid; - }), - ); - } + ngOnInit(): void {} public setCurrentForm(childForm: FindAccountComponent | FindUsernameComponent): void { this.currentForm = childForm; From dde9a601674fb632348d438129746711370543fe Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Tue, 10 Dec 2024 15:21:48 +0000 Subject: [PATCH 24/87] implement POST: findUserAccount endpoint --- backend/server/models/user.js | 31 ++++ backend/server/routes/registration.js | 4 +- .../routes/registration/findUserAccount.js | 46 ++++++ .../registration/findUserAccount.spec.js | 132 ++++++++++++++++++ 4 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 backend/server/routes/registration/findUserAccount.js create mode 100644 backend/server/test/unit/routes/registration/findUserAccount.spec.js diff --git a/backend/server/models/user.js b/backend/server/models/user.js index 0cc5ec57ce..21d12d2320 100644 --- a/backend/server/models/user.js +++ b/backend/server/models/user.js @@ -486,5 +486,36 @@ module.exports = function (sequelize, DataTypes) { }); }; + User.findByRelevantInfo = async function ({ name, workplaceId, postcode, email }) { + if (!workplaceId && !postcode) { + return null; + } + const workplaceIdWithSpacePadded = workplaceId.padEnd(8, ' '); + + const establishmentWhereClause = workplaceId + ? { NmdsID: [workplaceId, workplaceIdWithSpacePadded] } + : { postcode: postcode }; + + const query = { + attributes: ['uid', 'SecurityQuestionValue'], + where: { + Archived: false, + FullNameValue: name, + EmailValue: email, + }, + include: [ + { + model: sequelize.models.establishment, + where: establishmentWhereClause, + required: true, + attributes: [], + }, + ], + raw: true, + }; + + return await this.findOne(query); + }; + return User; }; diff --git a/backend/server/routes/registration.js b/backend/server/routes/registration.js index b89229b188..d53b3642ce 100644 --- a/backend/server/routes/registration.js +++ b/backend/server/routes/registration.js @@ -1,7 +1,6 @@ const express = require('express'); const router = express.Router(); const { v4: uuidv4 } = require('uuid'); -uuidv4(); const isLocal = require('../utils/security/isLocalTest').isLocal; const { registerAccount } = require('./registration/registerAccount'); const models = require('../models'); @@ -9,6 +8,7 @@ const models = require('../models'); const generateJWT = require('../utils/security/generateJWT'); const sendMail = require('../utils/email/notify-email').sendPasswordReset; const { authLimiter } = require('../utils/middleware/rateLimiting'); +const { findUserAccount } = require('./registration/findUserAccount'); router.use('/establishmentExistsCheck', require('./registration/establishmentExistsCheck')); @@ -368,4 +368,6 @@ router.post('/validateResetPassword', async (req, res) => { } }); +router.post('/findUserAccount', findUserAccount); + module.exports = router; diff --git a/backend/server/routes/registration/findUserAccount.js b/backend/server/routes/registration/findUserAccount.js new file mode 100644 index 0000000000..a2342cc658 --- /dev/null +++ b/backend/server/routes/registration/findUserAccount.js @@ -0,0 +1,46 @@ +const { isEmpty } = require('lodash'); +const { sanitisePostcode } = require('../../utils/postcodeSanitizer'); +const models = require('../../models/index'); + +const findUserAccount = async (req, res) => { + try { + const { name, workplaceIdOrPostcode, email } = req.body ?? {}; + if ([name, workplaceIdOrPostcode, email].some((field) => isEmpty(field))) { + return res.status(400).send('Invalid request'); + } + + let userFound = null; + + const postcode = sanitisePostcode(workplaceIdOrPostcode); + if (postcode) { + userFound = await models.user.findByRelevantInfo({ name, postcode, email }); + } + + userFound = + userFound ?? (await models.user.findByRelevantInfo({ name, workplaceId: workplaceIdOrPostcode, email })); + + if (userFound) { + return sendSuccessResponse(res, userFound); + } + + return sendNotFoundResponse(res); + } catch (err) { + console.error('registration POST findUserAccount - failed', err); + return res.status(500).send('Internal server error'); + } +}; + +const sendSuccessResponse = (res, userFound) => { + const { uid, SecurityQuestionValue } = userFound; + return res.status(200).json({ + accountFound: true, + accountUid: uid, + securityQuestion: SecurityQuestionValue, + }); +}; + +const sendNotFoundResponse = (res) => { + return res.status(200).json({ accountFound: false, remainingAttempts: 4 }); +}; + +module.exports = { findUserAccount }; diff --git a/backend/server/test/unit/routes/registration/findUserAccount.spec.js b/backend/server/test/unit/routes/registration/findUserAccount.spec.js new file mode 100644 index 0000000000..e45eee1457 --- /dev/null +++ b/backend/server/test/unit/routes/registration/findUserAccount.spec.js @@ -0,0 +1,132 @@ +const chai = require('chai'); +const sinon = require('sinon'); +const expect = chai.expect; +const httpMocks = require('node-mocks-http'); + +const { findUserAccount } = require('../../../../routes/registration/findUserAccount'); +const models = require('../../../../models/index'); + +describe.only('backend/server/routes/registration/findUserAccount', () => { + const mockRequestBody = { name: 'Test User', workplaceIdOrPostcode: 'A1234567', email: 'test@example.com' }; + + const buildRequest = (body) => { + const request = { + method: 'POST', + url: '/api/registration/findUserAccount', + body, + }; + return httpMocks.createRequest(request); + }; + + let stubFindUser; + beforeEach(() => { + stubFindUser = sinon.stub(models.user, 'findByRelevantInfo').callsFake(({ workplaceId, postcode }) => { + if (workplaceId === 'A1234567' || postcode === 'LS1 2RP') { + return { uid: 'mock-uid', SecurityQuestionValue: 'What is your favourite colour?' }; + } + return null; + }); + }); + + afterEach(() => { + sinon.restore(); + }); + + it('should respond with 200 and accountFound: true if user account is found', async () => { + const req = buildRequest(mockRequestBody); + const res = httpMocks.createResponse(); + + await findUserAccount(req, res); + + expect(res.statusCode).to.equal(200); + expect(res._getJSONData()).to.deep.equal({ + accountFound: true, + accountUid: 'mock-uid', + securityQuestion: 'What is your favourite colour?', + }); + + expect(stubFindUser).to.have.been.calledWith({ + name: 'Test User', + workplaceId: 'A1234567', + email: 'test@example.com', + }); + }); + + it('should call find user with postcode if request body contains a postcode', async () => { + const req = buildRequest({ ...mockRequestBody, workplaceIdOrPostcode: 'LS1 2RP' }); + const res = httpMocks.createResponse(); + + await findUserAccount(req, res); + + expect(res.statusCode).to.equal(200); + expect(res._getJSONData()).to.deep.equal({ + accountFound: true, + accountUid: 'mock-uid', + securityQuestion: 'What is your favourite colour?', + }); + + expect(stubFindUser).to.have.been.calledWith({ + name: 'Test User', + postcode: 'LS1 2RP', + email: 'test@example.com', + }); + }); + + it('should respond with 200 and accountFound: false if user account was not found', async () => { + const req = buildRequest({ ...mockRequestBody, workplaceIdOrPostcode: 'non-exist-workplace-id' }); + const res = httpMocks.createResponse(); + + await findUserAccount(req, res); + + expect(res.statusCode).to.equal(200); + expect(res._getJSONData()).to.deep.equal({ + accountFound: false, + remainingAttempts: 4, + }); + }); + + it('should respond with 400 error if request does not have a body', async () => { + const req = httpMocks.createRequest({ + method: 'POST', + url: '/api/registration/findUserAccount', + }); + const res = httpMocks.createResponse(); + + await findUserAccount(req, res); + expect(res.statusCode).to.equal(400); + }); + + it('should respond with 400 error if request body is empty', async () => { + const req = buildRequest({}); + const res = httpMocks.createResponse(); + + await findUserAccount(req, res); + expect(res.statusCode).to.equal(400); + }); + + Object.keys(mockRequestBody).forEach((field) => { + it(`should respond with 400 error if ${field} is missing from request body`, async () => { + const body = { ...mockRequestBody }; + delete body[field]; + + const req = buildRequest(body); + const res = httpMocks.createResponse(); + + await findUserAccount(req, res); + expect(res.statusCode).to.equal(400); + }); + }); + + it('should respond with 500 Internal server error if an error occur when finding user', async () => { + const req = buildRequest(mockRequestBody); + const res = httpMocks.createResponse(); + + sinon.stub(console, 'error'); // suppress noisy logging + stubFindUser.rejects(new Error('mock database error')); + + await findUserAccount(req, res); + + expect(res.statusCode).to.equal(500); + expect(res._getData()).to.equal('Internal server error'); + }); +}); From 1e323651a44a15001f160fd320fb6871b6c119e2 Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Tue, 10 Dec 2024 16:48:48 +0000 Subject: [PATCH 25/87] fix .padEnd call at backend --- backend/server/models/user.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/server/models/user.js b/backend/server/models/user.js index 21d12d2320..58dc0cb1da 100644 --- a/backend/server/models/user.js +++ b/backend/server/models/user.js @@ -1,5 +1,6 @@ /* jshint indent: 2 */ const { Op } = require('sequelize'); +const { padEnd } = require('lodash'); const { sanitise } = require('../utils/db'); module.exports = function (sequelize, DataTypes) { @@ -490,7 +491,7 @@ module.exports = function (sequelize, DataTypes) { if (!workplaceId && !postcode) { return null; } - const workplaceIdWithSpacePadded = workplaceId.padEnd(8, ' '); + const workplaceIdWithSpacePadded = padEnd(workplaceId, 8, ' '); const establishmentWhereClause = workplaceId ? { NmdsID: [workplaceId, workplaceIdWithSpacePadded] } From d61921ccd7b5e4c0c15390da36bb1fa5aab2a000 Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Tue, 10 Dec 2024 16:57:08 +0000 Subject: [PATCH 26/87] emit account uid & question to parent component, add scrolling --- .../core/services/find-username.service.ts | 4 +- .../find-account/find-account.component.html | 40 ++++++++++--------- .../find-account/find-account.component.ts | 38 +++++++++++------- .../forgot-your-username.component.html | 2 +- .../forgot-your-username.component.ts | 7 ++++ 5 files changed, 55 insertions(+), 36 deletions(-) diff --git a/frontend/src/app/core/services/find-username.service.ts b/frontend/src/app/core/services/find-username.service.ts index da25f4d5aa..be0c1512f2 100644 --- a/frontend/src/app/core/services/find-username.service.ts +++ b/frontend/src/app/core/services/find-username.service.ts @@ -31,9 +31,9 @@ export class FindUsernameService { constructor(private http: HttpClient) {} findUserAccount(params: FindAccountRequest): Observable { - return this.http.post( + return this.http.post( `${environment.appRunnerEndpoint}/api/registration/findUserAccount`, params, - ) as Observable; + ); } } diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html index 66af2e61a1..b2ee8e5225 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html @@ -22,26 +22,28 @@
- - -

Account found

+
+ + +

Account found

+
+ + +

Account not found

+

+ Some or all of the information you entered does not match that which we have for your account. +

+

+ You've {{ remainingAttempts }} more chances to enter the same information that we have. +

+

+ Make sure the details you entered are correct or call the ASC-WDS Support Team on + 0113 241 0969 for help. +

+
+
- -
-

Account not found

-

- Some or all of the information you entered does not match that which we have for your account. -

-

- You've {{ remainingAttempts }} more chances to enter the same information that we have. -

-

- Make sure the details you entered are correct or call the ASC-WDS Support Team on - 0113 241 0969 for help. -

-
-
- +
diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts index f7bfff3c7b..640c7f11cd 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts @@ -20,6 +20,8 @@ const InputFields = [ }) export class FindAccountComponent { @ViewChild('formEl') formEl: ElementRef; + @ViewChild('searchResult') searchResult: ElementRef; + public form: UntypedFormGroup; public formErrorsMap: Array; public formFields = InputFields; @@ -80,11 +82,25 @@ export class FindAccountComponent { return this.errorSummaryService.getFormErrorMessage(item, errorType, this.formErrorsMap); } - public handleFindUserAccountResponse(response: FindUserAccountResponse): void { + public onSubmit() { + this.submitted = true; + + if (!this.form.valid) { + this.accountFound = null; + this.remainingAttempts = null; + return; + } + + this.subscriptions.add( + this.findUsernameService.findUserAccount(this.form.value).subscribe((response) => this.handleResponse(response)), + ); + } + + public handleResponse(response: FindUserAccountResponse): void { switch (response?.accountFound) { case true: this.accountFound = true; - // emit info to parent + this.accountFoundEvent.emit(response); break; case false: this.accountFound = false; @@ -92,19 +108,13 @@ export class FindAccountComponent { // to navigate to error page when remaining attempt = 0 break; } - } - - public onSubmit() { - this.submitted = true; - if (!this.form.valid) { - return; - } + setTimeout(() => { + this.scrollToResult(); + }, 0); + } - this.subscriptions.add( - this.findUsernameService - .findUserAccount(this.form.value) - .subscribe((response) => this.handleFindUserAccountResponse(response)), - ); + private scrollToResult() { + this.searchResult.nativeElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); } } diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.html b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.html index 4716707b3d..d49317e9a9 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.html +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.html @@ -8,4 +8,4 @@

Forgot username

- + diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.ts index 8c47f01e63..7cb09936b1 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.ts +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.ts @@ -3,6 +3,7 @@ import { ErrorDetails } from '@core/model/errorSummary.model'; import { Subscription } from 'rxjs'; import { FindAccountComponent } from './find-account/find-account.component'; import { FindUsernameComponent } from './find-username/find-username.component'; +import { AccountFound } from '../../../core/services/find-username.service'; @Component({ selector: 'app-forgot-your-username', @@ -13,6 +14,7 @@ export class ForgotYourUsernameComponent implements OnInit { public currentForm: FindAccountComponent | FindUsernameComponent; public formErrorsMap: Array; public accountUid: string; + public securityQuestion: string; private subscriptions = new Subscription(); constructor(private cd: ChangeDetectorRef) {} @@ -24,4 +26,9 @@ export class ForgotYourUsernameComponent implements OnInit { this.formErrorsMap = childForm.formErrorsMap; this.cd.detectChanges(); } + + public onAccountFound({ accountUid, securityQuestion }: AccountFound): void { + this.accountUid = accountUid; + this.securityQuestion = securityQuestion; + } } From 870438cf110bd0bd154bd434205cdc7df4bac98f Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Tue, 10 Dec 2024 17:11:00 +0000 Subject: [PATCH 27/87] add aria-live to result div for accessibility --- .../find-account/find-account.component.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html index b2ee8e5225..0f24d4d8ff 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html @@ -22,11 +22,12 @@
-
+

Account found

+

Account not found

From 5a07605b92e8eafb2114acc0daed8e483e985904 Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Wed, 11 Dec 2024 10:13:26 +0000 Subject: [PATCH 28/87] add email format validation to form, remove unused scss files --- .../find-account/find-account.component.html | 6 +- .../find-account/find-account.component.scss | 0 .../find-account/find-account.component.ts | 38 ++++---- .../find-username.component.scss | 0 .../find-username.component.spec.ts | 23 ----- .../find-username/find-username.component.ts | 1 - .../forgot-your-username.component.scss | 0 .../forgot-your-username.component.spec.ts | 93 +++++++++++-------- .../forgot-your-username.component.ts | 7 +- 9 files changed, 84 insertions(+), 84 deletions(-) delete mode 100644 frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.scss delete mode 100644 frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.scss delete mode 100644 frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.spec.ts delete mode 100644 frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.scss diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html index 0f24d4d8ff..c51ef87bb2 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html @@ -22,14 +22,14 @@
-
+

Account found

- +

Account not found

Some or all of the information you entered does not match that which we have for your account. @@ -41,7 +41,7 @@

Account not found

Make sure the details you entered are correct or call the ASC-WDS Support Team on 0113 241 0969 for help.

- +
diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.scss b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts index 640c7f11cd..56c6933e6f 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts @@ -3,6 +3,7 @@ import { Subscription } from 'rxjs'; import { Component, ElementRef, EventEmitter, Output, ViewChild } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { EMAIL_PATTERN } from '@core/constants/constants'; import { ErrorDetails } from '@core/model/errorSummary.model'; import { ErrorSummaryService } from '@core/services/error-summary.service'; import { AccountFound, FindUserAccountResponse, FindUsernameService } from '@core/services/find-username.service'; @@ -10,13 +11,12 @@ import { AccountFound, FindUserAccountResponse, FindUsernameService } from '@cor const InputFields = [ { id: 'name', label: 'Name', maxlength: 120 }, { id: 'workplaceIdOrPostcode', label: 'Workplace ID or postcode', maxlength: 8 }, - { id: 'email', label: 'Email address', maxlength: 120 }, + { id: 'email', label: 'Email address', maxlength: 120, pattern: EMAIL_PATTERN }, ]; @Component({ selector: 'app-find-account', templateUrl: './find-account.component.html', - styleUrls: ['./find-account.component.scss'], }) export class FindAccountComponent { @ViewChild('formEl') formEl: ElementRef; @@ -42,15 +42,16 @@ export class FindAccountComponent { ) {} ngOnInit() { - const formElements = {}; - InputFields.forEach((field) => { - formElements[field.id] = [ - '', - { validators: [Validators.required, Validators.maxLength(field.maxlength)], updateOn: 'submit' }, - ]; + const formConfigs = {}; + this.formFields.forEach((field) => { + const validators = [Validators.required, Validators.maxLength(field.maxlength)]; + if (field.pattern) { + validators.push(Validators.pattern(field.pattern)); + } + formConfigs[field.id] = ['', { validators, updateOn: 'submit' }]; }); - this.form = this.FormBuilder.group(formElements); + this.form = this.FormBuilder.group(formConfigs); this.setupFormErrorsMap(); this.setCurrentForm.emit(this); } @@ -60,20 +61,25 @@ export class FindAccountComponent { } public setupFormErrorsMap(): void { - this.formErrorsMap = InputFields.map((field) => { - return { + this.formErrorsMap = this.formFields.map((field) => { + const errorMap = { item: field.id, type: [ - { - name: 'required', - message: `Enter your ${lowerFirst(field.label)}`, - }, + { name: 'required', message: `Enter your ${lowerFirst(field.label)}` }, { name: 'maxlength', message: `Your ${lowerFirst(field.label)} must be ${field.maxlength} characters or fewer`, }, ], }; + if (field.id === 'email') { + errorMap.type.push({ + name: 'pattern', + message: 'Enter the email address in the correct format, like name@example.com', + }); + } + + return errorMap; }); } @@ -86,8 +92,6 @@ export class FindAccountComponent { this.submitted = true; if (!this.form.valid) { - this.accountFound = null; - this.remainingAttempts = null; return; } diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.scss b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.spec.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.spec.ts deleted file mode 100644 index 4a56f743f4..0000000000 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { FindUsernameComponent } from './find-username.component'; - -describe('FindUsernameComponent', () => { - let component: FindUsernameComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ FindUsernameComponent ] - }) - .compileComponents(); - - fixture = TestBed.createComponent(FindUsernameComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.ts index f2496189e2..121df080e1 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.ts +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.ts @@ -5,7 +5,6 @@ import { ErrorDetails } from '@core/model/errorSummary.model'; @Component({ selector: 'app-find-username', templateUrl: './find-username.component.html', - styleUrls: ['./find-username.component.scss'], }) export class FindUsernameComponent { @ViewChild('formEl') formEl: ElementRef; diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.scss b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts index 44010988c4..dce354ee32 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts @@ -4,7 +4,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { SharedModule } from '@shared/shared.module'; -import { render, screen, within } from '@testing-library/angular'; +import { render, screen } from '@testing-library/angular'; import userEvent from '@testing-library/user-event'; import { FindUsernameService } from '../../../core/services/find-username.service'; @@ -42,14 +42,6 @@ fdescribe('ForgotYourUsernameComponent', () => { return { ...setupTools, component, routerSpy, findUsernameService }; }; - const fillInAndSubmitForm = async (name: string, workplaceIdOrPostcode: string, email: string) => { - userEvent.type(screen.getByRole('textbox', { name: 'Name' }), name); - userEvent.type(screen.getByRole('textbox', { name: 'Workplace ID or postcode' }), workplaceIdOrPostcode); - userEvent.type(screen.getByRole('textbox', { name: 'Email address' }), email); - - userEvent.click(screen.getByRole('button', { name: 'Find account' })); - }; - it('should create', async () => { const { component } = await setup(); @@ -63,42 +55,35 @@ fdescribe('ForgotYourUsernameComponent', () => { }); describe('Find account', () => { - it('should show text inputs for "Name", "Workplace ID or postcode" and "Email address"', async () => { - const { getByRole } = await setup(); - - expect(getByRole('textbox', { name: 'Name' })).toBeTruthy(); - expect(getByRole('textbox', { name: 'Workplace ID or postcode' })).toBeTruthy(); - expect(getByRole('textbox', { name: 'Email address' })).toBeTruthy(); - }); - - it('should show a "Find account" CTA button and a "Back to sign in" link', async () => { - const { getByRole, getByText } = await setup(); - - expect(getByRole('button', { name: 'Find account' })).toBeTruthy(); - - const backToSignIn = getByText('Back to sign in'); - expect(backToSignIn).toBeTruthy(); - expect(backToSignIn.getAttribute('href')).toEqual('/login'); - }); + const fillInAndSubmitForm = async (name: string, workplaceIdOrPostcode: string, email: string) => { + userEvent.type(screen.getByRole('textbox', { name: 'Name' }), name); + userEvent.type(screen.getByRole('textbox', { name: 'Workplace ID or postcode' }), workplaceIdOrPostcode); + userEvent.type(screen.getByRole('textbox', { name: 'Email address' }), email); - describe('submit form and validation', () => { - it('should show an error message if any of the text input is blank', async () => { - const { fixture, getByRole, getByText, getAllByText, findUsernameService } = await setup(); + userEvent.click(screen.getByRole('button', { name: 'Find account' })); + }; - spyOn(findUsernameService, 'findUserAccount').and.callThrough(); + describe('rendering', () => { + it('should show text inputs for "Name", "Workplace ID or postcode" and "Email address"', async () => { + const { getByRole } = await setup(); - userEvent.click(getByRole('button', { name: 'Find account' })); - fixture.detectChanges(); + expect(getByRole('textbox', { name: 'Name' })).toBeTruthy(); + expect(getByRole('textbox', { name: 'Workplace ID or postcode' })).toBeTruthy(); + expect(getByRole('textbox', { name: 'Email address' })).toBeTruthy(); + }); - expect(getByText('There is a problem')).toBeTruthy(); + it('should show a "Find account" CTA button and a "Back to sign in" link', async () => { + const { getByRole, getByText } = await setup(); - expect(getAllByText('Enter your name')).toHaveSize(2); - expect(getAllByText('Enter your workplace ID or postcode')).toHaveSize(2); - expect(getAllByText('Enter your email address')).toHaveSize(2); + expect(getByRole('button', { name: 'Find account' })).toBeTruthy(); - expect(findUsernameService.findUserAccount).not.toHaveBeenCalled(); + const backToSignIn = getByText('Back to sign in'); + expect(backToSignIn).toBeTruthy(); + expect(backToSignIn.getAttribute('href')).toEqual('/login'); }); + }); + describe('submit form and validation', () => { it('should call forgetUsernameService findUserAccount() on submit', async () => { const { fixture, findUsernameService } = await setup(); @@ -143,6 +128,40 @@ fdescribe('ForgotYourUsernameComponent', () => { expectedText.forEach((text) => expect(accountNotFoundMessage.innerText).toContain(text)); }); + + describe('errors', () => { + it('should show an error message if any of the text input is blank', async () => { + const { fixture, getByRole, getByText, getAllByText, findUsernameService } = await setup(); + + spyOn(findUsernameService, 'findUserAccount').and.callThrough(); + + userEvent.click(getByRole('button', { name: 'Find account' })); + fixture.detectChanges(); + + expect(getByText('There is a problem')).toBeTruthy(); + + expect(getAllByText('Enter your name')).toHaveSize(2); + expect(getAllByText('Enter your workplace ID or postcode')).toHaveSize(2); + expect(getAllByText('Enter your email address')).toHaveSize(2); + + expect(findUsernameService.findUserAccount).not.toHaveBeenCalled(); + }); + + it('should show an error message if the email address not in a right format', async () => { + const { fixture, getByText, getAllByText, findUsernameService } = await setup(); + + spyOn(findUsernameService, 'findUserAccount').and.callThrough(); + + await fillInAndSubmitForm('Test User', 'A1234567', 'not-a-email-address'); + fixture.detectChanges(); + + expect(getByText('There is a problem')).toBeTruthy(); + + expect(getAllByText('Enter the email address in the correct format, like name@example.com')).toHaveSize(2); + + expect(findUsernameService.findUserAccount).not.toHaveBeenCalled(); + }); + }); }); }); }); diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.ts index 7cb09936b1..65a908993f 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.ts +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.ts @@ -1,14 +1,15 @@ +import { Subscription } from 'rxjs'; + import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; import { ErrorDetails } from '@core/model/errorSummary.model'; -import { Subscription } from 'rxjs'; +import { AccountFound } from '@core/services/find-username.service'; + import { FindAccountComponent } from './find-account/find-account.component'; import { FindUsernameComponent } from './find-username/find-username.component'; -import { AccountFound } from '../../../core/services/find-username.service'; @Component({ selector: 'app-forgot-your-username', templateUrl: './forgot-your-username.component.html', - styleUrls: ['./forgot-your-username.component.scss'], }) export class ForgotYourUsernameComponent implements OnInit { public currentForm: FindAccountComponent | FindUsernameComponent; From 0ea5a7963a02f79c9e4966d155013ef45f9c62f9 Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Wed, 11 Dec 2024 11:53:20 +0000 Subject: [PATCH 29/87] cleanup, minor fix --- .../find-account/find-account.component.html | 5 ++++- .../find-account/find-account.component.ts | 7 +++---- .../forgot-your-username.component.html | 5 ++++- .../forgot-your-username/forgot-your-username.component.ts | 2 -- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html index c51ef87bb2..5f6a24b0dd 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html @@ -35,7 +35,10 @@

Account not found

Some or all of the information you entered does not match that which we have for your account.

- You've {{ remainingAttempts }} more chances to enter the same information that we have. + You've {{ remainingAttempts }} more {{ remainingAttempts > 1 ? 'chances' : 'chance' }} to enter the + same information that we have.

Make sure the details you entered are correct or call the ASC-WDS Support Team on diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts index 56c6933e6f..5b5ce3f26f 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts @@ -22,19 +22,18 @@ export class FindAccountComponent { @ViewChild('formEl') formEl: ElementRef; @ViewChild('searchResult') searchResult: ElementRef; + @Output() setCurrentForm = new EventEmitter(); + @Output() accountFoundEvent = new EventEmitter(); + public form: UntypedFormGroup; public formErrorsMap: Array; public formFields = InputFields; - public submitted = false; public accountFound: boolean; public remainingAttempts: number; private subscriptions = new Subscription(); - @Output() setCurrentForm = new EventEmitter(); - @Output() accountFoundEvent = new EventEmitter(); - constructor( private FormBuilder: UntypedFormBuilder, private errorSummaryService: ErrorSummaryService, diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.html b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.html index d49317e9a9..e4e5fc83a8 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.html +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.html @@ -1,6 +1,6 @@ @@ -9,3 +9,6 @@

Forgot username

+ + + \ No newline at end of file diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.ts index 65a908993f..0b9dea2580 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.ts +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.ts @@ -13,7 +13,6 @@ import { FindUsernameComponent } from './find-username/find-username.component'; }) export class ForgotYourUsernameComponent implements OnInit { public currentForm: FindAccountComponent | FindUsernameComponent; - public formErrorsMap: Array; public accountUid: string; public securityQuestion: string; private subscriptions = new Subscription(); @@ -24,7 +23,6 @@ export class ForgotYourUsernameComponent implements OnInit { public setCurrentForm(childForm: FindAccountComponent | FindUsernameComponent): void { this.currentForm = childForm; - this.formErrorsMap = childForm.formErrorsMap; this.cd.detectChanges(); } From 592f4ef78375109919f4b78942d2efdd30b3f3c2 Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Wed, 11 Dec 2024 13:57:04 +0000 Subject: [PATCH 30/87] amend where clause for finding user --- backend/server/models/user.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/server/models/user.js b/backend/server/models/user.js index 58dc0cb1da..d42747935d 100644 --- a/backend/server/models/user.js +++ b/backend/server/models/user.js @@ -503,6 +503,8 @@ module.exports = function (sequelize, DataTypes) { Archived: false, FullNameValue: name, EmailValue: email, + SecurityQuestionValue: { [Op.ne]: null }, + SecurityQuestionAnswerValue: { [Op.ne]: null }, }, include: [ { From 2ae645171ce30dad544a79a2b20739ad7c82917a Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Thu, 12 Dec 2024 09:35:25 +0000 Subject: [PATCH 31/87] minor fix at backend --- backend/server/models/user.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/server/models/user.js b/backend/server/models/user.js index d42747935d..1acf9f9aba 100644 --- a/backend/server/models/user.js +++ b/backend/server/models/user.js @@ -491,8 +491,8 @@ module.exports = function (sequelize, DataTypes) { if (!workplaceId && !postcode) { return null; } - const workplaceIdWithSpacePadded = padEnd(workplaceId, 8, ' '); + const workplaceIdWithSpacePadded = padEnd(workplaceId ?? '', 8, ' '); const establishmentWhereClause = workplaceId ? { NmdsID: [workplaceId, workplaceIdWithSpacePadded] } : { postcode: postcode }; @@ -517,7 +517,7 @@ module.exports = function (sequelize, DataTypes) { raw: true, }; - return await this.findOne(query); + return this.findOne(query); }; return User; From 786cc01f528d6a0eb3e10da93aa6e6f80be6fc3c Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Thu, 12 Dec 2024 09:37:35 +0000 Subject: [PATCH 32/87] display security question in find username component --- .../find-username/find-username.component.html | 5 ++++- .../find-username/find-username.component.ts | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.html b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.html index e102330d81..f76afd84da 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.html +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.html @@ -1 +1,4 @@ -

find-username works!

+
+

Your security question

+

{{ securityQuestion }}

+
diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.ts index 121df080e1..7c72b5f781 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.ts +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.ts @@ -1,4 +1,4 @@ -import { Component, ElementRef, ViewChild } from '@angular/core'; +import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; import { UntypedFormGroup } from '@angular/forms'; import { ErrorDetails } from '@core/model/errorSummary.model'; @@ -6,9 +6,19 @@ import { ErrorDetails } from '@core/model/errorSummary.model'; selector: 'app-find-username', templateUrl: './find-username.component.html', }) -export class FindUsernameComponent { +export class FindUsernameComponent implements OnInit { + @Input() accountUid: string; + @Input() securityQuestion: string; + + @Output() setCurrentForm = new EventEmitter(); + @ViewChild('formEl') formEl: ElementRef; + public form: UntypedFormGroup; public submitted = false; public formErrorsMap: Array; + + ngOnInit(): void { + this.setCurrentForm.emit(this); + } } From 5981584a56f210b00e447658c584f84e634e7c25 Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Thu, 12 Dec 2024 09:57:39 +0000 Subject: [PATCH 33/87] style and content adjustment --- .../find-account/find-account.component.html | 29 ++++++++++++++----- .../forgot-your-username.component.spec.ts | 15 +++++----- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html index 5f6a24b0dd..299ad34e98 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html @@ -13,6 +13,7 @@ Account found -
-

Account not found

-

+

+

+ + Error + Account not found + +

+ +

Some or all of the information you entered does not match that which we have for your account.

- You've {{ remainingAttempts }} more {{ remainingAttempts > 1 ? 'chances' : 'chance' }} to enter the - same information that we have. + + + + You've 1 more chance to enter the same information that we have, otherwise you'll need to call the + Support Team. + + + You've {{ remainingAttempts }} more chances to enter the same information that we have. + + +

+

Make sure the details you entered are correct or call the ASC-WDS Support Team on 0113 241 0969 for help. diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts index dce354ee32..c928f6caf8 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts @@ -4,7 +4,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { SharedModule } from '@shared/shared.module'; -import { render, screen } from '@testing-library/angular'; +import { render, screen, within } from '@testing-library/angular'; import userEvent from '@testing-library/user-event'; import { FindUsernameService } from '../../../core/services/find-username.service'; @@ -109,8 +109,8 @@ fdescribe('ForgotYourUsernameComponent', () => { expect(queryByRole('button', { name: 'Find account' })).toBeFalsy(); }); - it('should show a "Account not found" message and still showing "Find account" button when an account is not found', async () => { - const { fixture, getByText, getByRole } = await setup(); + it('should show a "Account not found" message and keep showing "Find account" button when an account is not found', async () => { + const { fixture, getByRole, getByTestId } = await setup(); fixture.autoDetectChanges(); await fillInAndSubmitForm('non-exist user', 'A1234567', 'test@example.com'); @@ -121,12 +121,11 @@ fdescribe('ForgotYourUsernameComponent', () => { 'Make sure the details you entered are correct or call the ASC-WDS Support Team on 0113 241 0969 for help.', ]; - expect(getByText('Account not found')).toBeTruthy(); - expect(getByRole('button', { name: 'Find account' })).toBeTruthy(); - - const accountNotFoundMessage = getByText('Account not found').parentElement; + const messageDiv = getByTestId('account-not-found'); + expect(within(messageDiv).getByText('Account not found')).toBeTruthy(); + expectedText.forEach((text) => expect(messageDiv.innerText).toContain(text)); - expectedText.forEach((text) => expect(accountNotFoundMessage.innerText).toContain(text)); + expect(getByRole('button', { name: 'Find account' })).toBeTruthy(); }); describe('errors', () => { From 65f2d77d04b3c85f20aa4835bb06103fc274885d Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Thu, 12 Dec 2024 10:13:38 +0000 Subject: [PATCH 34/87] add test for FindUsernameService, remove fdescribe --- .../services/find-username.service.spec.ts | 21 ++++++++++++++++++- .../forgot-your-username.component.spec.ts | 4 ++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/core/services/find-username.service.spec.ts b/frontend/src/app/core/services/find-username.service.spec.ts index 7685d34bc6..ea3b3eae6c 100644 --- a/frontend/src/app/core/services/find-username.service.spec.ts +++ b/frontend/src/app/core/services/find-username.service.spec.ts @@ -1,16 +1,35 @@ +import { environment } from 'src/environments/environment'; + +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { TestBed } from '@angular/core/testing'; import { FindUsernameService } from './find-username.service'; describe('FindUsernameService', () => { let service: FindUsernameService; + let http: HttpTestingController; beforeEach(() => { - TestBed.configureTestingModule({}); + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); service = TestBed.inject(FindUsernameService); + http = TestBed.inject(HttpTestingController); }); it('should be created', () => { expect(service).toBeTruthy(); }); + + describe('findUserAccount', () => { + it('should make a POST request to /registration/findUserAccount endpoint with the given search params', async () => { + const mockParams = { name: 'Test user', workplaceIdOrPostcode: 'A1234567', email: 'test@example.com' }; + + service.findUserAccount(mockParams).subscribe(); + const req = http.expectOne(`${environment.appRunnerEndpoint}/api/registration/findUserAccount`); + + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(mockParams); + }); + }); }); diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts index c928f6caf8..1b28d2cb32 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts @@ -3,6 +3,7 @@ import { getTestBed } from '@angular/core/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; +import { MockFindUsernameService } from '@core/test-utils/MockFindUsernameService'; import { SharedModule } from '@shared/shared.module'; import { render, screen, within } from '@testing-library/angular'; import userEvent from '@testing-library/user-event'; @@ -10,9 +11,8 @@ import userEvent from '@testing-library/user-event'; import { FindUsernameService } from '../../../core/services/find-username.service'; import { FindAccountComponent } from './find-account/find-account.component'; import { ForgotYourUsernameComponent } from './forgot-your-username.component'; -import { MockFindUsernameService } from '@core/test-utils/MockFindUsernameService'; -fdescribe('ForgotYourUsernameComponent', () => { +describe('ForgotYourUsernameComponent', () => { const setup = async () => { const setupTools = await render(ForgotYourUsernameComponent, { imports: [HttpClientTestingModule, FormsModule, ReactiveFormsModule, RouterTestingModule, SharedModule], From b3b92452bf15146f24be84dda7be58918721337c Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Thu, 12 Dec 2024 10:48:54 +0000 Subject: [PATCH 35/87] minor refactor at backend, add a unit test --- .../routes/registration/findUserAccount.js | 13 ++++++++++-- .../registration/findUserAccount.spec.js | 21 ++++++++++++++++++- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/backend/server/routes/registration/findUserAccount.js b/backend/server/routes/registration/findUserAccount.js index a2342cc658..c777abab7d 100644 --- a/backend/server/routes/registration/findUserAccount.js +++ b/backend/server/routes/registration/findUserAccount.js @@ -4,11 +4,11 @@ const models = require('../../models/index'); const findUserAccount = async (req, res) => { try { - const { name, workplaceIdOrPostcode, email } = req.body ?? {}; - if ([name, workplaceIdOrPostcode, email].some((field) => isEmpty(field))) { + if (!validateRequest(req)) { return res.status(400).send('Invalid request'); } + const { name, workplaceIdOrPostcode, email } = req.body; let userFound = null; const postcode = sanitisePostcode(workplaceIdOrPostcode); @@ -30,6 +30,15 @@ const findUserAccount = async (req, res) => { } }; +const validateRequest = (req) => { + if (!req.body) { + return false; + } + const { name, workplaceIdOrPostcode, email } = req.body; + + return [name, workplaceIdOrPostcode, email].every((field) => !isEmpty(field)); +}; + const sendSuccessResponse = (res, userFound) => { const { uid, SecurityQuestionValue } = userFound; return res.status(200).json({ diff --git a/backend/server/test/unit/routes/registration/findUserAccount.spec.js b/backend/server/test/unit/routes/registration/findUserAccount.spec.js index e45eee1457..55fb379043 100644 --- a/backend/server/test/unit/routes/registration/findUserAccount.spec.js +++ b/backend/server/test/unit/routes/registration/findUserAccount.spec.js @@ -52,7 +52,7 @@ describe.only('backend/server/routes/registration/findUserAccount', () => { }); }); - it('should call find user with postcode if request body contains a postcode', async () => { + it('should find user with postcode if request body contains a postcode', async () => { const req = buildRequest({ ...mockRequestBody, workplaceIdOrPostcode: 'LS1 2RP' }); const res = httpMocks.createResponse(); @@ -72,6 +72,25 @@ describe.only('backend/server/routes/registration/findUserAccount', () => { }); }); + it('should try to search with both postcode and workplace ID if incoming param is not distinguishable', async () => { + const req = buildRequest({ ...mockRequestBody, workplaceIdOrPostcode: 'AB101AB' }); + const res = httpMocks.createResponse(); + + await findUserAccount(req, res); + + expect(stubFindUser).to.have.been.calledWith({ + name: 'Test User', + postcode: 'AB10 1AB', + email: 'test@example.com', + }); + + expect(stubFindUser).to.have.been.calledWith({ + name: 'Test User', + workplaceId: 'AB101AB', + email: 'test@example.com', + }); + }); + it('should respond with 200 and accountFound: false if user account was not found', async () => { const req = buildRequest({ ...mockRequestBody, workplaceIdOrPostcode: 'non-exist-workplace-id' }); const res = httpMocks.createResponse(); From c7a1898b8c0a2dd4dee40a95ec9ca6e84bca4955 Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Thu, 12 Dec 2024 11:19:24 +0000 Subject: [PATCH 36/87] minor fix, remove describe.only --- backend/server/models/user.js | 4 ++-- .../test/unit/routes/registration/findUserAccount.spec.js | 2 +- .../find-account/find-account.component.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/server/models/user.js b/backend/server/models/user.js index 1acf9f9aba..faa9f12e94 100644 --- a/backend/server/models/user.js +++ b/backend/server/models/user.js @@ -492,9 +492,9 @@ module.exports = function (sequelize, DataTypes) { return null; } - const workplaceIdWithSpacePadded = padEnd(workplaceId ?? '', 8, ' '); + const workplaceIdWithTrailingSpace = padEnd(workplaceId ?? '', 8, ' '); const establishmentWhereClause = workplaceId - ? { NmdsID: [workplaceId, workplaceIdWithSpacePadded] } + ? { NmdsID: [workplaceId, workplaceIdWithTrailingSpace] } : { postcode: postcode }; const query = { diff --git a/backend/server/test/unit/routes/registration/findUserAccount.spec.js b/backend/server/test/unit/routes/registration/findUserAccount.spec.js index 55fb379043..1947397f5b 100644 --- a/backend/server/test/unit/routes/registration/findUserAccount.spec.js +++ b/backend/server/test/unit/routes/registration/findUserAccount.spec.js @@ -6,7 +6,7 @@ const httpMocks = require('node-mocks-http'); const { findUserAccount } = require('../../../../routes/registration/findUserAccount'); const models = require('../../../../models/index'); -describe.only('backend/server/routes/registration/findUserAccount', () => { +describe('backend/server/routes/registration/findUserAccount', () => { const mockRequestBody = { name: 'Test User', workplaceIdOrPostcode: 'A1234567', email: 'test@example.com' }; const buildRequest = (body) => { diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts index 5b5ce3f26f..85b030bba1 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.ts @@ -108,7 +108,7 @@ export class FindAccountComponent { case false: this.accountFound = false; this.remainingAttempts = response.remainingAttempts; - // to navigate to error page when remaining attempt = 0 + // TODO for #1570: navigate to error page when remaining attempt = 0 break; } From 4fcfbd062906cea6d591afa08eca3470c05db5b1 Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Thu, 12 Dec 2024 12:12:06 +0000 Subject: [PATCH 37/87] address PR comment (rename method validateRequest --> requestIsInvalid, flip boolean) --- backend/server/routes/registration/findUserAccount.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/server/routes/registration/findUserAccount.js b/backend/server/routes/registration/findUserAccount.js index c777abab7d..acc07798cc 100644 --- a/backend/server/routes/registration/findUserAccount.js +++ b/backend/server/routes/registration/findUserAccount.js @@ -4,7 +4,7 @@ const models = require('../../models/index'); const findUserAccount = async (req, res) => { try { - if (!validateRequest(req)) { + if (requestIsInvalid(req)) { return res.status(400).send('Invalid request'); } @@ -30,13 +30,13 @@ const findUserAccount = async (req, res) => { } }; -const validateRequest = (req) => { +const requestIsInvalid = (req) => { if (!req.body) { - return false; + return true; } const { name, workplaceIdOrPostcode, email } = req.body; - return [name, workplaceIdOrPostcode, email].every((field) => !isEmpty(field)); + return [name, workplaceIdOrPostcode, email].some((field) => isEmpty(field)); }; const sendSuccessResponse = (res, userFound) => { From 8a033b8a211bd08f3732bd66a58c341be67c726c Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Thu, 12 Dec 2024 14:11:08 +0000 Subject: [PATCH 38/87] add green tick icon beside "Account found" text --- .../find-account/find-account.component.html | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html index 299ad34e98..222596a7ed 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-account/find-account.component.html @@ -26,7 +26,11 @@

-

Account found

+

+ + Account found + +

From e485d9c91b4c9345ec64f24994fa951255e2f372 Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Thu, 12 Dec 2024 15:24:52 +0000 Subject: [PATCH 39/87] implement Find username form --- .../test-utils/MockFindUsernameService.ts | 6 ++ .../find-username.component.html | 46 +++++++++++++- .../find-username.component.scss | 22 +++++++ .../find-username/find-username.component.ts | 23 ++++++- .../forgot-your-username.component.spec.ts | 63 ++++++++++++++++++- 5 files changed, 153 insertions(+), 7 deletions(-) create mode 100644 frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.scss diff --git a/frontend/src/app/core/test-utils/MockFindUsernameService.ts b/frontend/src/app/core/test-utils/MockFindUsernameService.ts index 9d318ccbd3..7df780c7a1 100644 --- a/frontend/src/app/core/test-utils/MockFindUsernameService.ts +++ b/frontend/src/app/core/test-utils/MockFindUsernameService.ts @@ -2,6 +2,12 @@ import { Injectable } from '@angular/core'; import { FindAccountRequest, FindUsernameService } from '@core/services/find-username.service'; import { of } from 'rxjs'; +export const mockTestUser = { + accountUid: 'mock-user-uid', + securityQuestion: 'What is your favourite colour?', + securityQuestionAnswer: 'Blue', +}; + @Injectable() export class MockFindUsernameService extends FindUsernameService { findUserAccount(params: FindAccountRequest): ReturnType { diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.html b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.html index f76afd84da..a81217ad04 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.html +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.html @@ -1,4 +1,44 @@ -
-

Your security question

-

{{ securityQuestion }}

+
+
+
+
+
+

Your security question

+

You chose this question when you created your account

+

Question

+
+ {{ securityQuestion }} +
+
+ +
+ + + +
+ + +

Call the ASC-WDS Support Team on 0113 241 0969 for help.

+
+ +
+ + or + Back to sign in +
+
+
diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.scss b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.scss new file mode 100644 index 0000000000..25509bf2dd --- /dev/null +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.scss @@ -0,0 +1,22 @@ +@import 'govuk-frontend/govuk/base'; + +.asc-security-question__container { + > * { + margin-top: 0; + margin-bottom: 5px; + } + + margin-bottom: govuk-spacing(6); +} + +.asc-security-question { + height: 2.5rem; + max-width: 20.5em; + padding: 5px 5px 5px 10px; + font-size: 19px; + box-sizing: border-box; + + display: flex; + align-items: center; + background-color: govuk-colour('light-grey'); +} diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.ts index 7c72b5f781..e188a3563c 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.ts +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.ts @@ -1,10 +1,13 @@ import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; -import { UntypedFormGroup } from '@angular/forms'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { ErrorDetails } from '@core/model/errorSummary.model'; +import { ErrorSummaryService } from '@core/services/error-summary.service'; +import { FindUsernameService } from '@core/services/find-username.service'; @Component({ selector: 'app-find-username', templateUrl: './find-username.component.html', + styleUrls: ['./find-username.component.scss'], }) export class FindUsernameComponent implements OnInit { @Input() accountUid: string; @@ -18,7 +21,25 @@ export class FindUsernameComponent implements OnInit { public submitted = false; public formErrorsMap: Array; + constructor( + private FormBuilder: UntypedFormBuilder, + private errorSummaryService: ErrorSummaryService, + private findUsernameService: FindUsernameService, + ) {} + ngOnInit(): void { + this.form = this.FormBuilder.group({ + securityQuestionAnswer: [Validators.required], + }); + this.setupFormErrorsMap(); this.setCurrentForm.emit(this); } + + ngAfterViewInit() { + this.errorSummaryService.formEl$.next(this.formEl); + } + + public setupFormErrorsMap(): void {} + + public onSubmit(): void {} } diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts index 1b28d2cb32..9b59183606 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/forgot-your-username.component.spec.ts @@ -3,20 +3,21 @@ import { getTestBed } from '@angular/core/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; -import { MockFindUsernameService } from '@core/test-utils/MockFindUsernameService'; +import { MockFindUsernameService, mockTestUser } from '@core/test-utils/MockFindUsernameService'; import { SharedModule } from '@shared/shared.module'; import { render, screen, within } from '@testing-library/angular'; import userEvent from '@testing-library/user-event'; import { FindUsernameService } from '../../../core/services/find-username.service'; import { FindAccountComponent } from './find-account/find-account.component'; +import { FindUsernameComponent } from './find-username/find-username.component'; import { ForgotYourUsernameComponent } from './forgot-your-username.component'; -describe('ForgotYourUsernameComponent', () => { +fdescribe('ForgotYourUsernameComponent', () => { const setup = async () => { const setupTools = await render(ForgotYourUsernameComponent, { imports: [HttpClientTestingModule, FormsModule, ReactiveFormsModule, RouterTestingModule, SharedModule], - declarations: [FindAccountComponent], + declarations: [FindAccountComponent, FindUsernameComponent], providers: [ { provide: ActivatedRoute, @@ -163,4 +164,60 @@ describe('ForgotYourUsernameComponent', () => { }); }); }); + + fdescribe('Find username', () => { + const setupAndProceedToFindUsername = async () => { + const setuptools = await setup(); + + const { fixture, getByRole } = setuptools; + + userEvent.type(getByRole('textbox', { name: 'Name' }), 'Test User'); + userEvent.type(getByRole('textbox', { name: 'Workplace ID or postcode' }), 'A1234567'); + userEvent.type(getByRole('textbox', { name: 'Email address' }), 'test@example.com'); + + userEvent.click(getByRole('button', { name: 'Find account' })); + fixture.detectChanges(); + await fixture.whenStable(); + + return setuptools; + }; + + it('should show the security question of the user', async () => { + const { getByText } = await setupAndProceedToFindUsername(); + + expect(getByText('Your security question')).toBeTruthy(); + expect(getByText('You chose this question when you created your account')).toBeTruthy(); + expect(getByText('Question')).toBeTruthy(); + expect(getByText(mockTestUser.securityQuestion)).toBeTruthy(); + }); + + it('should show a text input for answer', async () => { + const { getByText, getByRole } = await setupAndProceedToFindUsername(); + + expect(getByText("What's the answer to your security question?")).toBeTruthy(); + expect(getByText('Answer')).toBeTruthy(); + expect(getByRole('textbox', { name: "What's the answer to your security question?" })).toBeTruthy(); + }); + + it('should show a reveal text of "Cannot remember the answer?"', async () => { + const { getByTestId } = await setupAndProceedToFindUsername(); + + const revealTextElement = getByTestId('reveal-text'); + const hiddenText = 'Call the ASC-WDS Support Team on 0113 241 0969 for help.'; + + expect(revealTextElement).toBeTruthy(); + expect(within(revealTextElement).getByText('Cannot remember the answer?')).toBeTruthy(); + expect(revealTextElement.textContent).toContain(hiddenText); + }); + + it('should render a "Find username" CTA button and a "Back to sign in" link', async () => { + const { getByRole, getByText } = await setupAndProceedToFindUsername(); + + expect(getByRole('button', { name: 'Find username' })).toBeTruthy(); + + const backToSignIn = getByText('Back to sign in'); + expect(backToSignIn).toBeTruthy(); + expect(backToSignIn.getAttribute('href')).toEqual('/login'); + }); + }); }); From e42aa4eff30926df7a5a3627af6a13c83a7f9a30 Mon Sep 17 00:00:00 2001 From: Joe Fong Date: Thu, 12 Dec 2024 16:54:47 +0000 Subject: [PATCH 40/87] adding validation to find username --- .../find-username.component.html | 23 +++++-- .../find-username.component.scss | 1 + .../find-username/find-username.component.ts | 30 ++++++-- .../forgot-your-username.component.spec.ts | 68 ++++++++++++------- 4 files changed, 87 insertions(+), 35 deletions(-) diff --git a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.html b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.html index a81217ad04..4896a18ebe 100644 --- a/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.html +++ b/frontend/src/app/features/forgot-your-username-or-password/forgot-your-username/find-username/find-username.component.html @@ -6,21 +6,34 @@

Your security question

You chose this question when you created your account

Question

-
+
{{ securityQuestion }}
-
-