1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-08-27 23:31:41 +02:00

[PM-6426] Moving alarms api calls out of BrowserApi and structuring them within the BrowserTaskSchedulerService

This commit is contained in:
Cesar Gonzalez 2024-04-17 11:59:10 -05:00
parent 0d9de68ba4
commit a45974fa6d
No known key found for this signature in database
GPG Key ID: 3381A5457F8CCECF
4 changed files with 217 additions and 218 deletions

View File

@ -7,9 +7,6 @@ describe("BrowserApi", () => {
afterEach(() => {
jest.clearAllMocks();
// eslint-disable-next-line
// @ts-ignore
globalThis.browser = {};
});
describe("isManifestVersion", () => {
@ -553,134 +550,4 @@ describe("BrowserApi", () => {
expect(callbackMock).toHaveBeenCalled();
});
});
describe("clearAlarm", () => {
it("uses the browser.alarms API if it is available", async () => {
const alarmName = "alarm-name";
globalThis.browser = {
// eslint-disable-next-line
// @ts-ignore
alarms: {
clear: jest.fn(),
},
};
await BrowserApi.clearAlarm(alarmName);
expect(browser.alarms.clear).toHaveBeenCalledWith(alarmName);
});
it("clears the alarm with the provided name", async () => {
const alarmName = "alarm-name";
const wasCleared = await BrowserApi.clearAlarm(alarmName);
expect(chrome.alarms.clear).toHaveBeenCalledWith(alarmName, expect.any(Function));
expect(wasCleared).toBe(true);
});
});
describe("clearAllAlarms", () => {
it("uses the browser.alarms API if it is available", async () => {
globalThis.browser = {
// eslint-disable-next-line
// @ts-ignore
alarms: {
clearAll: jest.fn(),
},
};
await BrowserApi.clearAllAlarms();
expect(browser.alarms.clearAll).toHaveBeenCalled();
});
it("clears all alarms", async () => {
const wasCleared = await BrowserApi.clearAllAlarms();
expect(chrome.alarms.clearAll).toHaveBeenCalledWith(expect.any(Function));
expect(wasCleared).toBe(true);
});
});
describe("createAlarm", () => {
it("uses the browser.alarms API if it is available", async () => {
const alarmName = "alarm-name";
const alarmInfo = { when: 1000 };
globalThis.browser = {
// eslint-disable-next-line
// @ts-ignore
alarms: {
create: jest.fn(),
},
};
await BrowserApi.createAlarm(alarmName, alarmInfo);
expect(browser.alarms.create).toHaveBeenCalledWith(alarmName, alarmInfo);
});
it("creates an alarm", async () => {
const alarmName = "alarm-name";
const alarmInfo = { when: 1000 };
await BrowserApi.createAlarm(alarmName, alarmInfo);
expect(chrome.alarms.create).toHaveBeenCalledWith(alarmName, alarmInfo, expect.any(Function));
});
});
describe("getAlarm", () => {
it("uses the browser.alarms API if it is available", async () => {
const alarmName = "alarm-name";
globalThis.browser = {
// eslint-disable-next-line
// @ts-ignore
alarms: {
get: jest.fn(),
},
};
await BrowserApi.getAlarm(alarmName);
expect(browser.alarms.get).toHaveBeenCalledWith(alarmName);
});
it("gets the alarm by name", async () => {
const alarmName = "alarm-name";
const alarmMock = mock<chrome.alarms.Alarm>();
chrome.alarms.get = jest.fn().mockImplementation((_name, callback) => callback(alarmMock));
const receivedAlarm = await BrowserApi.getAlarm(alarmName);
expect(chrome.alarms.get).toHaveBeenCalledWith(alarmName, expect.any(Function));
expect(receivedAlarm).toBe(alarmMock);
});
});
describe("getAllAlarms", () => {
it("uses the browser.alarms API if it is available", async () => {
globalThis.browser = {
// eslint-disable-next-line
// @ts-ignore
alarms: {
getAll: jest.fn(),
},
};
await BrowserApi.getAllAlarms();
expect(browser.alarms.getAll).toHaveBeenCalled();
});
it("gets all registered alarms", async () => {
const alarms = [mock<chrome.alarms.Alarm>(), mock<chrome.alarms.Alarm>()];
chrome.alarms.getAll = jest.fn().mockImplementation((callback) => callback(alarms));
const receivedAlarms = await BrowserApi.getAllAlarms();
expect(chrome.alarms.getAll).toHaveBeenCalledWith(expect.any(Function));
expect(receivedAlarms).toBe(alarms);
});
});
});

