Skip to content

Commit 3f72b49

Browse files
committed
🐛 fix(flaky-test): isolate test state
1 parent a0f971a commit 3f72b49

30 files changed

+608
-417
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { faker } from "@faker-js/faker/.";
2+
import { Subscriber } from "@core/types/email/email.types";
3+
import dayjs from "@core/util/date/dayjs";
4+
import EmailService from "@backend/email/email.service";
5+
import {
6+
Response_TagSubscriber,
7+
Response_UpsertSubscriber,
8+
} from "@backend/email/email.types";
9+
10+
export class EmailDriver {
11+
static mockEmailServiceResponse() {
12+
const id = faker.number.int();
13+
const created_at = dayjs().toISOString();
14+
const tagged_at = dayjs().toISOString();
15+
16+
// Mock emailer API calls
17+
const upsertSubscriber = jest
18+
.spyOn(EmailService, "upsertSubscriber")
19+
.mockImplementation((subscriber) =>
20+
EmailDriver.upsertSubscriber({ ...subscriber, id, created_at }),
21+
);
22+
23+
const addTagToSubscriber = jest
24+
.spyOn(EmailService, "addTagToSubscriber")
25+
.mockImplementation((subscriber, tagId) =>
26+
EmailDriver.addTagToSubscriber(
27+
{ ...subscriber, id, created_at, tagged_at },
28+
tagId,
29+
),
30+
);
31+
32+
return { upsertSubscriber, addTagToSubscriber };
33+
}
34+
35+
private static async addTagToSubscriber(
36+
subscriber: Subscriber & {
37+
id: number;
38+
created_at: string;
39+
tagged_at: string;
40+
},
41+
tagId: string,
42+
): Promise<Response_TagSubscriber> {
43+
return Promise.resolve({
44+
subscriber: {
45+
tagId,
46+
...subscriber,
47+
tagged_at: new Date().toISOString(),
48+
first_name: subscriber.first_name!,
49+
},
50+
});
51+
}
52+
53+
private static async upsertSubscriber(
54+
subscriber: Subscriber & { id: number; created_at: string },
55+
): Promise<Response_UpsertSubscriber> {
56+
return Promise.resolve({
57+
subscriber: {
58+
...subscriber,
59+
first_name: subscriber.first_name!,
60+
},
61+
});
62+
}
63+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { WithId } from "mongodb";
2+
import { Schema_User } from "@core/types/user.types";
3+
import { SyncDriver } from "@backend/__tests__/drivers/sync.driver";
4+
import { UserDriver } from "@backend/__tests__/drivers/user.driver";
5+
import { WaitListDriver } from "@backend/__tests__/drivers/waitlist.driver";
6+
7+
export class UtilDriver {
8+
static async setupTestUser(): Promise<{ user: WithId<Schema_User> }> {
9+
const user = await UserDriver.createUser();
10+
11+
await Promise.all([
12+
SyncDriver.createSync(user, true),
13+
WaitListDriver.saveWaitListRecord(
14+
WaitListDriver.createWaitListRecord(user),
15+
),
16+
]);
17+
18+
return { user };
19+
}
20+
}

packages/backend/src/__tests__/drivers/waitlist.driver.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import { Schema_Waitlist } from "@core/types/waitlist/waitlist.types";
44
import mongoService from "@backend/common/services/mongo.service";
55

66
export class WaitListDriver {
7-
static async createWaitListRecord(
7+
static createWaitListRecord(
88
user: Pick<WithId<Schema_User>, "email" | "firstName" | "lastName">,
9-
): Promise<WithId<Schema_Waitlist>> {
10-
const waitListRecord: Schema_Waitlist = {
9+
): Schema_Waitlist {
10+
return {
1111
email: user.email,
1212
schemaVersion: "0",
1313
source: "other",
@@ -20,7 +20,11 @@ export class WaitListDriver {
2020
status: "waitlisted",
2121
waitlistedAt: new Date().toISOString(),
2222
};
23+
}
2324

25+
static async saveWaitListRecord(
26+
waitListRecord: Schema_Waitlist,
27+
): Promise<WithId<Schema_Waitlist>> {
2428
const created = await mongoService.waitlist.insertOne(waitListRecord);
2529

2630
return { _id: created.insertedId, ...waitListRecord };
Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
import { Event_Core } from "@core/types/event.types";
1+
import { Filter } from "mongodb";
2+
import { Event_Core, Schema_Event } from "@core/types/event.types";
23
import { Schema_Waitlist } from "@core/types/waitlist/waitlist.types";
34
import { Collections } from "@backend/common/constants/collections";
45
import mongoService from "@backend/common/services/mongo.service";
56
import { Event_API } from "@backend/common/types/backend.event.types";
67

7-
export const getCategorizedEventsInDb = async () => {
8-
const allEvents = (await getEventsInDb()) as unknown as Event_Core[];
8+
export const getCategorizedEventsInDb = async (
9+
filter?: Filter<Omit<Schema_Event, "_id">>,
10+
) => {
11+
const allEvents = (await getEventsInDb(filter)) as unknown as Event_Core[];
912
const baseEvents = allEvents.filter((e) => e.recurrence?.rule !== undefined);
1013
const instanceEvents = allEvents.filter(
1114
(e) => e.recurrence?.eventId !== undefined,
@@ -14,10 +17,11 @@ export const getCategorizedEventsInDb = async () => {
1417
return { baseEvents, instanceEvents, regularEvents };
1518
};
1619

17-
export const getEventsInDb = async () => {
18-
return (await mongoService.db
19-
.collection(Collections.EVENT)
20-
.find()
20+
export const getEventsInDb = async (
21+
filter: Filter<Omit<Schema_Event, "_id">> = {},
22+
) => {
23+
return (await mongoService.event
24+
.find(filter)
2125
.toArray()) as unknown as Event_API[];
2226
};
2327

@@ -35,9 +39,8 @@ export const isEmailOnWaitlist = async (email: string) => {
3539
return (await getEmailsOnWaitlist()).includes(email);
3640
};
3741

38-
export const isEventCollectionEmpty = async () => {
39-
return (
40-
(await mongoService.db.collection(Collections.EVENT).find().toArray())
41-
.length === 0
42-
);
42+
export const isEventCollectionEmpty = async (
43+
filter: Filter<Omit<Schema_Event, "_id">> = {},
44+
) => {
45+
return (await mongoService.event.find(filter).toArray()).length === 0;
4346
};

packages/backend/src/__tests__/helpers/mock.db.setup.ts

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,12 @@
1-
import { Db } from "mongodb";
2-
import { SyncDriver } from "@backend/__tests__/drivers/sync.driver";
3-
import { UserDriver } from "@backend/__tests__/drivers/user.driver";
4-
import { WaitListDriver } from "@backend/__tests__/drivers/waitlist.driver";
51
import { Collections } from "@backend/common/constants/collections";
62
import mongoService from "@backend/common/services/mongo.service";
73

8-
export interface TestSetup {
9-
db: Db;
10-
userId: string;
11-
email: string;
12-
}
13-
144
/**
15-
* Setup a test database with a test user and a
16-
* sync record that points to the test user
17-
* @returns {Promise<TestSetup>} - The test setup object
5+
* Setup a test database
186
*/
19-
export async function setupTestDb(): Promise<TestSetup> {
7+
export async function setupTestDb(): Promise<void> {
208
try {
21-
await mongoService.start();
22-
23-
const user = await UserDriver.createUser();
24-
25-
await Promise.all([
26-
SyncDriver.createSync(user, true),
27-
WaitListDriver.createWaitListRecord(user),
28-
]);
29-
30-
return {
31-
db: mongoService.db,
32-
userId: user._id.toString(),
33-
email: user.email,
34-
};
9+
await mongoService.start(true);
3510
} catch (err) {
3611
const error = err as Error;
3712

packages/backend/src/__tests__/helpers/mock.events.init.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Db, ObjectId, WithoutId } from "mongodb";
1+
import { ObjectId, WithoutId } from "mongodb";
22
import { Origin } from "@core/constants/core.constants";
33
import { MapEvent } from "@core/mappers/map.event";
44
import { Schema_Event, WithCompassId } from "@core/types/event.types";
@@ -9,6 +9,7 @@ import {
99
} from "@core/types/gcal";
1010
import { isBase } from "@core/util/event/event.util";
1111
import { Collections } from "@backend/common/constants/collections";
12+
import mongoService from "../../common/services/mongo.service";
1213
import { mockGcalEvents } from "../mocks.gcal/mocks.gcal/factories/gcal.event.factory";
1314

1415
export interface State_AfterGcalImport {
@@ -29,15 +30,14 @@ export interface State_AfterGcalImport {
2930
* @returns {Object} - The gcal and compass events
3031
*/
3132
export const simulateDbAfterGcalImport = async (
32-
db: Db,
3333
userId: string,
3434
): Promise<State_AfterGcalImport> => {
3535
const { gcalEvents, compassEvents } = mockGcalAndCompassEvents(userId);
36-
await db
36+
await mongoService.db
3737
.collection(Collections.EVENT)
3838
.insertMany(compassEvents as unknown as WithoutId<Schema_Event>[]);
3939

40-
const compassEventsInDb = (await db
40+
const compassEventsInDb = (await mongoService.db
4141
.collection(Collections.EVENT)
4242
.find({})
4343
.toArray()) as unknown as WithCompassId<Schema_Event>[];

packages/backend/src/__tests__/helpers/mock.setup.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Request, Response } from "express";
1+
import { Handler, Request, Response } from "express";
22
import { GoogleApis } from "googleapis";
33
import { randomUUID } from "node:crypto";
44
import type SuperTokens from "supertokens-node";
@@ -10,6 +10,7 @@ import { SessionContainerInterface } from "supertokens-node/lib/build/recipe/ses
1010
import { UserMetadata } from "@core/types/user.types";
1111
import { mockAndCategorizeGcalEvents } from "@backend/__tests__/mocks.gcal/factories/gcal.event.batch";
1212
import { mockGcal } from "@backend/__tests__/mocks.gcal/factories/gcal.factory";
13+
import { ENV } from "@backend/common/constants/env.constants";
1314
import { SupertokensAccessTokenPayload } from "@backend/common/types/supertokens.types";
1415

1516
function mockGoogleapis() {
@@ -140,6 +141,44 @@ function mockSuperToken() {
140141
});
141142
}
142143

144+
function mockWinstonLogger() {
145+
jest.mock("@core/logger/winston.logger", () => {
146+
const mockLogger = {
147+
debug: jest.fn(),
148+
info: jest.fn(),
149+
warn: jest.fn(),
150+
error: jest.fn(),
151+
verbose: jest.fn(),
152+
};
153+
154+
return {
155+
Logger: jest.fn().mockImplementation(() => mockLogger),
156+
};
157+
});
158+
}
159+
160+
function mockHttpLoggingMiddleware() {
161+
mockModule("@backend/common/middleware/http.logger.middleware", () => ({
162+
httpLoggingMiddleware: jest.fn<void, Parameters<Handler>>((...args) =>
163+
args[2](),
164+
),
165+
}));
166+
}
167+
168+
export function mockEnv(env: Partial<typeof ENV>) {
169+
const entries = Object.entries(env) as Array<
170+
[keyof typeof env, (typeof env)[keyof typeof env]]
171+
>;
172+
173+
return entries.reduce(
174+
(newEnv, [key, value]) => ({
175+
...newEnv,
176+
[key]: jest.replaceProperty(ENV, key, value),
177+
}),
178+
{} as Record<keyof typeof env, jest.ReplaceProperty<keyof typeof env>>,
179+
);
180+
}
181+
143182
export function mockModule(
144183
mockPath: string,
145184
mockFactory?: (...args: unknown[]) => object,
@@ -157,6 +196,8 @@ export function mockModule(
157196
}
158197

159198
export function mockNodeModules() {
199+
mockWinstonLogger();
200+
mockHttpLoggingMiddleware();
160201
mockGoogleapis();
161202
mockSuperToken();
162203
}

packages/backend/src/__tests__/mocks.db/ccal.mock.db.util.ts

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,9 @@ import {
88
createMockBaseEvent,
99
createMockInstance,
1010
} from "@core/util/test/ccal.event.factory";
11-
import { Collections } from "@backend/common/constants/collections";
12-
import { TestSetup } from "../helpers/mock.db.setup";
11+
import mongoService from "@backend/common/services/mongo.service";
1312

1413
export const createRecurrenceSeries = async (
15-
setup: TestSetup,
1614
baseOverrides: Partial<Schema_Event_Recur_Base>,
1715
instanceOverrides?: Partial<Schema_Event_Recur_Instance>,
1816
) => {
@@ -34,15 +32,13 @@ export const createRecurrenceSeries = async (
3432
...instanceOverrides,
3533
});
3634

37-
await setup.db
38-
.collection(Collections.EVENT)
39-
.insertMany([
40-
withObjectId(baseEvent),
41-
withObjectId(instance1),
42-
withObjectId(instance2),
43-
]);
35+
await mongoService.event.insertMany([
36+
withObjectId(baseEvent),
37+
withObjectId(instance1),
38+
withObjectId(instance2),
39+
]);
4440

45-
const status = await setup.db.collection(Collections.EVENT).find().toArray();
41+
const status = await mongoService.event.find().toArray();
4642
const meta = { createdCount: status.length };
4743
return {
4844
state: {

packages/backend/src/event/services/event.find.test.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Collection } from "mongodb";
1+
import { Collection, Filter } from "mongodb";
22
import { mockEventSetJan22 } from "@core/__mocks__/v1/events/events.22jan";
33
import { mockEventSetSomeday1 } from "@core/__mocks__/v1/events/events.someday.1";
44
import { MapEvent } from "@core/mappers/map.event";
@@ -26,7 +26,12 @@ instances.forEach((i) => {
2626
});
2727

2828
const recurring = [base, ...instances];
29-
const allEvents = [...mockEventSetJan22, ...mockEventSetSomeday1, ...recurring];
29+
const allEvents = [
30+
...mockEventSetJan22,
31+
...mockEventSetSomeday1,
32+
...recurring,
33+
] as Schema_Event[];
34+
3035
describe("Jan 2022: Many Formats", () => {
3136
let eventCollection: Collection<Schema_Event>;
3237

@@ -61,7 +66,7 @@ describe("Jan 2022: Many Formats", () => {
6166
start,
6267
end,
6368
});
64-
const flatFilter = _flatten(filter, {});
69+
const flatFilter = _flatten(filter, {}) as Filter<Schema_Event>;
6570
expect(flatFilter["$lte"]).not.toEqual(new Date(start).toISOString());
6671
expect(flatFilter["$gte"]).not.toEqual(new Date(end).toISOString());
6772
});

packages/backend/src/event/services/event.service.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,8 @@ class EventService {
164164
return response;
165165
};
166166

167-
/*
168-
Deletes all of a user's events
167+
/*
168+
Deletes all of a user's events
169169
REMINDER: this should only delete a user's *Compass* events --
170170
don't ever delete their events in gcal or any other 3rd party calendar
171171
*/
@@ -402,8 +402,8 @@ const _deleteFromCompass = async (event: Schema_Event_Core) => {
402402
const filter = getDeleteByIdFilter(event);
403403

404404
const response = isRecurring
405-
? await mongoService.db.collection(Collections.EVENT).deleteMany(filter)
406-
: await mongoService.db.collection(Collections.EVENT).deleteOne(filter);
405+
? await mongoService.event.deleteMany(filter)
406+
: await mongoService.event.deleteOne(filter);
407407

408408
return response;
409409
};

0 commit comments

Comments
 (0)