From 24ac3b3a07913d8bd4fab3565b3e132de2cc9f36 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Wed, 2 Jul 2025 15:26:47 -0500 Subject: [PATCH] [PM-23226] ToTp updating on Desktop (#15435) * update `totpInfo$` observable when the cipher changes * mark cipher as required and remove ignore statements * adds totp countdown tests --- .../totp-countdown.component.spec.ts | 95 +++++++++++++++++++ .../totp-countdown.component.ts | 26 ++++- 2 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 libs/vault/src/components/totp-countdown/totp-countdown.component.spec.ts diff --git a/libs/vault/src/components/totp-countdown/totp-countdown.component.spec.ts b/libs/vault/src/components/totp-countdown/totp-countdown.component.spec.ts new file mode 100644 index 0000000000..58c03b3388 --- /dev/null +++ b/libs/vault/src/components/totp-countdown/totp-countdown.component.spec.ts @@ -0,0 +1,95 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { mock } from "jest-mock-extended"; +import { of } from "rxjs"; + +import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; +import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; + +import { BitTotpCountdownComponent } from "./totp-countdown.component"; + +describe("BitTotpCountdownComponent", () => { + let component: BitTotpCountdownComponent; + let fixture: ComponentFixture; + let totpService: jest.Mocked; + + const mockCipher1 = { + id: "cipher-id", + name: "Test Cipher", + login: { totp: "totp-secret" }, + } as CipherView; + + const mockCipher2 = { + id: "cipher-id-2", + name: "Test Cipher 2", + login: { totp: "totp-secret-2" }, + } as CipherView; + + const mockTotpResponse1 = { + code: "123456", + period: 30, + }; + + const mockTotpResponse2 = { + code: "987654", + period: 10, + }; + + beforeEach(async () => { + totpService = mock({ + getCode$: jest.fn().mockImplementation((totp) => { + if (totp === mockCipher1.login.totp) { + return of(mockTotpResponse1); + } + + return of(mockTotpResponse2); + }), + }); + + await TestBed.configureTestingModule({ + providers: [{ provide: TotpService, useValue: totpService }], + }).compileComponents(); + + fixture = TestBed.createComponent(BitTotpCountdownComponent); + component = fixture.componentInstance; + component.cipher = mockCipher1; + fixture.detectChanges(); + }); + + it("initializes totpInfo$ observable", (done) => { + component.totpInfo$?.subscribe((info) => { + expect(info.totpCode).toBe(mockTotpResponse1.code); + expect(info.totpCodeFormatted).toBe("123 456"); + done(); + }); + }); + + it("emits sendCopyCode when TOTP code is available", (done) => { + const emitter = jest.spyOn(component.sendCopyCode, "emit"); + + component.totpInfo$?.subscribe((info) => { + expect(emitter).toHaveBeenCalledWith({ + totpCode: info.totpCode, + totpCodeFormatted: info.totpCodeFormatted, + }); + done(); + }); + }); + + it("updates totpInfo$ when cipher changes", (done) => { + component.cipher = mockCipher2; + component.ngOnChanges({ + cipher: { + currentValue: mockCipher2, + previousValue: mockCipher1, + firstChange: false, + isFirstChange: () => false, + }, + }); + + component.totpInfo$?.subscribe((info) => { + expect(info.totpCode).toBe(mockTotpResponse2.code); + expect(info.totpCodeFormatted).toBe("987 654"); + done(); + }); + }); +}); diff --git a/libs/vault/src/components/totp-countdown/totp-countdown.component.ts b/libs/vault/src/components/totp-countdown/totp-countdown.component.ts index c634b1165d..5274ce621f 100644 --- a/libs/vault/src/components/totp-countdown/totp-countdown.component.ts +++ b/libs/vault/src/components/totp-countdown/totp-countdown.component.ts @@ -1,7 +1,13 @@ -// FIXME: Update this file to be type safe and remove this and next line -// @ts-strict-ignore import { CommonModule } from "@angular/common"; -import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; +import { + Component, + EventEmitter, + Input, + OnInit, + Output, + OnChanges, + SimpleChanges, +} from "@angular/core"; import { Observable, map, tap } from "rxjs"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; @@ -14,8 +20,8 @@ import { TypographyModule } from "@bitwarden/components"; templateUrl: "totp-countdown.component.html", imports: [CommonModule, TypographyModule], }) -export class BitTotpCountdownComponent implements OnInit { - @Input() cipher: CipherView; +export class BitTotpCountdownComponent implements OnInit, OnChanges { + @Input({ required: true }) cipher!: CipherView; @Output() sendCopyCode = new EventEmitter(); /** @@ -26,6 +32,16 @@ export class BitTotpCountdownComponent implements OnInit { constructor(protected totpService: TotpService) {} async ngOnInit() { + this.setTotpInfo(); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes["cipher"]) { + this.setTotpInfo(); + } + } + + private setTotpInfo(): void { this.totpInfo$ = this.cipher?.login?.totp ? this.totpService.getCode$(this.cipher.login.totp).pipe( map((response) => {