From 4e7399ed98985dd7041a54f3487bb1dc96f616be Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Mon, 9 Sep 2024 16:36:23 -0500 Subject: [PATCH] [PM-9733] Custom Vault Timeout (#10515) * handle timeout changes that are predefined string values - Passing a string to `Math.max` will cause a NaN to be set. * type form instance so TypeScript is more aware of the form values --- .../vault-timeout-input.component.spec.ts | 81 +++++++++++++++++++ .../vault-timeout-input.component.ts | 22 ++--- 2 files changed, 94 insertions(+), 9 deletions(-) create mode 100644 libs/auth/src/angular/vault-timeout-input/vault-timeout-input.component.spec.ts diff --git a/libs/auth/src/angular/vault-timeout-input/vault-timeout-input.component.spec.ts b/libs/auth/src/angular/vault-timeout-input/vault-timeout-input.component.spec.ts new file mode 100644 index 0000000000..6b39c90fc9 --- /dev/null +++ b/libs/auth/src/angular/vault-timeout-input/vault-timeout-input.component.spec.ts @@ -0,0 +1,81 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { BehaviorSubject } from "rxjs"; + +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; + +import { VaultTimeoutInputComponent } from "./vault-timeout-input.component"; + +describe("VaultTimeoutInputComponent", () => { + let component: VaultTimeoutInputComponent; + let fixture: ComponentFixture; + const get$ = jest.fn().mockReturnValue(new BehaviorSubject({})); + const availableVaultTimeoutActions$ = jest.fn().mockReturnValue(new BehaviorSubject([])); + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [VaultTimeoutInputComponent], + providers: [ + { provide: PolicyService, useValue: { get$ } }, + { provide: VaultTimeoutSettingsService, useValue: { availableVaultTimeoutActions$ } }, + { provide: I18nService, useValue: { t: (key: string) => key } }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(VaultTimeoutInputComponent); + component = fixture.componentInstance; + component.vaultTimeoutOptions = [ + { name: "oneMinute", value: 1 }, + { name: "fiveMinutes", value: 5 }, + { name: "fifteenMinutes", value: 15 }, + { name: "thirtyMinutes", value: 30 }, + { name: "oneHour", value: 60 }, + { name: "fourHours", value: 240 }, + { name: "onRefresh", value: VaultTimeoutStringType.OnRestart }, + ]; + fixture.detectChanges(); + }); + + describe("form", () => { + beforeEach(async () => { + await component.ngOnInit(); + }); + + it("invokes the onChange associated with `ControlValueAccessor`", () => { + const onChange = jest.fn(); + component.registerOnChange(onChange); + + component.form.controls.vaultTimeout.setValue(VaultTimeoutStringType.OnRestart); + + expect(onChange).toHaveBeenCalledWith(VaultTimeoutStringType.OnRestart); + }); + + it("updates custom value to match preset option", () => { + // 1 hour + component.form.controls.vaultTimeout.setValue(60); + + expect(component.form.value.custom).toEqual({ hours: 1, minutes: 0 }); + + // 17 minutes + component.form.controls.vaultTimeout.setValue(17); + + expect(component.form.value.custom).toEqual({ hours: 0, minutes: 17 }); + + // 2.25 hours + component.form.controls.vaultTimeout.setValue(135); + + expect(component.form.value.custom).toEqual({ hours: 2, minutes: 15 }); + }); + + it("sets custom timeout to 0 when a preset string option is selected", () => { + // Set custom value to random values + component.form.controls.custom.setValue({ hours: 1, minutes: 1 }); + + component.form.controls.vaultTimeout.setValue(VaultTimeoutStringType.OnLocked); + + expect(component.form.value.custom).toEqual({ hours: 0, minutes: 0 }); + }); + }); +}); diff --git a/libs/auth/src/angular/vault-timeout-input/vault-timeout-input.component.ts b/libs/auth/src/angular/vault-timeout-input/vault-timeout-input.component.ts index f1cfcec8c3..7c76083560 100644 --- a/libs/auth/src/angular/vault-timeout-input/vault-timeout-input.component.ts +++ b/libs/auth/src/angular/vault-timeout-input/vault-timeout-input.component.ts @@ -4,6 +4,8 @@ import { AbstractControl, ControlValueAccessor, FormBuilder, + FormControl, + FormGroup, NG_VALIDATORS, NG_VALUE_ACCESSOR, ReactiveFormsModule, @@ -22,13 +24,15 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { VaultTimeout, VaultTimeoutOption } from "@bitwarden/common/types/vault-timeout.type"; import { FormFieldModule, SelectModule } from "@bitwarden/components"; -interface VaultTimeoutFormValue { - vaultTimeout: VaultTimeout | null; - custom: { - hours: number | null; - minutes: number | null; - }; -} +type VaultTimeoutForm = FormGroup<{ + vaultTimeout: FormControl; + custom: FormGroup<{ + hours: FormControl; + minutes: FormControl; + }>; +}>; + +type VaultTimeoutFormValue = VaultTimeoutForm["value"]; @Component({ selector: "auth-vault-timeout-input", @@ -64,7 +68,7 @@ export class VaultTimeoutInputComponent static CUSTOM_VALUE = -100; static MIN_CUSTOM_MINUTES = 0; - form = this.formBuilder.group({ + form: VaultTimeoutForm = this.formBuilder.group({ vaultTimeout: [null], custom: this.formBuilder.group({ hours: [null], @@ -120,7 +124,7 @@ export class VaultTimeoutInputComponent takeUntil(this.destroy$), ) .subscribe((value) => { - const current = Math.max(value, 0); + const current = typeof value === "string" ? 0 : Math.max(value, 0); // This cannot emit an event b/c it would cause form.valueChanges to fire again // and we are already handling that above so just silently update