View File

@ -591,68 +591,4 @@ export class BrowserApi {
}
});
}
/**
* Clears a new alarm with the given name and create info. Returns a promise
* that indicates when the alarm has been cleared successfully.
*
* @param alarmName - The name of the alarm to create.
*/
static clearAlarm(alarmName: string): Promise<boolean> {
if (typeof browser !== "undefined" && browser.alarms) {
return browser.alarms.clear(alarmName);
}
return new Promise((resolve) => chrome.alarms.clear(alarmName, resolve));
}
/**
* Clears all alarms that have been set by the extension. Returns a promise
* that indicates when all alarms have been cleared successfully.
*/
static clearAllAlarms(): Promise<boolean> {
if (typeof browser !== "undefined" && browser.alarms) {
return browser.alarms.clearAll();
}
return new Promise((resolve) => chrome.alarms.clearAll(resolve));
}
/**
* Creates a new alarm with the given name and create info.
*
* @param name - The name of the alarm to create.
* @param createInfo - The creation info for the alarm.
*/
static async createAlarm(name: string, createInfo: chrome.alarms.AlarmCreateInfo): Promise<void> {
if (typeof browser !== "undefined" && browser.alarms) {
return browser.alarms.create(name, createInfo);
}
return new Promise((resolve) => chrome.alarms.create(name, createInfo, resolve));
}
/**
* Gets the alarm with the given name.
*
* @param alarmName - The name of the alarm to get.
*/
static getAlarm(alarmName: string): Promise<chrome.alarms.Alarm> {
if (typeof browser !== "undefined" && browser.alarms) {
return browser.alarms.get(alarmName);
}
return new Promise((resolve) => chrome.alarms.get(alarmName, resolve));
}
/**
* Gets all alarms that have been set by the extension.
*/
static getAllAlarms(): Promise<chrome.alarms.Alarm[]> {
if (typeof browser !== "undefined" && browser.alarms) {
return browser.alarms.getAll();
}
return new Promise((resolve) => chrome.alarms.getAll(resolve));
}
}

View File

