Skip to content

Commit

Permalink
[PM-4963] Migrate breach report components (#10045)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
jaasen-livefront and jordan-bite authored Jul 20, 2024
1 parent e22568f commit b2d4d1b
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 28 deletions.
25 changes: 8 additions & 17 deletions apps/web/src/app/tools/reports/pages/breach-report.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,17 @@

<bit-container>
<p>{{ "breachDesc" | i18n }}</p>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
<div class="row">
<div class="form-group col-6">
<label for="username">{{ "username" | i18n }}</label>
<input
id="username"
type="text"
name="Username"
class="form-control"
[(ngModel)]="username"
required
/>
<small class="form-text text-muted">{{ "breachCheckUsernameEmail" | i18n }}</small>
</div>
</div>
<button type="submit" buttonType="primary" bitButton [loading]="form.loading">
<form [bitSubmit]="submit" [formGroup]="formGroup">
<bit-form-field class="tw-w-1/2" disableMargin>
<bit-label>{{ "username" | i18n }}</bit-label>
<input id="username" type="text" formControlName="username" bitInput />
</bit-form-field>
<small class="form-text text-muted tw-mb-4">{{ "breachCheckUsernameEmail" | i18n }}</small>
<button type="submit" buttonType="primary" bitButton [loading]="loading">
{{ "checkBreaches" | i18n }}
</button>
</form>
<div class="mt-4" *ngIf="!form.loading && checkedUsername">
<div class="mt-4" *ngIf="!loading && checkedUsername">
<p *ngIf="error">{{ "reportError" | i18n }}...</p>
<ng-container *ngIf="!error">
<app-callout type="success" title="{{ 'goodNews' | i18n }}" *ngIf="!breachedAccounts.length">
Expand Down
128 changes: 128 additions & 0 deletions apps/web/src/app/tools/reports/pages/breach-report.component.spec.ts
Original file line number Diff line number Diff line change
@@ -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<BreachReportComponent>;
let auditService: MockProxy<AuditService>;
let accountService: MockProxy<AccountService>;
const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>({
id: "testId" as UserId,
email: "[email protected]",
emailVerified: true,
name: "Test User",
});

beforeEach(async () => {
auditService = mock<AuditService>();
accountService = mock<AccountService>();
accountService.activeAccount$ = activeAccountSubject;

await TestBed.configureTestingModule({
declarations: [BreachReportComponent, I18nPipe],
imports: [ReactiveFormsModule],
providers: [
{
provide: AuditService,
useValue: auditService,
},
{
provide: AccountService,
useValue: accountService,
},
{
provide: I18nService,
useValue: mock<I18nService>(),
},
],
}).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("[email protected]");
});

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("[email protected]");
});

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);
});
});
37 changes: 26 additions & 11 deletions apps/web/src/app/tools/reports/pages/breach-report.component.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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<BreachAccountResponse[]>;
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;
};
}

0 comments on commit b2d4d1b

Please sign in to comment.