Skip to content

Commit 9436564

Browse files
committed
✨ feat(sub-calendars): add migeration tests
1 parent cfdff24 commit 9436564

File tree

5 files changed

+228
-27
lines changed

5 files changed

+228
-27
lines changed

packages/backend/src/common/services/mongo.service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,10 @@ class MongoService {
220220
const r = await this.db.collection(collection).findOne(filter);
221221
return r !== null;
222222
}
223+
224+
async collectionExists(name: string): Promise<boolean> {
225+
return this.db.listCollections({ name }).hasNext();
226+
}
223227
}
224228

225229
export default new MongoService();

packages/core/src/__mocks__/v1/calendarlist/gcal.calendarlist.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
export const gcalCalendarList = {
1+
import { gSchema$CalendarList } from "@core/types/gcal";
2+
3+
export const gcalCalendarList: gSchema$CalendarList = {
24
kind: "calendar#calendarList",
35
etag: '"p32g9nb5bt6fva0g"',
46
nextSyncToken: "CKCbrKvpn_UCEhh0eWxlci5oaXR6ZW1hbkBnbWFpbC5jb20=",

packages/core/src/types/calendar.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const GoogleCalendarMetadataSchema = z.object({
2424
description: z.string().nullable().optional(),
2525
location: z.string().nullable().optional(),
2626
accessRole: z.enum(["freeBusyReader", "reader", "writer", "owner"]),
27-
primary: z.boolean().default(false),
27+
primary: z.boolean().default(false).optional(),
2828
conferenceProperties: z.object({
2929
allowedConferenceSolutionTypes: z.array(
3030
z.enum(["hangoutsMeet", "eventHangout", "eventNamedHangout"]),
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import { faker } from "@faker-js/faker";
2+
import { zodToMongoSchema } from "@scripts/common/zod-to-mongo-schema";
3+
import Migration from "@scripts/migrations/2025.10.03T01.19.59.calendar-schema";
4+
import { gcalCalendarList } from "@core/__mocks__/v1/calendarlist/gcal.calendarlist";
5+
import {
6+
CompassCalendarSchema,
7+
GoogleCalendarMetadataSchema,
8+
} from "@core/types/calendar.types";
9+
import {
10+
cleanupCollections,
11+
cleanupTestDb,
12+
setupTestDb,
13+
} from "@backend/__tests__/helpers/mock.db.setup";
14+
import { Collections } from "@backend/common/constants/collections";
15+
import mongoService from "@backend/common/services/mongo.service";
16+
17+
describe("2025.10.03T01.19.59.calendar-schema", () => {
18+
const migration = new Migration();
19+
const collectionName = Collections.CALENDAR;
20+
21+
beforeAll(setupTestDb);
22+
beforeEach(cleanupCollections);
23+
afterEach(() => mongoService.calendar.drop());
24+
afterAll(cleanupTestDb);
25+
26+
async function validateUpMigration() {
27+
const indexes = await mongoService.calendar.indexes();
28+
const collectionInfo = await mongoService.calendar.options();
29+
const $jsonSchema = zodToMongoSchema(CompassCalendarSchema);
30+
31+
expect(collectionInfo["validationLevel"]).toBe("strict");
32+
expect(collectionInfo["validator"]).toBeDefined();
33+
expect(collectionInfo["validator"]).toHaveProperty("$jsonSchema");
34+
expect(collectionInfo["validator"]["$jsonSchema"]).toEqual($jsonSchema);
35+
36+
expect(indexes).toEqual(
37+
expect.arrayContaining([
38+
expect.objectContaining({ name: "_id_", key: { _id: 1 } }),
39+
expect.objectContaining({
40+
name: "calendar_user_primary_unique",
41+
unique: true,
42+
key: { user: 1, primary: 1 },
43+
}),
44+
expect.objectContaining({
45+
name: "calendar_user_metadata__id_metadata__provider_unique",
46+
unique: true,
47+
key: { user: 1, "metadata.id": 1, "metadata.provider": 1 },
48+
}),
49+
expect.objectContaining({
50+
name: "calendar_user_selected_index",
51+
key: { user: 1, selected: 1 },
52+
}),
53+
]),
54+
);
55+
}
56+
57+
it("should create collection with schema and indexes if not exists", async () => {
58+
const existsBefore = await mongoService.collectionExists(collectionName);
59+
60+
expect(existsBefore).toBe(false);
61+
62+
await migration.up();
63+
64+
const existsAfter = await mongoService.collectionExists(collectionName);
65+
66+
expect(existsAfter).toBe(true);
67+
68+
await validateUpMigration();
69+
});
70+
71+
it("should update collection with schema and indexes if exists", async () => {
72+
await mongoService.db.createCollection(collectionName);
73+
74+
const existsBefore = await mongoService.collectionExists(collectionName);
75+
76+
expect(existsBefore).toBe(true);
77+
78+
await migration.up();
79+
80+
const existsAfter = await mongoService.collectionExists(collectionName);
81+
82+
expect(existsAfter).toBe(true);
83+
84+
await validateUpMigration();
85+
});
86+
87+
it("should remove schema and indexes but not collection on down", async () => {
88+
await migration.up();
89+
90+
const existsBefore = await mongoService.collectionExists(collectionName);
91+
92+
expect(existsBefore).toBe(true);
93+
94+
await validateUpMigration();
95+
96+
await migration.down();
97+
98+
const existsAfter = await mongoService.collectionExists(collectionName);
99+
100+
expect(existsAfter).toBe(true);
101+
102+
const indexes = await mongoService.calendar.indexes();
103+
const collectionInfo = await mongoService.calendar.options();
104+
105+
expect(indexes).toHaveLength(1);
106+
expect(indexes).toEqual([
107+
expect.objectContaining({ name: "_id_", key: { _id: 1 } }),
108+
]);
109+
110+
expect(collectionInfo["validationLevel"]).toBe("off");
111+
expect(collectionInfo["validationAction"]).toBe("error");
112+
expect(collectionInfo).not.toHaveProperty("validator");
113+
});
114+
115+
describe("mongo $jsonSchema validation", () => {
116+
const maxIndex = gcalCalendarList.items!.length - 1;
117+
118+
function generateCompassCalendar() {
119+
const calendarIndex = faker.number.int({ min: 0, max: maxIndex });
120+
const gCalendarEntry = gcalCalendarList.items![calendarIndex]!;
121+
const gCalendar = GoogleCalendarMetadataSchema.parse(gCalendarEntry);
122+
123+
return CompassCalendarSchema.parse({
124+
_id: faker.database.mongodbObjectId(),
125+
user: faker.database.mongodbObjectId(),
126+
backgroundColor: gCalendarEntry.backgroundColor!,
127+
color: gCalendarEntry.foregroundColor!,
128+
primary: gCalendarEntry.primary!,
129+
selected: gCalendarEntry.selected!,
130+
timezone: gCalendarEntry.timeZone!,
131+
createdAt: new Date(),
132+
metadata: gCalendar,
133+
});
134+
}
135+
136+
beforeEach(migration.up.bind(migration));
137+
138+
it("should accept valid calendar document", async () => {
139+
const calendar = generateCompassCalendar();
140+
141+
await expect(mongoService.calendar.insertOne(calendar)).resolves.toEqual(
142+
expect.objectContaining({
143+
acknowledged: true,
144+
insertedId: calendar._id,
145+
}),
146+
);
147+
});
148+
149+
it("should reject calendar with missing 'user' field", async () => {
150+
const calendar = generateCompassCalendar();
151+
// @ts-expect-error testing missing user field
152+
delete calendar.user;
153+
154+
await expect(mongoService.calendar.insertOne(calendar)).rejects.toThrow(
155+
/Document failed validation/,
156+
);
157+
});
158+
159+
it("should reject calendar with missing 'primary' field", async () => {
160+
const calendar = generateCompassCalendar();
161+
// @ts-expect-error testing missing primary field
162+
delete calendar.primary;
163+
164+
await expect(mongoService.calendar.insertOne(calendar)).rejects.toThrow(
165+
/Document failed validation/,
166+
);
167+
});
168+
169+
it("should reject calendar with missing 'selected' field", async () => {
170+
const calendar = generateCompassCalendar();
171+
// @ts-expect-error testing missing selected field
172+
delete calendar.selected;
173+
174+
await expect(mongoService.calendar.insertOne(calendar)).rejects.toThrow(
175+
/Document failed validation/,
176+
);
177+
});
178+
179+
it("should reject calendar with missing 'color' field", async () => {
180+
const calendar = generateCompassCalendar();
181+
// @ts-expect-error testing missing color field
182+
delete calendar.color;
183+
184+
await expect(mongoService.calendar.insertOne(calendar)).rejects.toThrow(
185+
/Document failed validation/,
186+
);
187+
});
188+
189+
it("should reject calendar with missing 'backgroundColor' field", async () => {
190+
const calendar = generateCompassCalendar();
191+
// @ts-expect-error testing missing backgroundColor field
192+
delete calendar.backgroundColor;
193+
194+
await expect(mongoService.calendar.insertOne(calendar)).rejects.toThrow(
195+
/Document failed validation/,
196+
);
197+
});
198+
199+
it("should reject calendar with missing 'createdAt' field", async () => {
200+
const calendar = generateCompassCalendar();
201+
// @ts-expect-error testing missing createdAt field
202+
delete calendar.createdAt;
203+
204+
await expect(mongoService.calendar.insertOne(calendar)).rejects.toThrow(
205+
/Document failed validation/,
206+
);
207+
});
208+
});
209+
});

packages/scripts/src/migrations/2025.10.03T01.19.59.calendar-schema.ts

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,9 @@ export default class Migration implements RunnableMigration<MigrationContext> {
88
readonly name: string = "2025.10.03T01.19.59.calendar-schema";
99
readonly path: string = "2025.10.03T01.19.59.calendar-schema.ts";
1010

11-
async clearSchemaValidation() {
12-
const { collectionName } = mongoService.calendar;
13-
const collections = await mongoService.db.listCollections().toArray();
14-
const exists = collections.some(({ name }) => collectionName === name);
15-
16-
if (!exists) return;
17-
18-
// do not run in session
19-
await mongoService.db.command({
20-
collMod: mongoService.calendar.collectionName,
21-
validationLevel: "off",
22-
validator: {},
23-
});
24-
}
25-
2611
async up(): Promise<void> {
2712
const { collectionName } = mongoService.calendar;
28-
const collections = await mongoService.db.listCollections().toArray();
29-
const exists = collections.some(({ name }) => collectionName === name);
13+
const exists = await mongoService.collectionExists(collectionName);
3014

3115
const $jsonSchema = zodToMongoSchema(CompassCalendarSchema);
3216

@@ -46,10 +30,7 @@ export default class Migration implements RunnableMigration<MigrationContext> {
4630

4731
await mongoService.calendar.createIndex(
4832
{ user: 1, primary: 1 },
49-
{
50-
unique: true,
51-
name: `${collectionName}_user_primary_unique`,
52-
},
33+
{ unique: true, name: `${collectionName}_user_primary_unique` },
5334
);
5435

5536
await mongoService.calendar.createIndex(
@@ -62,17 +43,22 @@ export default class Migration implements RunnableMigration<MigrationContext> {
6243

6344
await mongoService.calendar.createIndex(
6445
{ user: 1, selected: 1 },
65-
{
66-
name: `${collectionName}_user_selected_index`,
67-
},
46+
{ name: `${collectionName}_user_selected_index` },
6847
);
6948
}
7049

7150
async down(): Promise<void> {
7251
// do not drop table, just remove indexes and schema validation
7352
const { collectionName } = mongoService.calendar;
53+
const exists = await mongoService.collectionExists(collectionName);
54+
55+
if (!exists) return;
7456

75-
await this.clearSchemaValidation();
57+
await mongoService.db.command({
58+
collMod: collectionName,
59+
validationLevel: "off",
60+
validator: {},
61+
});
7662

7763
await mongoService.calendar.dropIndex(
7864
`${collectionName}_user_primary_unique`,

0 commit comments

Comments
 (0)