From b2d4d1bec21c789baf88578dc2a65a5cf013360c Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Fri, 19 Jul 2024 17:23:02 -0700 Subject: [PATCH] [PM-4963] Migrate breach report components (#10045) * WIP - migrate exposed passwords report components * lint fix * migrate components in reports * migrate breach and unsecured websites reports * undo change routing * revert changes to reports * revert changes * migrate breach report component * update form * revert back to text input * revert change to logic * layout fixes * add spec * fix typo * undo changes to exposed passowords report * fix test --------- Co-authored-by: jordan-bite --- .../pages/breach-report.component.html | 25 ++-- .../pages/breach-report.component.spec.ts | 128 ++++++++++++++++++ .../reports/pages/breach-report.component.ts | 37 +++-- 3 files changed, 162 insertions(+), 28 deletions(-) create mode 100644 apps/web/src/app/tools/reports/pages/breach-report.component.spec.ts diff --git a/apps/web/src/app/tools/reports/pages/breach-report.component.html b/apps/web/src/app/tools/reports/pages/breach-report.component.html index 51191ddf2d..f975d9fd2a 100644 --- a/apps/web/src/app/tools/reports/pages/breach-report.component.html +++ b/apps/web/src/app/tools/reports/pages/breach-report.component.html @@ -2,26 +2,17 @@

{{ "breachDesc" | i18n }}

-
-
-
- - - {{ "breachCheckUsernameEmail" | i18n }} -
-
-
-
+

{{ "reportError" | i18n }}...

diff --git a/apps/web/src/app/tools/reports/pages/breach-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/breach-report.component.spec.ts new file mode 100644 index 0000000000..5df2ffd0f2 --- /dev/null +++ b/apps/web/src/app/tools/reports/pages/breach-report.component.spec.ts @@ -0,0 +1,128 @@ +// eslint-disable-next-line no-restricted-imports +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { ReactiveFormsModule } from "@angular/forms"; +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; + +import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; +import { AuditService } from "@bitwarden/common/abstractions/audit.service"; +import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { BreachAccountResponse } from "@bitwarden/common/models/response/breach-account.response"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { UserId } from "@bitwarden/common/types/guid"; + +import { BreachReportComponent } from "./breach-report.component"; + +const breachedAccounts = [ + new BreachAccountResponse({ + addedDate: "2021-01-01", + breachDate: "2021-01-01", + dataClasses: ["test"], + description: "test", + domain: "test.com", + isActive: true, + isVerified: true, + logoPath: "test", + modifiedDate: "2021-01-01", + name: "test", + pwnCount: 1, + title: "test", + }), +]; + +describe("BreachReportComponent", () => { + let component: BreachReportComponent; + let fixture: ComponentFixture; + let auditService: MockProxy; + let accountService: MockProxy; + const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>({ + id: "testId" as UserId, + email: "test@example.com", + emailVerified: true, + name: "Test User", + }); + + beforeEach(async () => { + auditService = mock(); + accountService = mock(); + accountService.activeAccount$ = activeAccountSubject; + + await TestBed.configureTestingModule({ + declarations: [BreachReportComponent, I18nPipe], + imports: [ReactiveFormsModule], + providers: [ + { + provide: AuditService, + useValue: auditService, + }, + { + provide: AccountService, + useValue: accountService, + }, + { + provide: I18nService, + useValue: mock(), + }, + ], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(BreachReportComponent); + component = fixture.componentInstance as BreachReportComponent; + fixture.detectChanges(); + jest.clearAllMocks(); + }); + + it("should initialize component", () => { + expect(component).toBeTruthy(); + }); + + it("should initialize form with account email", async () => { + expect(component.formGroup.get("username").value).toEqual("test@example.com"); + }); + + it("should mark form as touched and show validation error if form is invalid on submit", async () => { + component.formGroup.get("username").setValue(""); + await component.submit(); + + expect(component.formGroup.touched).toBe(true); + expect(component.formGroup.invalid).toBe(true); + }); + + it("should call auditService.breachedAccounts with lowercase username", async () => { + auditService.breachedAccounts.mockResolvedValue(breachedAccounts); + component.formGroup.get("username").setValue("validUsername"); + + await component.submit(); + + expect(auditService.breachedAccounts).toHaveBeenCalledWith("validusername"); + }); + + it("should set breachedAccounts and checkedUsername after successful submit", async () => { + auditService.breachedAccounts.mockResolvedValue(breachedAccounts); + + await component.submit(); + + expect(component.breachedAccounts).toEqual(breachedAccounts); + expect(component.checkedUsername).toEqual("test@example.com"); + }); + + it("should set error to true if auditService.breachedAccounts throws an error", async () => { + auditService.breachedAccounts.mockRejectedValue(new Error("test error")); + component.formGroup.get("username").setValue("validUsername"); + + await component.submit(); + + expect(component.error).toBe(true); + }); + + it("should set loading to false after submit", async () => { + auditService.breachedAccounts.mockResolvedValue([]); + component.formGroup.get("username").setValue("validUsername"); + + await component.submit(); + + expect(component.loading).toBe(false); + }); +}); diff --git a/apps/web/src/app/tools/reports/pages/breach-report.component.ts b/apps/web/src/app/tools/reports/pages/breach-report.component.ts index 5728d36078..177dcac4f2 100644 --- a/apps/web/src/app/tools/reports/pages/breach-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/breach-report.component.ts @@ -1,4 +1,5 @@ import { Component, OnInit } from "@angular/core"; +import { FormBuilder, Validators } from "@angular/forms"; import { firstValueFrom, map } from "rxjs"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; @@ -10,32 +11,46 @@ import { BreachAccountResponse } from "@bitwarden/common/models/response/breach- templateUrl: "breach-report.component.html", }) export class BreachReportComponent implements OnInit { + loading = false; error = false; - username: string; checkedUsername: string; breachedAccounts: BreachAccountResponse[] = []; - formPromise: Promise; + formGroup = this.formBuilder.group({ + username: ["", { validators: [Validators.required], updateOn: "change" }], + }); constructor( private auditService: AuditService, private accountService: AccountService, + private formBuilder: FormBuilder, ) {} async ngOnInit() { - this.username = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.email)), - ); + this.formGroup + .get("username") + .setValue( + await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.email))), + ); } - async submit() { + submit = async () => { + this.formGroup.markAsTouched(); + + if (this.formGroup.invalid) { + return; + } + this.error = false; - this.username = this.username.toLowerCase(); + this.loading = true; + const username = this.formGroup.value.username.toLowerCase(); try { - this.formPromise = this.auditService.breachedAccounts(this.username); - this.breachedAccounts = await this.formPromise; + this.breachedAccounts = await this.auditService.breachedAccounts(username); } catch { this.error = true; + } finally { + this.loading = false; } - this.checkedUsername = this.username; - } + + this.checkedUsername = username; + }; }