diff --git a/apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.spec.ts b/apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.spec.ts new file mode 100644 index 0000000000..110326e3d7 --- /dev/null +++ b/apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.spec.ts @@ -0,0 +1,127 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { Observable } from "rxjs"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { ScheduledTaskNames } from "@bitwarden/common/platform/enums/scheduled-task-name.enum"; +import { GlobalState, StateProvider } from "@bitwarden/common/platform/state"; + +import { createPortSpyMock } from "../../../autofill/spec/autofill-mocks"; +import { + flushPromises, + sendPortMessage, + triggerPortOnDisconnectEvent, + triggerRuntimeOnConnectEvent, +} from "../../../autofill/spec/testing-utils"; +import { + BrowserTaskSchedulerPortActions, + BrowserTaskSchedulerPortName, +} from "../abstractions/browser-task-scheduler.service"; + +import { BackgroundTaskSchedulerService } from "./background-task-scheduler.service"; + +describe("BackgroundTaskSchedulerService", () => { + let logService: MockProxy; + let stateProvider: MockProxy; + let globalStateMock: MockProxy>; + let portMock: chrome.runtime.Port; + let backgroundTaskSchedulerService: BackgroundTaskSchedulerService; + + beforeEach(() => { + logService = mock(); + globalStateMock = mock>({ + state$: mock>(), + update: jest.fn((callback) => callback([], {} as any)), + }); + stateProvider = mock({ + getGlobal: jest.fn(() => globalStateMock), + }); + portMock = createPortSpyMock(BrowserTaskSchedulerPortName); + backgroundTaskSchedulerService = new BackgroundTaskSchedulerService(logService, stateProvider); + jest.spyOn(globalThis, "setTimeout"); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("ports on connect", () => { + it("ignores port connections that do not have the correct task scheduler port name", () => { + const portMockWithDifferentName = createPortSpyMock("different-name"); + triggerRuntimeOnConnectEvent(portMockWithDifferentName); + + expect(portMockWithDifferentName.onMessage.addListener).not.toHaveBeenCalled(); + expect(portMockWithDifferentName.onDisconnect.addListener).not.toHaveBeenCalled(); + }); + + it("sets up onMessage and onDisconnect listeners for connected ports", () => { + triggerRuntimeOnConnectEvent(portMock); + + expect(portMock.onMessage.addListener).toHaveBeenCalled(); + expect(portMock.onDisconnect.addListener).toHaveBeenCalled(); + }); + }); + + describe("ports on disconnect", () => { + it("removes the port from the set of connected ports", () => { + triggerRuntimeOnConnectEvent(portMock); + expect(backgroundTaskSchedulerService["ports"].size).toBe(1); + + triggerPortOnDisconnectEvent(portMock); + expect(backgroundTaskSchedulerService["ports"].size).toBe(0); + }); + }); + + describe("port message handlers", () => { + beforeEach(() => { + triggerRuntimeOnConnectEvent(portMock); + backgroundTaskSchedulerService.registerTaskHandler( + ScheduledTaskNames.loginStrategySessionTimeout, + jest.fn(), + ); + }); + + it("sets a setTimeout backup alarm", async () => { + sendPortMessage(portMock, { + action: BrowserTaskSchedulerPortActions.setTimeout, + taskName: ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs: 1000, + }); + await flushPromises(); + + expect(globalThis.setTimeout).toHaveBeenCalled(); + expect(chrome.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + { delayInMinutes: 0.5 }, + expect.any(Function), + ); + }); + + it("sets a setInterval backup alarm", async () => { + sendPortMessage(portMock, { + action: BrowserTaskSchedulerPortActions.setInterval, + taskName: ScheduledTaskNames.loginStrategySessionTimeout, + intervalInMs: 600000, + }); + await flushPromises(); + + expect(chrome.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + { delayInMinutes: 10, periodInMinutes: 10 }, + expect.any(Function), + ); + }); + + it("clears a scheduled alarm", async () => { + sendPortMessage(portMock, { + action: BrowserTaskSchedulerPortActions.clearAlarm, + alarmName: ScheduledTaskNames.loginStrategySessionTimeout, + }); + await flushPromises(); + + expect(chrome.alarms.clear).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + expect.any(Function), + ); + }); + }); +}); diff --git a/apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.ts b/apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.ts index 3768b1b29e..52a3bf656e 100644 --- a/apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.ts +++ b/apps/browser/src/platform/services/task-scheduler/background-task-scheduler.service.ts @@ -25,6 +25,10 @@ export class BackgroundTaskSchedulerService extends BrowserTaskSchedulerServiceI * @param port - The port that was connected. */ private handlePortOnConnect = (port: chrome.runtime.Port) => { + if (port.name !== BrowserTaskSchedulerPortName) { + return; + } + this.ports.add(port); port.onMessage.addListener(this.handlePortMessage); port.onDisconnect.addListener(this.handlePortOnDisconnect); @@ -50,10 +54,6 @@ export class BackgroundTaskSchedulerService extends BrowserTaskSchedulerServiceI message: BrowserTaskSchedulerPortMessage, port: chrome.runtime.Port, ) => { - if (port.name !== BrowserTaskSchedulerPortName) { - return; - } - if (message.action === BrowserTaskSchedulerPortActions.setTimeout) { super.setTimeout(message.taskName, message.delayInMs); return; @@ -65,8 +65,7 @@ export class BackgroundTaskSchedulerService extends BrowserTaskSchedulerServiceI } if (message.action === BrowserTaskSchedulerPortActions.clearAlarm) { - void super.clearScheduledAlarm(message.alarmName); - return; + super.clearScheduledAlarm(message.alarmName).catch((error) => this.logService.error(error)); } }; } diff --git a/apps/browser/src/platform/services/task-scheduler/foreground-task-scheduler.service.spec.ts b/apps/browser/src/platform/services/task-scheduler/foreground-task-scheduler.service.spec.ts new file mode 100644 index 0000000000..0a3cabcfb6 --- /dev/null +++ b/apps/browser/src/platform/services/task-scheduler/foreground-task-scheduler.service.spec.ts @@ -0,0 +1,79 @@ +import { mock, MockProxy } from "jest-mock-extended"; +import { Observable } from "rxjs"; + +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { ScheduledTaskNames } from "@bitwarden/common/platform/enums/scheduled-task-name.enum"; +import { GlobalState, StateProvider } from "@bitwarden/common/platform/state"; + +import { createPortSpyMock } from "../../../autofill/spec/autofill-mocks"; +import { flushPromises } from "../../../autofill/spec/testing-utils"; +import { + BrowserTaskSchedulerPortActions, + BrowserTaskSchedulerPortName, +} from "../abstractions/browser-task-scheduler.service"; + +import { ForegroundTaskSchedulerService } from "./foreground-task-scheduler.service"; + +describe("ForegroundTaskSchedulerService", () => { + let logService: MockProxy; + let stateProvider: MockProxy; + let globalStateMock: MockProxy>; + let portMock: chrome.runtime.Port; + let foregroundTaskSchedulerService: ForegroundTaskSchedulerService; + + beforeEach(() => { + logService = mock(); + globalStateMock = mock>({ + state$: mock>(), + update: jest.fn((callback) => callback([], {} as any)), + }); + stateProvider = mock({ + getGlobal: jest.fn(() => globalStateMock), + }); + portMock = createPortSpyMock(BrowserTaskSchedulerPortName); + foregroundTaskSchedulerService = new ForegroundTaskSchedulerService(logService, stateProvider); + foregroundTaskSchedulerService["port"] = portMock; + foregroundTaskSchedulerService.registerTaskHandler( + ScheduledTaskNames.loginStrategySessionTimeout, + jest.fn(), + ); + jest.spyOn(globalThis, "setTimeout"); + jest.spyOn(globalThis, "setInterval"); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("sets a timeout for a task and sends a message to the background to set up a backup timeout alarm", async () => { + foregroundTaskSchedulerService.setTimeout(ScheduledTaskNames.loginStrategySessionTimeout, 1000); + await flushPromises(); + + expect(globalThis.setTimeout).toHaveBeenCalledWith(expect.any(Function), 1000); + expect(chrome.alarms.create).toHaveBeenCalledWith( + "loginStrategySessionTimeout", + { delayInMinutes: 0.5 }, + expect.any(Function), + ); + expect(portMock.postMessage).toHaveBeenCalledWith({ + action: BrowserTaskSchedulerPortActions.setTimeout, + taskName: ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs: 1000, + }); + }); + + it("sets an interval for a task and sends a message to the background to set up a backup interval alarm", async () => { + foregroundTaskSchedulerService.setInterval( + ScheduledTaskNames.loginStrategySessionTimeout, + 1000, + ); + await flushPromises(); + + expect(globalThis.setInterval).toHaveBeenCalledWith(expect.any(Function), 1000); + expect(portMock.postMessage).toHaveBeenCalledWith({ + action: BrowserTaskSchedulerPortActions.setInterval, + taskName: ScheduledTaskNames.loginStrategySessionTimeout, + intervalInMs: 1000, + }); + }); +});