1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-09-13 01:58:44 +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(() => { afterEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
// eslint-disable-next-line
// @ts-ignore
globalThis.browser = {};
}); });
describe("isManifestVersion", () => { describe("isManifestVersion", () => {
@ -553,134 +550,4 @@ describe("BrowserApi", () => {
expect(callbackMock).toHaveBeenCalled(); 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 { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
import { GlobalState, StateProvider } from "@bitwarden/common/platform/state"; import { GlobalState, StateProvider } from "@bitwarden/common/platform/state";
import { BrowserApi } from "../browser/browser-api";
import { ActiveAlarm } from "./abstractions/browser-task-scheduler.service"; import { ActiveAlarm } from "./abstractions/browser-task-scheduler.service";
import { BrowserTaskSchedulerService } from "./browser-task-scheduler.service"; import { BrowserTaskSchedulerService } from "./browser-task-scheduler.service";
@ -18,6 +16,7 @@ jest.mock("rxjs", () => ({
Observable: jest.fn(), Observable: jest.fn(),
})); }));
// TODO CG - Likely need to rethink how to test this service a bit more carefully.
describe("BrowserTaskSchedulerService", () => { describe("BrowserTaskSchedulerService", () => {
let logService: MockProxy<ConsoleLogService>; let logService: MockProxy<ConsoleLogService>;
let stateProvider: MockProxy<StateProvider>; let stateProvider: MockProxy<StateProvider>;
@ -27,11 +26,6 @@ describe("BrowserTaskSchedulerService", () => {
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers(); jest.useFakeTimers();
jest.spyOn(BrowserApi, "getAlarm").mockImplementation((alarmName) => {
if (alarmName === ScheduledTaskNames.scheduleNextSyncInterval) {
return Promise.resolve(mock<chrome.alarms.Alarm>({ name: alarmName }));
}
});
activeAlarms = [ activeAlarms = [
mock<ActiveAlarm>({ mock<ActiveAlarm>({
name: ScheduledTaskNames.eventUploadsInterval, name: ScheduledTaskNames.eventUploadsInterval,
@ -57,12 +51,20 @@ describe("BrowserTaskSchedulerService", () => {
), ),
}); });
browserTaskSchedulerService = new BrowserTaskSchedulerService(logService, stateProvider); 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(() => { afterEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
jest.clearAllTimers(); jest.clearAllTimers();
jest.useRealTimers(); jest.useRealTimers();
// eslint-disable-next-line
// @ts-ignore
globalThis.browser = {};
}); });
describe("verifyAlarmsState", () => { describe("verifyAlarmsState", () => {
@ -162,12 +164,12 @@ describe("BrowserTaskSchedulerService", () => {
it("skips creating a duplicate timeout alarm", async () => { it("skips creating a duplicate timeout alarm", async () => {
const callback = jest.fn(); const callback = jest.fn();
const delayInMinutes = 2; const delayInMinutes = 2;
jest.spyOn(BrowserApi, "getAlarm").mockResolvedValue( jest.spyOn(browserTaskSchedulerService as any, "getAlarm").mockResolvedValue(
mock<chrome.alarms.Alarm>({ mock<chrome.alarms.Alarm>({
name: ScheduledTaskNames.loginStrategySessionTimeout, name: ScheduledTaskNames.loginStrategySessionTimeout,
}), }),
); );
jest.spyOn(BrowserApi, "createAlarm"); jest.spyOn(browserTaskSchedulerService, "createAlarm");
await browserTaskSchedulerService.setTimeout( await browserTaskSchedulerService.setTimeout(
callback, callback,
@ -175,7 +177,7 @@ describe("BrowserTaskSchedulerService", () => {
ScheduledTaskNames.loginStrategySessionTimeout, 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", () => { it("logs a warning if a duplicate handler is registered when creating an alarm", () => {
@ -295,12 +297,12 @@ describe("BrowserTaskSchedulerService", () => {
describe("clearAllScheduledTasks", () => { describe("clearAllScheduledTasks", () => {
it("clears all scheduled tasks and extension alarms", async () => { it("clears all scheduled tasks and extension alarms", async () => {
jest.spyOn(BrowserApi, "clearAllAlarms"); jest.spyOn(browserTaskSchedulerService, "clearAllAlarms");
jest.spyOn(browserTaskSchedulerService as any, "updateActiveAlarms"); jest.spyOn(browserTaskSchedulerService as any, "updateActiveAlarms");
await browserTaskSchedulerService.clearAllScheduledTasks(); await browserTaskSchedulerService.clearAllScheduledTasks();
expect(BrowserApi.clearAllAlarms).toHaveBeenCalled(); expect(browserTaskSchedulerService.clearAllAlarms).toHaveBeenCalled();
expect(browserTaskSchedulerService["updateActiveAlarms"]).toHaveBeenCalledWith([]); expect(browserTaskSchedulerService["updateActiveAlarms"]).toHaveBeenCalledWith([]);
expect(browserTaskSchedulerService["onAlarmHandlers"]).toEqual({}); expect(browserTaskSchedulerService["onAlarmHandlers"]).toEqual({});
expect(browserTaskSchedulerService["recoveredAlarms"].size).toBe(0); expect(browserTaskSchedulerService["recoveredAlarms"].size).toBe(0);
@ -318,4 +320,134 @@ describe("BrowserTaskSchedulerService", () => {
expect(callback).toHaveBeenCalled(); 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; 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; const initialDelayInMinutes = initialDelayInMs ? initialDelayInMs / 1000 / 60 : undefined;
await this.createAlarm(taskName, { await this.scheduleAlarm(taskName, {
periodInMinutes: intervalInMinutes, periodInMinutes: intervalInMinutes,
delayInMinutes: initialDelayInMinutes ?? intervalInMinutes, delayInMinutes: initialDelayInMinutes ?? intervalInMinutes,
}); });
@ -122,7 +122,7 @@ export class BrowserTaskSchedulerService
return; return;
} }
const wasCleared = await BrowserApi.clearAlarm(taskName); const wasCleared = await this.clearAlarm(taskName);
if (wasCleared) { if (wasCleared) {
await this.deleteActiveAlarm(taskName); await this.deleteActiveAlarm(taskName);
this.recoveredAlarms.delete(taskName); this.recoveredAlarms.delete(taskName);
@ -134,7 +134,7 @@ export class BrowserTaskSchedulerService
* alarms and resetting the active alarms state. * alarms and resetting the active alarms state.
*/ */
async clearAllScheduledTasks(): Promise<void> { async clearAllScheduledTasks(): Promise<void> {
await BrowserApi.clearAllAlarms(); await this.clearAllAlarms();
await this.updateActiveAlarms([]); await this.updateActiveAlarms([]);
this.onAlarmHandlers = {}; this.onAlarmHandlers = {};
this.recoveredAlarms.clear(); this.recoveredAlarms.clear();
@ -146,17 +146,17 @@ export class BrowserTaskSchedulerService
* @param name - The name of the alarm. * @param name - The name of the alarm.
* @param createInfo - The alarm create info. * @param createInfo - The alarm create info.
*/ */
private async createAlarm( private async scheduleAlarm(
name: ScheduledTaskName, name: ScheduledTaskName,
createInfo: chrome.alarms.AlarmCreateInfo, createInfo: chrome.alarms.AlarmCreateInfo,
): Promise<void> { ): Promise<void> {
const existingAlarm = await BrowserApi.getAlarm(name); const existingAlarm = await this.getAlarm(name);
if (existingAlarm) { if (existingAlarm) {
this.logService.warning(`Alarm ${name} already exists. Skipping creation.`); this.logService.warning(`Alarm ${name} already exists. Skipping creation.`);
return; return;
} }
await BrowserApi.createAlarm(name, createInfo); await this.createAlarm(name, createInfo);
await this.setActiveAlarm({ name, startTime: Date.now(), createInfo }); await this.setActiveAlarm({ name, startTime: Date.now(), createInfo });
} }
@ -184,7 +184,7 @@ export class BrowserTaskSchedulerService
for (const alarm of activeAlarms) { for (const alarm of activeAlarms) {
const { name, startTime, createInfo } = alarm; const { name, startTime, createInfo } = alarm;
const existingAlarm = await BrowserApi.getAlarm(name); const existingAlarm = await this.getAlarm(name);
if (existingAlarm) { if (existingAlarm) {
continue; continue;
} }
@ -199,7 +199,7 @@ export class BrowserTaskSchedulerService
continue; 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. // 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(); 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));
}
} }