1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-07-02 11:34:53 +02:00

[PM-6426] Removing user-based alarms and fixing an issue found with setting steppedd alarm interavals

This commit is contained in:
Cesar Gonzalez 2024-05-08 13:16:34 -05:00
parent 742db66210
commit 63b8556502
No known key found for this signature in database
GPG Key ID: 3381A5457F8CCECF
2 changed files with 36 additions and 141 deletions

View File

@ -4,7 +4,6 @@ import { BehaviorSubject, Observable } from "rxjs";
import { ScheduledTaskNames } from "@bitwarden/common/platform/enums/scheduled-task-name.enum";
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
import { GlobalState, StateProvider } from "@bitwarden/common/platform/state";
import { UserId } from "@bitwarden/common/types/guid";
import { flushPromises, triggerOnAlarmEvent } from "../../autofill/spec/testing-utils";
@ -37,15 +36,10 @@ function setupGlobalBrowserMock(overrides: Partial<chrome.alarms.Alarm> = {}) {
...overrides,
};
}
const userUuid = "user-uuid" as UserId;
function getAlarmNameMock(taskName: string) {
return `${taskName}__${userUuid}`;
}
describe("BrowserTaskSchedulerService", () => {
const callback = jest.fn();
const delayInMinutes = 2;
let activeUserIdMock$: BehaviorSubject<UserId>;
let activeAlarmsMock$: BehaviorSubject<ActiveAlarm[]>;
let logService: MockProxy<ConsoleLogService>;
let stateProvider: MockProxy<StateProvider>;
@ -73,14 +67,12 @@ describe("BrowserTaskSchedulerService", () => {
}),
];
activeAlarmsMock$ = new BehaviorSubject(activeAlarms);
activeUserIdMock$ = new BehaviorSubject(userUuid);
logService = mock<ConsoleLogService>();
globalStateMock = mock<GlobalState<any>>({
state$: mock<Observable<any>>(),
update: jest.fn((callback) => callback([], {} as any)),
});
stateProvider = mock<StateProvider>({
activeUserId$: activeUserIdMock$,
getGlobal: jest.fn(() => globalStateMock),
});
browserTaskSchedulerService = new BrowserTaskSchedulerServiceImplementation(
@ -126,7 +118,7 @@ describe("BrowserTaskSchedulerService", () => {
);
expect(chrome.alarms.create).toHaveBeenCalledWith(
getAlarmNameMock(ScheduledTaskNames.loginStrategySessionTimeout),
ScheduledTaskNames.loginStrategySessionTimeout,
{ delayInMinutes },
expect.any(Function),
);
@ -144,43 +136,6 @@ describe("BrowserTaskSchedulerService", () => {
expect(chrome.alarms.create).not.toHaveBeenCalled();
});
it("clears a scheduled alarm if a user-specific alarm for the same task is being registered", async () => {
const mockAlarm = mock<chrome.alarms.Alarm>({
name: ScheduledTaskNames.loginStrategySessionTimeout,
});
chrome.alarms.get = jest
.fn()
.mockImplementation((name, callback) =>
callback(name === ScheduledTaskNames.loginStrategySessionTimeout ? mockAlarm : undefined),
);
await browserTaskSchedulerService.setTimeout(
ScheduledTaskNames.loginStrategySessionTimeout,
delayInMinutes * 60 * 1000,
);
expect(chrome.alarms.clear).toHaveBeenCalledWith(
ScheduledTaskNames.loginStrategySessionTimeout,
expect.any(Function),
);
});
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;
@ -203,7 +158,7 @@ describe("BrowserTaskSchedulerService", () => {
);
expect(chrome.alarms.create).toHaveBeenCalledWith(
getAlarmNameMock(ScheduledTaskNames.loginStrategySessionTimeout),
ScheduledTaskNames.loginStrategySessionTimeout,
{ delayInMinutes: 0.5 },
expect.any(Function),
);
@ -218,7 +173,7 @@ describe("BrowserTaskSchedulerService", () => {
);
expect(browser.alarms.create).toHaveBeenCalledWith(
getAlarmNameMock(ScheduledTaskNames.loginStrategySessionTimeout),
ScheduledTaskNames.loginStrategySessionTimeout,
{ delayInMinutes: 1 },
);
});
@ -234,7 +189,7 @@ describe("BrowserTaskSchedulerService", () => {
await flushPromises();
expect(chrome.alarms.clear).toHaveBeenCalledWith(
getAlarmNameMock(ScheduledTaskNames.loginStrategySessionTimeout),
ScheduledTaskNames.loginStrategySessionTimeout,
expect.any(Function),
);
});
@ -263,18 +218,23 @@ describe("BrowserTaskSchedulerService", () => {
);
expect(chrome.alarms.create).toHaveBeenCalledWith(
`${getAlarmNameMock(ScheduledTaskNames.loginStrategySessionTimeout)}__0`,
{ periodInMinutes: 0.5 },
`${ScheduledTaskNames.loginStrategySessionTimeout}__0`,
{ periodInMinutes: 0.6666666666666666, delayInMinutes: 0.5 },
expect.any(Function),
);
expect(chrome.alarms.create).toHaveBeenCalledWith(
`${getAlarmNameMock(ScheduledTaskNames.loginStrategySessionTimeout)}__1`,
{ periodInMinutes: 0.6666666666666666 },
`${ScheduledTaskNames.loginStrategySessionTimeout}__1`,
{ periodInMinutes: 0.6666666666666666, delayInMinutes: 0.6666666666666666 },
expect.any(Function),
);
expect(chrome.alarms.create).toHaveBeenCalledWith(
`${getAlarmNameMock(ScheduledTaskNames.loginStrategySessionTimeout)}__2`,
{ periodInMinutes: 0.8333333333333333 },
`${ScheduledTaskNames.loginStrategySessionTimeout}__2`,
{ periodInMinutes: 0.6666666666666666, delayInMinutes: 0.8333333333333333 },
expect.any(Function),
);
expect(chrome.alarms.create).toHaveBeenCalledWith(
`${ScheduledTaskNames.loginStrategySessionTimeout}__3`,
{ periodInMinutes: 0.6666666666666666, delayInMinutes: 1 },
expect.any(Function),
);
});
@ -315,7 +275,7 @@ describe("BrowserTaskSchedulerService", () => {
);
expect(chrome.alarms.create).toHaveBeenCalledWith(
getAlarmNameMock(ScheduledTaskNames.loginStrategySessionTimeout),
ScheduledTaskNames.loginStrategySessionTimeout,
{ periodInMinutes, delayInMinutes: 0.5 },
expect.any(Function),
);
@ -329,7 +289,7 @@ describe("BrowserTaskSchedulerService", () => {
);
expect(chrome.alarms.create).toHaveBeenCalledWith(
getAlarmNameMock(ScheduledTaskNames.loginStrategySessionTimeout),
ScheduledTaskNames.loginStrategySessionTimeout,
{ periodInMinutes, delayInMinutes: periodInMinutes },
expect.any(Function),
);
@ -380,27 +340,6 @@ describe("BrowserTaskSchedulerService", () => {
});
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<chrome.alarms.Alarm>()));
await browserTaskSchedulerService.setTimeout(
ScheduledTaskNames.loginStrategySessionTimeout,
delayInMs,
);
jest.advanceTimersByTime(delayInMs);
await flushPromises();
expect(chrome.alarms.clear).toHaveBeenCalledWith(
ScheduledTaskNames.loginStrategySessionTimeout,
expect.any(Function),
);
});
it("triggers a task when an onAlarm event is triggered", () => {
const alarm = mock<chrome.alarms.Alarm>({
name: ScheduledTaskNames.loginStrategySessionTimeout,
@ -425,7 +364,7 @@ describe("BrowserTaskSchedulerService", () => {
});
expect(chrome.alarms.clear).toHaveBeenCalledWith(
getAlarmNameMock(ScheduledTaskNames.loginStrategySessionTimeout),
ScheduledTaskNames.loginStrategySessionTimeout,
expect.any(Function),
);
});
@ -438,7 +377,7 @@ describe("BrowserTaskSchedulerService", () => {
});
expect(browser.alarms.clear).toHaveBeenCalledWith(
getAlarmNameMock(ScheduledTaskNames.loginStrategySessionTimeout),
ScheduledTaskNames.loginStrategySessionTimeout,
);
});
});

