diff --git a/apps/browser/src/platform/services/browser-task-scheduler.service.spec.ts b/apps/browser/src/platform/services/browser-task-scheduler.service.spec.ts index 45a16474e5..7d6cc83070 100644 --- a/apps/browser/src/platform/services/browser-task-scheduler.service.spec.ts +++ b/apps/browser/src/platform/services/browser-task-scheduler.service.spec.ts @@ -6,6 +6,8 @@ import { ConsoleLogService } from "@bitwarden/common/platform/services/console-l import { GlobalState, StateProvider } from "@bitwarden/common/platform/state"; import { UserId } from "@bitwarden/common/types/guid"; +import { flushPromises } from "../../autofill/spec/testing-utils"; + import { ActiveAlarm, BrowserTaskSchedulerService, @@ -20,10 +22,29 @@ jest.mock("rxjs", () => { }; }); +function setupGlobalBrowserMock(overrides: Partial = {}) { + globalThis.browser.alarms = { + create: jest.fn(), + clear: jest.fn(), + get: jest.fn(), + getAll: jest.fn(), + clearAll: jest.fn(), + onAlarm: { + addListener: jest.fn(), + removeListener: jest.fn(), + hasListener: jest.fn(), + }, + ...overrides, + }; +} +const userUuid = "user-uuid" as UserId; +function getAlarmNameMock(taskName: string) { + return `${userUuid}__${taskName}`; +} + describe("BrowserTaskSchedulerService", () => { const callback = jest.fn(); const delayInMinutes = 2; - const userUuid = "user-uuid" as UserId; let activeUserIdMock$: BehaviorSubject; let activeAlarmsMock$: BehaviorSubject; let logService: MockProxy; @@ -71,12 +92,17 @@ describe("BrowserTaskSchedulerService", () => { ScheduledTaskNames.loginStrategySessionTimeout, callback, ); + // @ts-expect-error mocking global browser object + // eslint-disable-next-line no-global-assign + globalThis.browser = {}; }); afterEach(() => { jest.clearAllMocks(); jest.clearAllTimers(); jest.useRealTimers(); + // eslint-disable-next-line no-global-assign + globalThis.browser = undefined; }); describe("setTimeout", () => { @@ -87,7 +113,7 @@ describe("BrowserTaskSchedulerService", () => { ); expect(chrome.alarms.create).toHaveBeenCalledWith( - `${userUuid}__${ScheduledTaskNames.loginStrategySessionTimeout}`, + getAlarmNameMock(ScheduledTaskNames.loginStrategySessionTimeout), { delayInMinutes }, expect.any(Function), ); @@ -126,19 +152,100 @@ describe("BrowserTaskSchedulerService", () => { ); }); - it("uses the global setTimeout API if the delay is less than 1000ms", async () => { - const delayInMs = 15000; - jest.spyOn(globalThis, "setTimeout"); + it("creates an alarm that is not associated with a user", async () => { + activeUserIdMock$.next(undefined); + chrome.alarms.get = jest.fn().mockImplementation((_name, callback) => callback(undefined)); + + await browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMinutes * 60 * 1000, + ); + + expect(chrome.alarms.create).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, + { delayInMinutes }, + expect.any(Function), + ); + }); + + describe("when the task is scheduled to be triggered in less than 1 minute", () => { + const delayInMs = 45000; + + it("sets a timeout using the global setTimeout API", async () => { + jest.spyOn(globalThis, "setTimeout"); + + await browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs, + ); + + expect(globalThis.setTimeout).toHaveBeenCalledWith(expect.any(Function), delayInMs); + }); + + it("sets a fallback alarm", async () => { + const delayInMs = 15000; + await browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs, + ); + + expect(chrome.alarms.create).toHaveBeenCalledWith( + getAlarmNameMock(ScheduledTaskNames.loginStrategySessionTimeout), + { delayInMinutes: 0.5 }, + expect.any(Function), + ); + }); + + it("sets the fallback for a minimum of 1 minute if the environment not for Chrome", async () => { + setupGlobalBrowserMock(); + + await browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs, + ); + + expect(browser.alarms.create).toHaveBeenCalledWith( + getAlarmNameMock(ScheduledTaskNames.loginStrategySessionTimeout), + { delayInMinutes: 1 }, + ); + }); + + it("clears the fallback alarm when the setTimeout is triggered", async () => { + jest.useFakeTimers(); + + await browserTaskSchedulerService.setTimeout( + ScheduledTaskNames.loginStrategySessionTimeout, + delayInMs, + ); + jest.advanceTimersByTime(delayInMs); + await flushPromises(); + + expect(chrome.alarms.clear).toHaveBeenCalledWith( + getAlarmNameMock(ScheduledTaskNames.loginStrategySessionTimeout), + expect.any(Function), + ); + }); + }); + }); + + describe("triggering a task", () => { + it("clears an non user-based alarm if a separate user-based alarm has been set up", async () => { + jest.useFakeTimers(); + activeUserIdMock$.next(undefined); + const delayInMs = 10000; + chrome.alarms.get = jest + .fn() + .mockImplementation((_name, callback) => callback(mock())); await browserTaskSchedulerService.setTimeout( ScheduledTaskNames.loginStrategySessionTimeout, delayInMs, ); + jest.advanceTimersByTime(delayInMs); + await flushPromises(); - expect(globalThis.setTimeout).toHaveBeenCalledWith(expect.any(Function), delayInMs); - expect(chrome.alarms.create).toHaveBeenCalledWith( - `${userUuid}__${ScheduledTaskNames.loginStrategySessionTimeout}`, - { delayInMinutes: 0.5 }, + expect(chrome.alarms.clear).toHaveBeenCalledWith( + ScheduledTaskNames.loginStrategySessionTimeout, expect.any(Function), ); }); diff --git a/apps/browser/src/platform/services/browser-task-scheduler.service.ts b/apps/browser/src/platform/services/browser-task-scheduler.service.ts index 1105e70e33..b0f537051e 100644 --- a/apps/browser/src/platform/services/browser-task-scheduler.service.ts +++ b/apps/browser/src/platform/services/browser-task-scheduler.service.ts @@ -152,7 +152,7 @@ export class BrowserTaskSchedulerServiceImplementation createInfo.delayInMinutes && startTime + createInfo.delayInMinutes * 60 * 1000 < currentTime; if (shouldAlarmHaveBeenTriggered || hasSetTimeoutAlarmExceededDelay) { - await this.triggerRecoveredAlarm(alarmName); + await this.triggerTask(alarmName); continue; } @@ -247,16 +247,6 @@ export class BrowserTaskSchedulerServiceImplementation await this.activeAlarmsState.update(() => alarms); } - /** - * Triggers a recovered alarm by deleting it from the recovered alarms set - * - * @param alarmName - The name of the recovered alarm to trigger. - * @param periodInMinutes - The period in minutes of the recovered alarm. - */ - private async triggerRecoveredAlarm(alarmName: string, periodInMinutes?: number): Promise { - await this.triggerTask(alarmName, periodInMinutes); - } - /** * Sets up the on alarm listener to handle alarms. */ @@ -398,7 +388,9 @@ export class BrowserTaskSchedulerServiceImplementation /** * Checks if the environment is a non-Chrome environment. This is used to determine - * if the browser alarms API should be used in place of the chrome alarms API. + * if the browser alarms API should be used in place of the chrome alarms API. This + * is necessary because the `chrome` polyfill that Mozilla implements does not allow + * passing the callback parameter in the same way most `chrome.alarm` api calls allow. */ private isNonChromeEnvironment(): boolean { return typeof browser !== "undefined" && !!browser.alarms;