@ -6,8 +6,6 @@ import { ScheduledTaskNames } from "@bitwarden/common/platform/enums/scheduled-t
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
import { GlobalState, StateProvider } from "@bitwarden/common/platform/state";
import { BrowserApi } from "../browser/browser-api";
import { ActiveAlarm } from "./abstractions/browser-task-scheduler.service";
import { BrowserTaskSchedulerService } from "./browser-task-scheduler.service";
@ -18,6 +16,7 @@ jest.mock("rxjs", () => ({
Observable: jest.fn(),
}));
// TODO CG - Likely need to rethink how to test this service a bit more carefully.
describe("BrowserTaskSchedulerService", () => {
let logService: MockProxy<ConsoleLogService>;
let stateProvider: MockProxy<StateProvider>;
@ -27,11 +26,6 @@ describe("BrowserTaskSchedulerService", () => {
beforeEach(() => {
jest.useFakeTimers();
jest.spyOn(BrowserApi, "getAlarm").mockImplementation((alarmName) => {
if (alarmName === ScheduledTaskNames.scheduleNextSyncInterval) {
return Promise.resolve(mock<chrome.alarms.Alarm>({ name: alarmName }));
}
});
activeAlarms = [
mock<ActiveAlarm>({
name: ScheduledTaskNames.eventUploadsInterval,
@ -57,12 +51,20 @@ describe("BrowserTaskSchedulerService", () => {
),
});
browserTaskSchedulerService = new BrowserTaskSchedulerService(logService, stateProvider);
jest.spyOn(browserTaskSchedulerService as any, "getAlarm").mockImplementation((alarmName) => {
if (alarmName === ScheduledTaskNames.scheduleNextSyncInterval) {
return Promise.resolve(mock<chrome.alarms.Alarm>({ name: alarmName }));
}
});
});
afterEach(() => {
jest.clearAllMocks();
jest.clearAllTimers();
jest.useRealTimers();
// eslint-disable-next-line
// @ts-ignore
globalThis.browser = {};
});
describe("verifyAlarmsState", () => {
@ -162,12 +164,12 @@ describe("BrowserTaskSchedulerService", () => {
it("skips creating a duplicate timeout alarm", async () => {
const callback = jest.fn();
const delayInMinutes = 2;
jest.spyOn(BrowserApi, "getAlarm").mockResolvedValue(
jest.spyOn(browserTaskSchedulerService as any, "getAlarm").mockResolvedValue(
mock<chrome.alarms.Alarm>({
name: ScheduledTaskNames.loginStrategySessionTimeout,
}),
);
jest.spyOn(BrowserApi, "createAlarm");
jest.spyOn(browserTaskSchedulerService, "createAlarm");
await browserTaskSchedulerService.setTimeout(
callback,
@ -175,7 +177,7 @@ describe("BrowserTaskSchedulerService", () => {
ScheduledTaskNames.loginStrategySessionTimeout,
);
expect(BrowserApi.createAlarm).not.toHaveBeenCalled();
expect(browserTaskSchedulerService.createAlarm).not.toHaveBeenCalled();
});
it("logs a warning if a duplicate handler is registered when creating an alarm", () => {
@ -295,12 +297,12 @@ describe("BrowserTaskSchedulerService", () => {
describe("clearAllScheduledTasks", () => {
it("clears all scheduled tasks and extension alarms", async () => {
jest.spyOn(BrowserApi, "clearAllAlarms");
jest.spyOn(browserTaskSchedulerService, "clearAllAlarms");
jest.spyOn(browserTaskSchedulerService as any, "updateActiveAlarms");
await browserTaskSchedulerService.clearAllScheduledTasks();
expect(BrowserApi.clearAllAlarms).toHaveBeenCalled();
expect(browserTaskSchedulerService.clearAllAlarms).toHaveBeenCalled();
expect(browserTaskSchedulerService["updateActiveAlarms"]).toHaveBeenCalledWith([]);
expect(browserTaskSchedulerService["onAlarmHandlers"]).toEqual({});
expect(browserTaskSchedulerService["recoveredAlarms"].size).toBe(0);
@ -318,4 +320,134 @@ describe("BrowserTaskSchedulerService", () => {
expect(callback).toHaveBeenCalled();
});
});
describe("clearAlarm", () => {
it("uses the browser.alarms API if it is available", async () => {
const alarmName = "alarm-name";
globalThis.browser = {
// eslint-disable-next-line
// @ts-ignore
alarms: {
clear: jest.fn(),
},
};
await browserTaskSchedulerService.clearAlarm(alarmName);
expect(browser.alarms.clear).toHaveBeenCalledWith(alarmName);
});
it("clears the alarm with the provided name", async () => {
const alarmName = "alarm-name";
const wasCleared = await browserTaskSchedulerService.clearAlarm(alarmName);
expect(chrome.alarms.clear).toHaveBeenCalledWith(alarmName, expect.any(Function));
expect(wasCleared).toBe(true);
});
});
describe("clearAllAlarms", () => {
it("uses the browser.alarms API if it is available", async () => {
globalThis.browser = {
// eslint-disable-next-line
// @ts-ignore
alarms: {
clearAll: jest.fn(),
},
};
await browserTaskSchedulerService.clearAllAlarms();
expect(browser.alarms.clearAll).toHaveBeenCalled();
});
it("clears all alarms", async () => {
const wasCleared = await browserTaskSchedulerService.clearAllAlarms();
expect(chrome.alarms.clearAll).toHaveBeenCalledWith(expect.any(Function));
expect(wasCleared).toBe(true);
});
});
describe("createAlarm", () => {
it("uses the browser.alarms API if it is available", async () => {
const alarmName = "alarm-name";
const alarmInfo = { when: 1000 };
globalThis.browser = {
// eslint-disable-next-line
// @ts-ignore
alarms: {
create: jest.fn(),
},
};
await browserTaskSchedulerService.createAlarm(alarmName, alarmInfo);
expect(browser.alarms.create).toHaveBeenCalledWith(alarmName, alarmInfo);
});
it("creates an alarm", async () => {
const alarmName = "alarm-name";
const alarmInfo = { when: 1000 };
await browserTaskSchedulerService.createAlarm(alarmName, alarmInfo);
expect(chrome.alarms.create).toHaveBeenCalledWith(alarmName, alarmInfo, expect.any(Function));
});
});
describe.skip("getAlarm", () => {
it("uses the browser.alarms API if it is available", async () => {
const alarmName = "alarm-name";
globalThis.browser = {
// eslint-disable-next-line
// @ts-ignore
alarms: {
get: jest.fn(),
},
};
await browserTaskSchedulerService.getAlarm(alarmName);
expect(browser.alarms.get).toHaveBeenCalledWith(alarmName);
});
it("gets the alarm by name", async () => {
const alarmName = "alarm-name";
const alarmMock = mock<chrome.alarms.Alarm>();
chrome.alarms.get = jest.fn().mockImplementation((_name, callback) => callback(alarmMock));
const receivedAlarm = await browserTaskSchedulerService.getAlarm(alarmName);
expect(chrome.alarms.get).toHaveBeenCalledWith(alarmName, expect.any(Function));
expect(receivedAlarm).toBe(alarmMock);
});
});
describe("getAllAlarms", () => {
it("uses the browser.alarms API if it is available", async () => {
globalThis.browser = {
// eslint-disable-next-line
// @ts-ignore
alarms: {
getAll: jest.fn(),
},
};
await browserTaskSchedulerService.getAllAlarms();
expect(browser.alarms.getAll).toHaveBeenCalled();
});
it("gets all registered alarms", async () => {
const alarms = [mock<chrome.alarms.Alarm>(), mock<chrome.alarms.Alarm>()];
chrome.alarms.getAll = jest.fn().mockImplementation((callback) => callback(alarms));
const receivedAlarms = await browserTaskSchedulerService.getAllAlarms();
expect(chrome.alarms.getAll).toHaveBeenCalledWith(expect.any(Function));
expect(receivedAlarms).toBe(alarms);
});
});
});

View File

@ -71,7 +71,7 @@ export class BrowserTaskSchedulerService
return;
}
await this.createAlarm(taskName, { delayInMinutes });
await this.scheduleAlarm(taskName, { delayInMinutes });
}
/**
@ -101,7 +101,7 @@ export class BrowserTaskSchedulerService
}
const initialDelayInMinutes = initialDelayInMs ? initialDelayInMs / 1000 / 60 : undefined;
await this.createAlarm(taskName, {
await this.scheduleAlarm(taskName, {
periodInMinutes: intervalInMinutes,
delayInMinutes: initialDelayInMinutes ?? intervalInMinutes,
});
@ -122,7 +122,7 @@ export class BrowserTaskSchedulerService
return;
}
const wasCleared = await BrowserApi.clearAlarm(taskName);
const wasCleared = await this.clearAlarm(taskName);
if (wasCleared) {
await this.deleteActiveAlarm(taskName);
this.recoveredAlarms.delete(taskName);
@ -134,7 +134,7 @@ export class BrowserTaskSchedulerService
* alarms and resetting the active alarms state.
*/
async clearAllScheduledTasks(): Promise<void> {
await BrowserApi.clearAllAlarms();
await this.clearAllAlarms();
await this.updateActiveAlarms([]);
this.onAlarmHandlers = {};
this.recoveredAlarms.clear();
@ -146,17 +146,17 @@ export class BrowserTaskSchedulerService
* @param name - The name of the alarm.
* @param createInfo - The alarm create info.
*/
private async createAlarm(
private async scheduleAlarm(
name: ScheduledTaskName,
createInfo: chrome.alarms.AlarmCreateInfo,
): Promise<void> {
const existingAlarm = await BrowserApi.getAlarm(name);
const existingAlarm = await this.getAlarm(name);
if (existingAlarm) {
this.logService.warning(`Alarm ${name} already exists. Skipping creation.`);
return;
}
await BrowserApi.createAlarm(name, createInfo);
await this.createAlarm(name, createInfo);
await this.setActiveAlarm({ name, startTime: Date.now(), createInfo });
}
@ -184,7 +184,7 @@ export class BrowserTaskSchedulerService
for (const alarm of activeAlarms) {
const { name, startTime, createInfo } = alarm;
const existingAlarm = await BrowserApi.getAlarm(name);
const existingAlarm = await this.getAlarm(name);
if (existingAlarm) {
continue;
}
@ -199,7 +199,7 @@ export class BrowserTaskSchedulerService
continue;
}
void this.createAlarm(name, createInfo);
void this.scheduleAlarm(name, createInfo);
}
// 10 seconds after verifying the alarm state, we should treat any newly created alarms as non-recovered alarms.
@ -286,4 +286,68 @@ export class BrowserTaskSchedulerService
handler();
}
}
/**
* Clears a new alarm with the given name and create info. Returns a promise
* that indicates when the alarm has been cleared successfully.
*
* @param alarmName - The name of the alarm to create.
*/
clearAlarm(alarmName: string): Promise<boolean> {
if (typeof browser !== "undefined" && browser.alarms) {
return browser.alarms.clear(alarmName);
}
return new Promise((resolve) => chrome.alarms.clear(alarmName, resolve));
}
/**
* Clears all alarms that have been set by the extension. Returns a promise
* that indicates when all alarms have been cleared successfully.
*/
clearAllAlarms(): Promise<boolean> {
if (typeof browser !== "undefined" && browser.alarms) {
return browser.alarms.clearAll();
}
return new Promise((resolve) => chrome.alarms.clearAll(resolve));
}
/**
* Creates a new alarm with the given name and create info.
*
* @param name - The name of the alarm to create.
* @param createInfo - The creation info for the alarm.
*/
async createAlarm(name: string, createInfo: chrome.alarms.AlarmCreateInfo): Promise<void> {
if (typeof browser !== "undefined" && browser.alarms) {
return browser.alarms.create(name, createInfo);
}
return new Promise((resolve) => chrome.alarms.create(name, createInfo, resolve));
}
/**
* Gets the alarm with the given name.
*
* @param alarmName - The name of the alarm to get.
*/
getAlarm(alarmName: string): Promise<chrome.alarms.Alarm> {
if (typeof browser !== "undefined" && browser.alarms) {
return browser.alarms.get(alarmName);
}
return new Promise((resolve) => chrome.alarms.get(alarmName, resolve));
}
/**
* Gets all alarms that have been set by the extension.
*/
getAllAlarms(): Promise<chrome.alarms.Alarm[]> {
if (typeof browser !== "undefined" && browser.alarms) {
return browser.alarms.getAll();
}
return new Promise((resolve) => chrome.alarms.getAll(resolve));
}
}