From 1cfbcf4ee09b0285767dc5607fb1df99eab88c33 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Wed, 5 Jun 2024 08:10:36 -0700 Subject: [PATCH] [PM-8334] Sort ciphers after autofill (#9511) * [PM-8334] Add localData$ to CipherService and watch it for updates * Fix leftover tw-fixed class * [PM-8334] Fix tests --- .../vault-popup-items.service.spec.ts | 32 ++++++++++++++++++- .../services/vault-popup-items.service.ts | 7 ++-- .../src/vault/abstractions/cipher.service.ts | 3 ++ 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts index 6a7cbbfc1a..b7091eb87b 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts @@ -6,6 +6,7 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { ProductType } from "@bitwarden/common/enums"; +import { ObservableTracker } from "@bitwarden/common/spec"; import { CipherId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; @@ -50,7 +51,8 @@ describe("VaultPopupItemsService", () => { cipherList[3].favorite = true; cipherServiceMock.getAllDecrypted.mockResolvedValue(cipherList); - cipherServiceMock.ciphers$ = new BehaviorSubject(null).asObservable(); + cipherServiceMock.ciphers$ = new BehaviorSubject(null); + cipherServiceMock.localData$ = new BehaviorSubject(null); searchService.searchCiphers.mockImplementation(async (_, __, ciphers) => ciphers); cipherServiceMock.filterCiphersForUrl.mockImplementation(async (ciphers) => ciphers.filter((c) => ["0", "1"].includes(c.id)), @@ -123,6 +125,34 @@ describe("VaultPopupItemsService", () => { }); }); + it("should update cipher list when cipherService.ciphers$ emits", async () => { + const tracker = new ObservableTracker(service.autoFillCiphers$); + + await tracker.expectEmission(); + + (cipherServiceMock.ciphers$ as BehaviorSubject).next(null); + + await tracker.expectEmission(); + + // Should only emit twice + expect(tracker.emissions.length).toBe(2); + await expect(tracker.pauseUntilReceived(3)).rejects.toThrow("Timeout exceeded"); + }); + + it("should update cipher list when cipherService.localData$ emits", async () => { + const tracker = new ObservableTracker(service.autoFillCiphers$); + + await tracker.expectEmission(); + + (cipherServiceMock.localData$ as BehaviorSubject).next(null); + + await tracker.expectEmission(); + + // Should only emit twice + expect(tracker.emissions.length).toBe(2); + await expect(tracker.pauseUntilReceived(3)).rejects.toThrow("Timeout exceeded"); + }); + describe("autoFillCiphers$", () => { it("should return empty array if there is no current tab", (done) => { jest.spyOn(BrowserApi, "getTabFromCurrentWindow").mockResolvedValue(null); diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index ab98722d06..189ce2c09f 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -5,6 +5,7 @@ import { distinctUntilKeyChanged, from, map, + merge, Observable, of, shareReplay, @@ -78,10 +79,12 @@ export class VaultPopupItemsService { * Observable that contains the list of all decrypted ciphers. * @private */ - private _cipherList$: Observable = this.cipherService.ciphers$.pipe( + private _cipherList$: Observable = merge( + this.cipherService.ciphers$, + this.cipherService.localData$, + ).pipe( runInsideAngular(inject(NgZone)), // Workaround to ensure cipher$ state provider emissions are run inside Angular switchMap(() => Utils.asyncToObservable(() => this.cipherService.getAllDecrypted())), - map((ciphers) => Object.values(ciphers)), switchMap((ciphers) => combineLatest([ this.organizationService.organizations$, diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 34bc819355..2c0676f644 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -1,5 +1,7 @@ import { Observable } from "rxjs"; +import { LocalData } from "@bitwarden/common/vault/models/data/local.data"; + import { UriMatchStrategySetting } from "../../models/domain/domain-service"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { CipherId, CollectionId, OrganizationId } from "../../types/guid"; @@ -14,6 +16,7 @@ import { AddEditCipherInfo } from "../types/add-edit-cipher-info"; export abstract class CipherService { cipherViews$: Observable>; ciphers$: Observable>; + localData$: Observable>; /** * An observable monitoring the add/edit cipher info saved to memory. */