1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-01-10 19:38:11 +01:00

[PM-2014] fix: move error handling to components

After discussing it with Jake we decided that following convention was best.
This commit is contained in:
Andreas Coroiu 2023-05-16 15:16:24 +02:00
parent 38adb9ee0a
commit 72f10bab65
No known key found for this signature in database
GPG Key ID: E70B5FFC81DFEC1A
4 changed files with 76 additions and 151 deletions

View File

@ -1,20 +1,12 @@
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { Verification } from "@bitwarden/common/types/verification";
import { CredentialCreateOptionsView } from "../../views/credential-create-options.view"; import { CredentialCreateOptionsView } from "../../views/credential-create-options.view";
import { CredentialCreateOptionsResponse } from "./response/credential-create-options.response";
import { WebauthnApiService } from "./webauthn-api.service"; import { WebauthnApiService } from "./webauthn-api.service";
import { WebauthnService } from "./webauthn.service"; import { WebauthnService } from "./webauthn.service";
describe("WebauthnService", () => { describe("WebauthnService", () => {
let apiService!: MockProxy<WebauthnApiService>; let apiService!: MockProxy<WebauthnApiService>;
let platformUtilsService!: MockProxy<PlatformUtilsService>;
let i18nService!: MockProxy<I18nService>;
let credentials: MockProxy<CredentialsContainer>; let credentials: MockProxy<CredentialsContainer>;
let webauthnService!: WebauthnService; let webauthnService!: WebauthnService;
@ -23,39 +15,8 @@ describe("WebauthnService", () => {
window.PublicKeyCredential = class {} as any; window.PublicKeyCredential = class {} as any;
window.AuthenticatorAttestationResponse = class {} as any; window.AuthenticatorAttestationResponse = class {} as any;
apiService = mock<WebauthnApiService>(); apiService = mock<WebauthnApiService>();
platformUtilsService = mock<PlatformUtilsService>();
i18nService = mock<I18nService>();
credentials = mock<CredentialsContainer>(); credentials = mock<CredentialsContainer>();
webauthnService = new WebauthnService( webauthnService = new WebauthnService(apiService, credentials);
apiService,
platformUtilsService,
i18nService,
credentials
);
});
describe("getNewCredentialOptions", () => {
it("should return undefined and show toast when api service call throws", async () => {
apiService.getCredentialCreateOptions.mockRejectedValue(new Error("Mock error"));
const verification = createVerification();
const result = await webauthnService.getCredentialCreateOptions(verification);
expect(result).toBeUndefined();
expect(platformUtilsService.showToast).toHaveBeenCalled();
});
it("should return options when api service call is successfull", async () => {
const options = Symbol() as any;
const token = Symbol() as any;
const response = { options, token } as CredentialCreateOptionsResponse;
apiService.getCredentialCreateOptions.mockResolvedValue(response);
const verification = createVerification();
const result = await webauthnService.getCredentialCreateOptions(verification);
expect(result).toEqual({ options, token });
});
}); });
describe("createCredential", () => { describe("createCredential", () => {
@ -78,40 +39,8 @@ describe("WebauthnService", () => {
expect(result).toBe(credential); expect(result).toBe(credential);
}); });
}); });
describe("saveCredential", () => {
it("should return false and show toast when api service call throws", async () => {
apiService.saveCredential.mockRejectedValue(new Error("Mock error"));
const options = createCredentialCreateOptions();
const deviceResponse = Symbol() as any;
const result = await webauthnService.saveCredential(options, deviceResponse, "name");
expect(result).toBe(false);
expect(platformUtilsService.showToast).toHaveBeenCalled();
});
it("should return true when api service call is successfull", async () => {
apiService.saveCredential.mockResolvedValue(true);
const options = createCredentialCreateOptions();
const deviceResponse = createDeviceResponse();
const result = await webauthnService.saveCredential(options, deviceResponse, "name");
expect(result).toBe(true);
expect(apiService.saveCredential).toHaveBeenCalled();
expect(platformUtilsService.showToast).toHaveBeenCalled();
});
});
}); });
function createVerification(): Verification {
return {
type: VerificationType.MasterPassword,
secret: "secret",
};
}
function createCredentialCreateOptions(): CredentialCreateOptionsView { function createCredentialCreateOptions(): CredentialCreateOptionsView {
return new CredentialCreateOptionsView(Symbol() as any, Symbol() as any); return new CredentialCreateOptionsView(Symbol() as any, Symbol() as any);
} }