View File

@ -58,8 +58,7 @@ export class BrowserTaskSchedulerServiceImplementation
this.validateRegisteredTask(taskName);
const delayInMinutes = delayInMs / 1000 / 60;
const alarmName = await this.getActiveUserAlarmName(taskName);
await this.scheduleAlarm(alarmName, {
await this.scheduleAlarm(taskName, {
delayInMinutes: this.getUpperBoundDelayInMinutes(delayInMinutes),
});
@ -67,8 +66,8 @@ export class BrowserTaskSchedulerServiceImplementation
// The alarm previously scheduled will be used as a backup in case the setTimeout fails.
if (delayInMinutes < 1) {
return globalThis.setTimeout(async () => {
await this.clearScheduledAlarm(alarmName);
await this.triggerTask(alarmName);
await this.clearScheduledAlarm(taskName);
await this.triggerTask(taskName);
}, delayInMs);
}
}
@ -90,16 +89,15 @@ export class BrowserTaskSchedulerServiceImplementation
this.validateRegisteredTask(taskName);
const intervalInMinutes = intervalInMs / 1000 / 60;
const alarmName = await this.getActiveUserAlarmName(taskName);
const initialDelayInMinutes = initialDelayInMs
? initialDelayInMs / 1000 / 60
: intervalInMinutes;
if (intervalInMinutes < 1) {
return this.setupSteppedIntervalAlarms(taskName, alarmName, intervalInMs);
return this.setupSteppedIntervalAlarms(taskName, intervalInMs);
}
await this.scheduleAlarm(alarmName, {
await this.scheduleAlarm(taskName, {
periodInMinutes: this.getUpperBoundDelayInMinutes(intervalInMinutes),
delayInMinutes: this.getUpperBoundDelayInMinutes(initialDelayInMinutes),
});
@ -112,28 +110,28 @@ export class BrowserTaskSchedulerServiceImplementation
* api does not support intervals less than 1 minute.
*
* @param taskName - The name of the task, separate of any user id.
* @param alarmName - The name of the alarm to create, could contain a user id.
* @param intervalInMs - The interval in milliseconds.
*/
private async setupSteppedIntervalAlarms(
taskName: ScheduledTaskName,
alarmName: string,
intervalInMs: number,
): Promise<number | NodeJS.Timeout> {
const alarmMinDelayInMinutes = this.getAlarmMinDelayInMinutes();
const intervalInMinutes = intervalInMs / 1000 / 60;
const numberOfAlarmsToCreate = Math.ceil(1 / intervalInMinutes);
const numberOfAlarmsToCreate = Math.ceil(Math.ceil(1 / intervalInMinutes) / 2) + 1;
const steppedAlarmPeriodInMinutes = alarmMinDelayInMinutes + intervalInMinutes;
for (let alarmIndex = 0; alarmIndex < numberOfAlarmsToCreate; alarmIndex++) {
const steppedAlarmName = `${alarmName}__${alarmIndex}`;
const periodInMinutes = alarmMinDelayInMinutes + intervalInMinutes * alarmIndex;
const steppedAlarmName = `${taskName}__${alarmIndex}`;
const delayInMinutes = this.getUpperBoundDelayInMinutes(
alarmMinDelayInMinutes + intervalInMinutes * alarmIndex,
);
// We need to clear alarms based on the task name as well as the
// user-based alarm name to ensure duplicate alarms are not created.
await this.clearScheduledAlarm(`${taskName}__${alarmIndex}`);
await this.clearScheduledAlarm(steppedAlarmName);
await this.scheduleAlarm(steppedAlarmName, {
periodInMinutes: this.getUpperBoundDelayInMinutes(periodInMinutes),
periodInMinutes: steppedAlarmPeriodInMinutes,
delayInMinutes,
});
}
@ -147,7 +145,7 @@ export class BrowserTaskSchedulerServiceImplementation
return;
}
await this.triggerTask(alarmName, intervalInMinutes);
await this.triggerTask(taskName, intervalInMinutes);
}, intervalInMs);
return intervalId;
@ -168,8 +166,7 @@ export class BrowserTaskSchedulerServiceImplementation
return;
}
const alarmName = await this.getActiveUserAlarmName(taskName);
await this.clearScheduledAlarm(alarmName);
await this.clearScheduledAlarm(taskName);
}
/**
@ -226,14 +223,6 @@ export class BrowserTaskSchedulerServiceImplementation
return;
}
// We should always prioritize user-based alarms over non-user-based alarms. If a non-user-based alarm
// exists when the user-based alarm is being created, we want to clear the non-user-based alarm.
const taskName = this.getTaskFromAlarmName(alarmName);
const existingTaskBasedAlarm = await this.getAlarm(taskName);
if (existingTaskBasedAlarm) {
await this.clearScheduledAlarm(taskName);
}
await this.createAlarm(alarmName, createInfo);
await this.setActiveAlarm(alarmName, createInfo);
}
@ -331,43 +320,10 @@ export class BrowserTaskSchedulerServiceImplementation
if (handler) {
handler();
}
// We should always prioritize user-based alarms over non-user-based alarms. As a result, if a triggered
// alarm is not user-based, we want to verify if the alarm should continue to exist. If an alarm exists
// for the same task that is user-based, we want to clear the non-user-based alarm.
if (alarmName === taskName) {
const taskName = this.getTaskFromAlarmName(alarmName);
const existingUserBasedAlarm = await this.getAlarm(taskName);
if (existingUserBasedAlarm) {
await this.clearScheduledAlarm(taskName);
}
}
}
/**
* Gets the active user id from state.
*/
private async getActiveUserId(): Promise<string> {
return await firstValueFrom(this.stateProvider.activeUserId$);
}
/**
* Gets the active user alarm name by appending the active user id to the task name.
*
* @param taskName - The task name to append the active user id to.
*/
private async getActiveUserAlarmName(taskName: ScheduledTaskName): Promise<string> {
const activeUserId = await this.getActiveUserId();
if (!activeUserId) {
return taskName;
}
return `${taskName}__${activeUserId}`;
}
/**
* Parses and returns the task name from an alarm name. If the alarm name
* contains a user id, it will return the task name without the user id.
* Parses and returns the task name from an alarm name.
*
* @param alarmName - The alarm name to parse.
*/