From 1450c45b797bb620916da18dff6663951bfa5350 Mon Sep 17 00:00:00 2001 From: Leslie Tilton <23057410+Banrion@users.noreply.github.com> Date: Thu, 22 Jan 2026 19:38:32 -0600 Subject: [PATCH] Added addUrls function to use instead of saveUrls so appending daily does not clear all urls --- .../services/phishing-data.service.spec.ts | 8 +- .../services/phishing-data.service.ts | 2 +- .../phishing-indexeddb.service.spec.ts | 80 +++++++++++++++++++ .../services/phishing-indexeddb.service.ts | 21 +++++ 4 files changed, 105 insertions(+), 6 deletions(-) diff --git a/apps/browser/src/dirt/phishing-detection/services/phishing-data.service.spec.ts b/apps/browser/src/dirt/phishing-detection/services/phishing-data.service.spec.ts index ed3df0904aa..34a8160da8f 100644 --- a/apps/browser/src/dirt/phishing-detection/services/phishing-data.service.spec.ts +++ b/apps/browser/src/dirt/phishing-detection/services/phishing-data.service.spec.ts @@ -41,6 +41,7 @@ describe("PhishingDataService", () => { mockIndexedDbService.hasUrl.mockResolvedValue(false); mockIndexedDbService.loadAllUrls.mockResolvedValue([]); mockIndexedDbService.saveUrls.mockResolvedValue(undefined); + mockIndexedDbService.addUrls.mockResolvedValue(undefined); mockIndexedDbService.saveUrlsFromStream.mockResolvedValue(undefined); platformUtilsService = mock(); @@ -139,7 +140,7 @@ describe("PhishingDataService", () => { expect(mockIndexedDbService.saveUrlsFromStream).toHaveBeenCalled(); }); - it("should update daily dataset via saveUrls", async () => { + it("should update daily dataset via addUrls", async () => { // Mock daily update const mockResponse = { ok: true, @@ -149,10 +150,7 @@ describe("PhishingDataService", () => { await firstValueFrom(service["_updateDailyDataSet"]()); - expect(mockIndexedDbService.saveUrls).toHaveBeenCalledWith([ - "newphish.com", - "anotherbad.net", - ]); + expect(mockIndexedDbService.addUrls).toHaveBeenCalledWith(["newphish.com", "anotherbad.net"]); }); it("should get updated meta information", async () => { diff --git a/apps/browser/src/dirt/phishing-detection/services/phishing-data.service.ts b/apps/browser/src/dirt/phishing-detection/services/phishing-data.service.ts index f4c7cb62639..1db2d500cf5 100644 --- a/apps/browser/src/dirt/phishing-detection/services/phishing-data.service.ts +++ b/apps/browser/src/dirt/phishing-detection/services/phishing-data.service.ts @@ -260,7 +260,7 @@ export class PhishingDataService { } return from(this.fetchToday(todayUrl)).pipe( - switchMap((lines) => from(this.indexedDbService.saveUrls(lines))), + switchMap((lines) => from(this.indexedDbService.addUrls(lines))), ); } diff --git a/apps/browser/src/dirt/phishing-detection/services/phishing-indexeddb.service.spec.ts b/apps/browser/src/dirt/phishing-detection/services/phishing-indexeddb.service.spec.ts index 75bd634b1fc..99e101cc199 100644 --- a/apps/browser/src/dirt/phishing-detection/services/phishing-indexeddb.service.spec.ts +++ b/apps/browser/src/dirt/phishing-detection/services/phishing-indexeddb.service.spec.ts @@ -215,6 +215,86 @@ describe("PhishingIndexedDbService", () => { }); }); + describe("addUrls", () => { + it("appends URLs to IndexedDB without clearing", async () => { + // Pre-populate store with existing data + mockStore.set("https://existing.com", { url: "https://existing.com" }); + + const urls = ["https://phishing.com", "https://malware.net"]; + const result = await service.addUrls(urls); + + expect(result).toBe(true); + expect(mockDb.transaction).toHaveBeenCalledWith("phishing-urls", "readwrite"); + expect(mockObjectStore.clear).not.toHaveBeenCalled(); + expect(mockObjectStore.put).toHaveBeenCalledTimes(2); + // Existing data should still be present + expect(mockStore.has("https://existing.com")).toBe(true); + expect(mockStore.size).toBe(3); + expect(mockDb.close).toHaveBeenCalled(); + }); + + it("handles empty array without clearing", async () => { + mockStore.set("https://existing.com", { url: "https://existing.com" }); + + const result = await service.addUrls([]); + + expect(result).toBe(true); + expect(mockObjectStore.clear).not.toHaveBeenCalled(); + expect(mockStore.has("https://existing.com")).toBe(true); + }); + + it("trims whitespace from URLs", async () => { + const urls = [" https://example.com ", "\nhttps://test.org\n"]; + + await service.addUrls(urls); + + expect(mockObjectStore.put).toHaveBeenCalledWith({ url: "https://example.com" }); + expect(mockObjectStore.put).toHaveBeenCalledWith({ url: "https://test.org" }); + }); + + it("skips empty lines", async () => { + const urls = ["https://example.com", "", " ", "https://test.org"]; + + await service.addUrls(urls); + + expect(mockObjectStore.put).toHaveBeenCalledTimes(2); + }); + + it("handles duplicate URLs via upsert", async () => { + mockStore.set("https://example.com", { url: "https://example.com" }); + + const urls = [ + "https://example.com", // Already exists + "https://test.org", + ]; + + const result = await service.addUrls(urls); + + expect(result).toBe(true); + expect(mockObjectStore.put).toHaveBeenCalledTimes(2); + expect(mockStore.size).toBe(2); + }); + + it("logs error and returns false on failure", async () => { + const error = new Error("IndexedDB error"); + mockOpenRequest.error = error; + (global.indexedDB.open as jest.Mock).mockImplementation(() => { + setTimeout(() => { + mockOpenRequest.onerror?.(); + }, 0); + return mockOpenRequest; + }); + + const result = await service.addUrls(["https://test.com"]); + + expect(result).toBe(false); + expect(logService.error).toHaveBeenCalledWith( + "[PhishingIndexedDbService] Add failed", + expect.any(Error), + ); + }); + }); + describe("hasUrl", () => { it("returns true for existing URL", async () => { mockStore.set("https://example.com", { url: "https://example.com" }); diff --git a/apps/browser/src/dirt/phishing-detection/services/phishing-indexeddb.service.ts b/apps/browser/src/dirt/phishing-detection/services/phishing-indexeddb.service.ts index 099839a38d9..8e7df785dd6 100644 --- a/apps/browser/src/dirt/phishing-detection/services/phishing-indexeddb.service.ts +++ b/apps/browser/src/dirt/phishing-detection/services/phishing-indexeddb.service.ts @@ -67,6 +67,27 @@ export class PhishingIndexedDbService { } } + /** + * Adds an array of phishing URLs to IndexedDB. + * Appends to existing data without clearing. + * + * @param urls - Array of phishing URLs to add + * @returns `true` if add succeeded, `false` on error + */ + async addUrls(urls: string[]): Promise { + let db: IDBDatabase | null = null; + try { + db = await this.openDatabase(); + await this.saveChunked(db, urls); + return true; + } catch (error) { + this.logService.error("[PhishingIndexedDbService] Add failed", error); + return false; + } finally { + db?.close(); + } + } + /** * Saves URLs in chunks to prevent transaction timeouts and UI freezes. */