1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

[PM-8847] Delay browser local storage operations during reseed (#9536)

* Define matchers to test promise fulfillment

These are useful for validating that promises depend on other events prior to fulfilling.

* Expose custom matchers to jest projects

Team-specific projects are not touched here to try and reduce review burden.

* Block browser local operations awaiting reseed

This should closes a narrow race condition resulting from storage operations during a reseed event.

* Import from barrel file

This might fix the failing test, but I'm not sure _why_

* Document helper methods

* Validate as few properties as possible per test

* Simplify expected value representation

* Allow waiting in promise matchers

* Specify resolution times in promise orchestration tests.

* Test behavior triggering multiple reseeds.

* Fix typo

* Avoid testing implementation details

* Clear reseed on startup

in case a previous process was aborted in the middle of a reseed.

* Correct formatting
This commit is contained in:
Matt Gibson
2024-07-19 13:12:29 -07:00
committed by GitHub
parent 1320d96cb4
commit 5b5c165e10
11 changed files with 485 additions and 96 deletions

View File

@@ -1 +1,57 @@
import { toBeFulfilled, toBeResolved, toBeRejected } from "./promise-fulfilled";
import { toAlmostEqual } from "./to-almost-equal";
import { toEqualBuffer } from "./to-equal-buffer";
export * from "./to-equal-buffer";
export * from "./to-almost-equal";
export * from "./promise-fulfilled";
export function addCustomMatchers() {
expect.extend({
toEqualBuffer: toEqualBuffer,
toAlmostEqual: toAlmostEqual,
toBeFulfilled: toBeFulfilled,
toBeResolved: toBeResolved,
toBeRejected: toBeRejected,
});
}
export interface CustomMatchers<R = unknown> {
toEqualBuffer(expected: Uint8Array | ArrayBuffer): R;
/**
* Matches the expected date within an optional ms precision
* @param expected The expected date
* @param msPrecision The optional precision in milliseconds
*/
toAlmostEqual(expected: Date, msPrecision?: number): R;
/**
* Matches whether the received promise has been fulfilled.
*
* Failure if the promise is not currently fulfilled.
*
* @param received The promise to test
* @param withinMs The time within the promise should be fulfilled. Defaults to 0, indicating that the promise should already be fulfilled
* @returns CustomMatcherResult indicating whether or not the test passed
*/
toBeFulfilled(withinMs?: number): Promise<R>;
/**
* Matches whether the received promise has been resolved.
*
* Failure if the promise is not currently fulfilled or if it has been rejected.
*
* @param received The promise to test
* @param withinMs The time within the promise should be resolved. Defaults to 0, indicating that the promise should already be resolved
* @returns CustomMatcherResult indicating whether or not the test passed
*/
toBeResolved(withinMs?: number): Promise<R>;
/**
* Matches whether the received promise has been rejected.
*
* Failure if the promise is not currently fulfilled or if it has been resolved, but not rejected.
*
* @param received The promise to test
* @param withinMs The time within the promise should be rejected. Defaults to 0, indicating that the promise should already be rejected
* @returns CustomMatcherResult indicating whether or not the test passed
*/
toBeRejected(withinMs?: number): Promise<R>;
}

View File

@@ -0,0 +1,86 @@
describe("toBeFulfilled", () => {
it("passes when promise is resolved", async () => {
const promise = Promise.resolve("resolved");
await promise;
await expect(promise).toBeFulfilled();
});
it("passes when promise is rejected", async () => {
const promise = Promise.reject("rejected");
await promise.catch(() => {});
await expect(promise).toBeFulfilled();
});
it("fails when promise is pending", async () => {
const promise = new Promise((resolve) => setTimeout(resolve, 1000));
await expect(promise).not.toBeFulfilled();
});
it("passes when the promise is fulfilled within the given time limit", async () => {
const promise = new Promise((resolve) => setTimeout(resolve, 1));
await expect(promise).toBeFulfilled(2);
});
it("passes when the promise is not fulfilled within the given time limit", async () => {
const promise = new Promise((resolve) => setTimeout(resolve, 2));
await expect(promise).not.toBeFulfilled(1);
});
});
describe("toBeResolved", () => {
it("passes when promise is resolved", async () => {
const promise = Promise.resolve("resolved");
await promise;
await expect(promise).toBeResolved();
});
it("fails when promise is rejected", async () => {
const promise = Promise.reject("rejected");
await promise.catch(() => {});
await expect(promise).not.toBeResolved();
});
it("fails when promise is pending", async () => {
const promise = new Promise((resolve) => setTimeout(resolve, 1000));
await expect(promise).not.toBeResolved();
});
it("passes when the promise is resolved within the given time limit", async () => {
const promise = new Promise((resolve) => setTimeout(resolve, 1));
await expect(promise).toBeResolved(2);
});
it("passes when the promise is not resolved within the given time limit", async () => {
const promise = new Promise((resolve) => setTimeout(resolve, 2));
await expect(promise).not.toBeResolved(1);
});
});
describe("toBeRejected", () => {
it("fails when promise is resolved", async () => {
const promise = Promise.resolve("resolved");
await promise;
await expect(promise).not.toBeRejected();
});
it("passes when promise is rejected", async () => {
const promise = Promise.reject("rejected");
await promise.catch(() => {});
await expect(promise).toBeRejected();
});
it("fails when promise is pending", async () => {
const promise = new Promise((resolve) => setTimeout(resolve, 1000));
await expect(promise).not.toBeRejected();
});
it("passes when the promise is resolved within the given time limit", async () => {
const promise = new Promise((_, reject) => setTimeout(reject, 1));
await expect(promise).toBeFulfilled(2);
});
it("passes when the promise is not resolved within the given time limit", async () => {
const promise = new Promise((_, reject) => setTimeout(reject, 2));
await expect(promise).not.toBeFulfilled(1);
});
});

View File

@@ -0,0 +1,79 @@
async function wait(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Matches whether the received promise has been fulfilled.
*
* Failure if the promise is not currently fulfilled.
*
* @param received The promise to test
* @param withinMs The time within the promise should be fulfilled. Defaults to 0, indicating that the promise should already be fulfilled
* @returns CustomMatcherResult indicating whether or not the test passed
*/
export const toBeFulfilled: jest.CustomMatcher = async function (
received: Promise<unknown>,
withinMs = 0,
) {
return {
pass: await Promise.race([
wait(withinMs).then(() => false),
received.then(
() => true,
() => true,
),
]),
message: () => `expected promise to be fulfilled`,
};
};
/**
* Matches whether the received promise has been resolved.
*
* Failure if the promise is not currently fulfilled or if it has been rejected.
*
* @param received The promise to test
* @param withinMs The time within the promise should be resolved. Defaults to 0, indicating that the promise should already be resolved
* @returns CustomMatcherResult indicating whether or not the test passed
*/
export const toBeResolved: jest.CustomMatcher = async function (
received: Promise<unknown>,
withinMs = 0,
) {
return {
pass: await Promise.race([
wait(withinMs).then(() => false),
received.then(
() => true,
() => false,
),
]),
message: () => `expected promise to be resolved`,
};
};
/**
* Matches whether the received promise has been rejected.
*
* Failure if the promise is not currently fulfilled or if it has been resolved, but not rejected.
*
* @param received The promise to test
* @param withinMs The time within the promise should be rejected. Defaults to 0, indicating that the promise should already be rejected
* @returns CustomMatcherResult indicating whether or not the test passed
*/
export const toBeRejected: jest.CustomMatcher = async function (
received: Promise<unknown>,
withinMs = 0,
) {
return {
pass: await Promise.race([
wait(withinMs).then(() => false),
received.then(
() => false,
() => true,
),
]),
message: () => `expected promise to be rejected`,
};
};