1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-04 02:33:33 +00:00

Added addUrls function to use instead of saveUrls so appending daily does not clear all urls

This commit is contained in:
Leslie Tilton
2026-01-22 19:38:32 -06:00
parent 04585dfa74
commit 1450c45b79
4 changed files with 105 additions and 6 deletions

View File

@@ -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<PlatformUtilsService>();
@@ -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 () => {

View File

@@ -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))),
);
}

View File

@@ -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" });

View File

@@ -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<boolean> {
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.
*/