Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions packages/extension/src/background_script.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"
import { enhancedSettings } from "@/services/settings/enhancedSettings"
import { Settings } from "@/services/settings/settings"
import { BgCommand } from "@/services/ipc"
import { NEW_HUB_URL } from "@/const"

// Mock dependencies
vi.mock("@/services/settings/enhancedSettings")
Expand Down Expand Up @@ -578,6 +579,7 @@ describe("onInstalled: installed analytics event", () => {
vi.doMock("@/services/analytics", () => ({
ANALYTICS_EVENTS: { INSTALLED: "installed" },
sendEvent: mockSendEvent,
getOrCreateClientId: vi.fn().mockResolvedValue("test-client-id"),
}))

vi.resetModules()
Expand All @@ -600,6 +602,7 @@ describe("onInstalled: installed analytics event", () => {
vi.doMock("@/services/analytics", () => ({
ANALYTICS_EVENTS: { INSTALLED: "installed" },
sendEvent: mockSendEvent,
getOrCreateClientId: vi.fn().mockResolvedValue("test-client-id"),
}))

vi.resetModules()
Expand All @@ -620,3 +623,76 @@ describe("onInstalled: installed analytics event", () => {
)
})
})

describe("Uninstall URL (onInstalled)", () => {
beforeEach(() => {
vi.clearAllMocks()
})

it("UN-01: should set uninstall URL with client_id on install", async () => {
const mockGetOrCreateClientId = vi.fn().mockResolvedValue("test-client-id")
vi.doMock("@/services/analytics", () => ({
ANALYTICS_EVENTS: { INSTALLED: "installed" },
sendEvent: vi.fn(),
getOrCreateClientId: mockGetOrCreateClientId,
}))

vi.resetModules()
await import("./background_script")

const listenerCalls = (chrome.runtime.onInstalled.addListener as any).mock
.calls
expect(listenerCalls.length).toBeGreaterThan(0)
const onInstalledListener = listenerCalls[0][0]

await onInstalledListener({
reason: chrome.runtime.OnInstalledReason.INSTALL,
})
await new Promise((resolve) => setTimeout(resolve, 10))

expect(mockGetOrCreateClientId).toHaveBeenCalled()
expect(chrome.runtime.setUninstallURL).toHaveBeenCalledWith(
`${NEW_HUB_URL}/uninstall?client_id=test-client-id`,
)
})

it("UN-02: should fall back to the uninstall URL without client_id when getOrCreateClientId fails, without skipping the rest of initialization", async () => {
const consoleErrorSpy = vi
.spyOn(console, "error")
.mockImplementation(() => {})
const mockGetOrCreateClientId = vi
.fn()
.mockRejectedValue(new Error("Quota exceeded"))
vi.doMock("@/services/analytics", () => ({
ANALYTICS_EVENTS: { INSTALLED: "installed" },
sendEvent: vi.fn(),
getOrCreateClientId: mockGetOrCreateClientId,
}))

vi.resetModules()
await import("./background_script")

const listenerCalls = (chrome.runtime.onInstalled.addListener as any).mock
.calls
const onInstalledListener = listenerCalls[0][0]

await onInstalledListener({
reason: chrome.runtime.OnInstalledReason.INSTALL,
})
await new Promise((resolve) => setTimeout(resolve, 10))

// Falls back to the URL without client_id instead of leaving the
// uninstall URL unset.
expect(chrome.runtime.setUninstallURL).toHaveBeenCalledWith(
`${NEW_HUB_URL}/uninstall`,
)
// The client_id failure must not propagate to the outer catch, which
// would otherwise skip the daily/weekly backup checks that follow.
expect(consoleErrorSpy).not.toHaveBeenCalledWith(
"Error during onInstalled initialization:",
expect.anything(),
)

consoleErrorSpy.mockRestore()
})
})
20 changes: 17 additions & 3 deletions packages/extension/src/background_script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ import { execute } from "@/action/background"
import * as ActionHelper from "@/action/helper"
import type { WindowType } from "@/types"
import { Storage, SESSION_STORAGE_KEY } from "@/services/storage"
import { ANALYTICS_EVENTS, sendEvent } from "@/services/analytics"
import {
ANALYTICS_EVENTS,
sendEvent,
getOrCreateClientId,
} from "@/services/analytics"
import * as HubBackground from "@/services/hub/background"

import { importIf } from "@import-if"
Expand Down Expand Up @@ -453,8 +457,18 @@ chrome.runtime.onInstalled.addListener(async (details) => {
details.reason === chrome.runtime.OnInstalledReason.INSTALL ||
details.reason === chrome.runtime.OnInstalledReason.UPDATE
) {
// Set uninstall survey URL
chrome.runtime.setUninstallURL(`${NEW_HUB_URL}/uninstall`)
// Set uninstall survey URL with client_id for analysis.
// Wrapped in its own try/catch so a failure here (e.g. storage quota
// error) does not skip the backup checks below.
try {
const clientId = await getOrCreateClientId()
chrome.runtime.setUninstallURL(
`${NEW_HUB_URL}/uninstall?client_id=${clientId}`,
)
} catch (error) {
console.error("Failed to set uninstall URL with client_id:", error)
chrome.runtime.setUninstallURL(`${NEW_HUB_URL}/uninstall`)
}
}

// Check for daily backup on startup
Expand Down
8 changes: 4 additions & 4 deletions packages/extension/src/test/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,16 +288,16 @@ global.chrome = {
},
runtime: {
sendMessage: vi.fn(),
onInstalled: {
addListener: vi.fn(),
},
setUninstallURL: vi.fn(),
OnInstalledReason: {
INSTALL: "install",
UPDATE: "update",
CHROME_UPDATE: "chrome_update",
SHARED_MODULE_UPDATE: "shared_module_update",
},
setUninstallURL: vi.fn(),
onInstalled: {
addListener: vi.fn(),
},
onStartup: {
addListener: vi.fn(),
},
Expand Down
Loading