From 7dcd3fa60383fc4bf3482a2c03fdb20ab716f89d Mon Sep 17 00:00:00 2001 From: Daniel Riera Date: Mon, 20 Oct 2025 10:34:21 -0400 Subject: [PATCH] [PM-26302] Extension when launching a website from a vault item autofill fills wrong cipher (#16810) * PM-26302 create helper that will assign local data to ciphers and cached ciphers on decryption * remove helper and call local data only on get cipher for url to make operation less expensive * add tests for local data using functions that call the getcipherforurl function * reorder to have early null check --- .../src/vault/services/cipher.service.spec.ts | 71 +++++++++++++++++++ .../src/vault/services/cipher.service.ts | 14 +++- 2 files changed, 83 insertions(+), 2 deletions(-) diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index 9b1d8096fc7..e6c22961673 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -923,4 +923,75 @@ describe("Cipher Service", () => { sub.unsubscribe(); }); }); + + describe("getCipherForUrl localData application", () => { + beforeEach(() => { + Object.defineProperty(autofillSettingsService, "autofillOnPageLoadDefault$", { + value: of(true), + writable: true, + }); + }); + + it("should apply localData to ciphers when getCipherForUrl is called via getLastLaunchedForUrl", async () => { + const testUrl = "https://test-url.com"; + const cipherId = "test-cipher-id" as CipherId; + const testLocalData = { + lastLaunched: Date.now().valueOf(), + lastUsedDate: Date.now().valueOf() - 1000, + }; + + jest.spyOn(cipherService, "localData$").mockReturnValue(of({ [cipherId]: testLocalData })); + + const mockCipherView = new CipherView(); + mockCipherView.id = cipherId; + mockCipherView.localData = null; + + jest.spyOn(cipherService, "getAllDecryptedForUrl").mockResolvedValue([mockCipherView]); + + const result = await cipherService.getLastLaunchedForUrl(testUrl, userId, true); + + expect(result.localData).toEqual(testLocalData); + }); + + it("should apply localData to ciphers when getCipherForUrl is called via getLastUsedForUrl", async () => { + const testUrl = "https://test-url.com"; + const cipherId = "test-cipher-id" as CipherId; + const testLocalData = { lastUsedDate: Date.now().valueOf() - 1000 }; + + jest.spyOn(cipherService, "localData$").mockReturnValue(of({ [cipherId]: testLocalData })); + + const mockCipherView = new CipherView(); + mockCipherView.id = cipherId; + mockCipherView.localData = null; + + jest.spyOn(cipherService, "getAllDecryptedForUrl").mockResolvedValue([mockCipherView]); + + const result = await cipherService.getLastUsedForUrl(testUrl, userId, true); + + expect(result.localData).toEqual(testLocalData); + }); + + it("should not modify localData if it already matches in getCipherForUrl", async () => { + const testUrl = "https://test-url.com"; + const cipherId = "test-cipher-id" as CipherId; + const existingLocalData = { + lastLaunched: Date.now().valueOf(), + lastUsedDate: Date.now().valueOf() - 1000, + }; + + jest + .spyOn(cipherService, "localData$") + .mockReturnValue(of({ [cipherId]: existingLocalData })); + + const mockCipherView = new CipherView(); + mockCipherView.id = cipherId; + mockCipherView.localData = existingLocalData; + + jest.spyOn(cipherService, "getAllDecryptedForUrl").mockResolvedValue([mockCipherView]); + + const result = await cipherService.getLastLaunchedForUrl(testUrl, userId, true); + + expect(result.localData).toBe(existingLocalData); + }); + }); }); diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 4bdc0d9b9fd..41f94e02cdf 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -1947,13 +1947,23 @@ export class CipherService implements CipherServiceAbstraction { autofillOnPageLoad: boolean, ): Promise { const cacheKey = autofillOnPageLoad ? "autofillOnPageLoad-" + url : url; - if (!this.sortedCiphersCache.isCached(cacheKey)) { let ciphers = await this.getAllDecryptedForUrl(url, userId); - if (!ciphers) { + + if (!ciphers?.length) { return null; } + const localData = await firstValueFrom(this.localData$(userId)); + if (localData) { + for (const view of ciphers) { + const data = localData[view.id as CipherId]; + if (data) { + view.localData = data; + } + } + } + if (autofillOnPageLoad) { const autofillOnPageLoadDefault = await this.getAutofillOnPageLoadDefault();