View File

@ -1,10 +1,7 @@
import { Injectable, Optional } from "@angular/core"; import { Injectable, Optional } from "@angular/core";
import { BehaviorSubject, filter, from, map, Observable, shareReplay, switchMap, tap } from "rxjs"; import { BehaviorSubject, filter, from, map, Observable, shareReplay, switchMap, tap } from "rxjs";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service"; import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { Verification } from "@bitwarden/common/types/verification"; import { Verification } from "@bitwarden/common/types/verification";
import { CoreAuthModule } from "../../core.module"; import { CoreAuthModule } from "../../core.module";
@ -31,8 +28,6 @@ export class WebauthnService {
constructor( constructor(
private apiService: WebauthnApiService, private apiService: WebauthnApiService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
@Optional() navigatorCredentials?: CredentialsContainer, @Optional() navigatorCredentials?: CredentialsContainer,
@Optional() private logService?: LogService @Optional() private logService?: LogService
) { ) {
@ -43,22 +38,8 @@ export class WebauthnService {
async getCredentialCreateOptions( async getCredentialCreateOptions(
verification: Verification verification: Verification
): Promise<CredentialCreateOptionsView | undefined> { ): Promise<CredentialCreateOptionsView | undefined> {
try { const response = await this.apiService.getCredentialCreateOptions(verification);
const response = await this.apiService.getCredentialCreateOptions(verification); return new CredentialCreateOptionsView(response.options, response.token);
return new CredentialCreateOptionsView(response.options, response.token);
} catch (error) {
if (error instanceof ErrorResponse && error.statusCode === 400) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("error"),
this.i18nService.t("invalidMasterPassword")
);
} else {
this.logService?.error(error);
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
}
return undefined;
}
} }
async createCredential( async createCredential(
@ -85,24 +66,12 @@ export class WebauthnService {
deviceResponse: PublicKeyCredential, deviceResponse: PublicKeyCredential,
name: string name: string
) { ) {
try { const request = new SaveCredentialRequest();
const request = new SaveCredentialRequest(); request.deviceResponse = new WebauthnAttestationResponseRequest(deviceResponse);
request.deviceResponse = new WebauthnAttestationResponseRequest(deviceResponse); request.token = credentialOptions.token;
request.token = credentialOptions.token; request.name = name;
request.name = name; await this.apiService.saveCredential(request);
await this.apiService.saveCredential(request); this.refresh();
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("passkeySaved", name)
);
this.refresh();
return true;
} catch (error) {
this.logService?.error(error);
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
return false;
}
} }
getCredential$(credentialId: string): Observable<WebauthnCredentialView> { getCredential$(credentialId: string): Observable<WebauthnCredentialView> {
@ -114,25 +83,9 @@ export class WebauthnService {
); );
} }
async deleteCredential(credentialId: string, verification: Verification): Promise<boolean> { async deleteCredential(credentialId: string, verification: Verification): Promise<void> {
try { await this.apiService.deleteCredential(credentialId, verification);
await this.apiService.deleteCredential(credentialId, verification); this.refresh();
this.platformUtilsService.showToast("success", null, this.i18nService.t("passkeyRemoved"));
this.refresh();
return true;
} catch (error) {
if (error instanceof ErrorResponse && error.statusCode === 400) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("error"),
this.i18nService.t("invalidMasterPassword")
);
} else {
this.logService?.error(error);
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
}
return false;
}
} }
private getCredentials$(): Observable<WebauthnCredentialView[]> { private getCredentials$(): Observable<WebauthnCredentialView[]> {

View File

@ -4,7 +4,11 @@ import { FormBuilder, Validators } from "@angular/forms";
import { map, Observable } from "rxjs"; import { map, Observable } from "rxjs";
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog"; import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { WebauthnService } from "../../../core"; import { WebauthnService } from "../../../core";
import { CredentialCreateOptionsView } from "../../../core/views/credential-create-options.view"; import { CredentialCreateOptionsView } from "../../../core/views/credential-create-options.view";
@ -46,7 +50,10 @@ export class CreateCredentialDialogComponent implements OnInit {
constructor( constructor(
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private dialogRef: DialogRef, private dialogRef: DialogRef,
private webauthnService: WebauthnService private webauthnService: WebauthnService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private logService: LogService
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
@ -80,12 +87,22 @@ export class CreateCredentialDialogComponent implements OnInit {
return; return;
} }
this.credentialOptions = await this.webauthnService.getCredentialCreateOptions({ try {
type: VerificationType.MasterPassword, this.credentialOptions = await this.webauthnService.getCredentialCreateOptions({
secret: this.formGroup.value.userVerification.masterPassword, type: VerificationType.MasterPassword,
}); secret: this.formGroup.value.userVerification.masterPassword,
});
if (this.credentialOptions === undefined) { } catch (error) {
if (error instanceof ErrorResponse && error.statusCode === 400) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("error"),
this.i18nService.t("invalidMasterPassword")
);
} else {
this.logService?.error(error);
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
}
return; return;
} }
@ -114,16 +131,21 @@ export class CreateCredentialDialogComponent implements OnInit {
return; return;
} }
const result = await this.webauthnService.saveCredential( const name = this.formGroup.value.credentialNaming.name;
this.credentialOptions, try {
this.deviceResponse, await this.webauthnService.saveCredential(
this.formGroup.value.credentialNaming.name this.credentialOptions,
); this.deviceResponse,
this.formGroup.value.credentialNaming.name
if (!result) { );
} catch (error) {
this.logService?.error(error);
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
return; return;
} }
this.platformUtilsService.showToast("success", null, this.i18nService.t("passkeySaved", name));
this.dialogRef.close(CreateCredentialDialogResult.Success); this.dialogRef.close(CreateCredentialDialogResult.Success);
} }
} }

View File

@ -4,7 +4,11 @@ import { FormBuilder, Validators } from "@angular/forms";
import { Subject, takeUntil } from "rxjs"; import { Subject, takeUntil } from "rxjs";
import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog"; import { DialogServiceAbstraction } from "@bitwarden/angular/services/dialog";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { WebauthnService } from "../../../core"; import { WebauthnService } from "../../../core";
import { WebauthnCredentialView } from "../../../core/views/webauth-credential.view"; import { WebauthnCredentialView } from "../../../core/views/webauth-credential.view";
@ -28,7 +32,10 @@ export class DeleteCredentialDialogComponent implements OnInit, OnDestroy {
@Inject(DIALOG_DATA) private params: DeleteCredentialDialogParams, @Inject(DIALOG_DATA) private params: DeleteCredentialDialogParams,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private dialogRef: DialogRef, private dialogRef: DialogRef,
private webauthnService: WebauthnService private webauthnService: WebauthnService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService,
private logService: LogService
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
@ -44,14 +51,28 @@ export class DeleteCredentialDialogComponent implements OnInit, OnDestroy {
} }
this.dialogRef.disableClose = true; this.dialogRef.disableClose = true;
const success = await this.webauthnService.deleteCredential(this.credential.id, { try {
type: VerificationType.MasterPassword, await this.webauthnService.deleteCredential(this.credential.id, {
secret: this.formGroup.value.masterPassword, type: VerificationType.MasterPassword,
}); secret: this.formGroup.value.masterPassword,
if (!success) { });
this.platformUtilsService.showToast("success", null, this.i18nService.t("passkeyRemoved"));
} catch (error) {
if (error instanceof ErrorResponse && error.statusCode === 400) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("error"),
this.i18nService.t("invalidMasterPassword")
);
} else {
this.logService.error(error);
this.platformUtilsService.showToast("error", null, this.i18nService.t("unexpectedError"));
}
return false;
} finally {
this.dialogRef.disableClose = false; this.dialogRef.disableClose = false;
return;
} }
this.dialogRef.close(); this.dialogRef.close();
}; };