Skip to content

Commit 4fd0e10

Browse files
committed
fix etag tests
1 parent 037022a commit 4fd0e10

File tree

4 files changed

+520
-64
lines changed

4 files changed

+520
-64
lines changed

src/api/routes/events.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,12 @@ const eventsPlugin: FastifyPluginAsync = async (fastify, _options) => {
358358
});
359359
}
360360
await deleteCacheCounter(fastify.dynamoClient, `events-etag-${id}`);
361+
await atomicIncrementCacheCounter(
362+
fastify.dynamoClient,
363+
"events-etag-all",
364+
1,
365+
false,
366+
);
361367
request.log.info(
362368
{ type: "audit", actor: request.username, target: id },
363369
`deleted event "${id}"`,

tests/unit/eventPost.test.ts

Lines changed: 342 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
import { afterAll, expect, test, beforeEach, vi } from "vitest";
2-
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
1+
import { afterAll, expect, test, beforeEach, vi, describe } from "vitest";
2+
import {
3+
DynamoDBClient,
4+
GetItemCommand,
5+
PutItemCommand,
6+
ScanCommand,
7+
} from "@aws-sdk/client-dynamodb";
38
import { mockClient } from "aws-sdk-client-mock";
49
import init from "../../src/api/index.js";
510
import { createJwt } from "./auth.test.js";
@@ -9,6 +14,7 @@ import {
914
} from "@aws-sdk/client-secrets-manager";
1015
import { secretJson, secretObject } from "./secret.testdata.js";
1116
import supertest from "supertest";
17+
import { marshall } from "@aws-sdk/util-dynamodb";
1218

1319
const ddbMock = mockClient(DynamoDBClient);
1420
const smMock = mockClient(SecretsManagerClient);
@@ -17,6 +23,13 @@ vi.stubEnv("JwtSigningKey", jwt_secret);
1723

1824
const app = await init();
1925

26+
vi.mock("../../src/api/functions/discord.js", () => {
27+
const updateDiscordMock = vi.fn().mockResolvedValue({});
28+
return {
29+
updateDiscord: updateDiscordMock,
30+
};
31+
});
32+
2033
test("Sad path: Not authenticated", async () => {
2134
await app.ready();
2235
const response = await supertest(app.server).post("/api/v1/events").send({
@@ -192,6 +205,332 @@ test("Happy path: Adding a weekly repeating, non-featured, paid event", async ()
192205
});
193206
});
194207

208+
describe("ETag Lifecycle Tests", () => {
209+
test("ETag should increment after event creation", async () => {
210+
// Setup
211+
(app as any).nodeCache.flushAll();
212+
ddbMock.reset();
213+
smMock.reset();
214+
vi.useFakeTimers();
215+
216+
// Mock secrets manager
217+
smMock.on(GetSecretValueCommand).resolves({
218+
SecretString: secretJson,
219+
});
220+
221+
// Mock successful DynamoDB operations
222+
ddbMock.on(PutItemCommand).resolves({});
223+
224+
// Mock ScanCommand to return empty Items array
225+
ddbMock.on(ScanCommand).resolves({
226+
Items: [],
227+
});
228+
229+
const testJwt = createJwt(undefined, "0");
230+
231+
// 1. Check initial etag for all events is 0
232+
const initialAllResponse = await app.inject({
233+
method: "GET",
234+
url: "/api/v1/events",
235+
headers: {
236+
Authorization: `Bearer ${testJwt}`,
237+
},
238+
});
239+
240+
expect(initialAllResponse.statusCode).toBe(200);
241+
expect(initialAllResponse.headers.etag).toBe("0");
242+
243+
// 2. Create a new event using supertest
244+
const eventResponse = await supertest(app.server)
245+
.post("/api/v1/events")
246+
.set("Authorization", `Bearer ${testJwt}`)
247+
.send({
248+
description: "Test event for ETag verification",
249+
host: "Social Committee",
250+
location: "Siebel Center",
251+
start: "2024-09-25T18:00:00",
252+
title: "ETag Test Event",
253+
featured: false,
254+
});
255+
256+
expect(eventResponse.statusCode).toBe(201);
257+
const eventId = eventResponse.body.id;
258+
259+
// Mock GetItemCommand to return the event we just created
260+
ddbMock.on(GetItemCommand).resolves({
261+
Item: marshall({
262+
id: eventId,
263+
title: "ETag Test Event",
264+
description: "Test event for ETag verification",
265+
host: "Social Committee",
266+
location: "Siebel Center",
267+
start: "2024-09-25T18:00:00",
268+
featured: false,
269+
}),
270+
});
271+
272+
// 3. Check that the all events etag is now 1
273+
const allEventsResponse = await app.inject({
274+
method: "GET",
275+
url: "/api/v1/events",
276+
headers: {
277+
Authorization: `Bearer ${testJwt}`,
278+
},
279+
});
280+
281+
expect(allEventsResponse.statusCode).toBe(200);
282+
expect(allEventsResponse.headers.etag).toBe("1");
283+
284+
// 4. Check that the specific event etag is also 1
285+
const specificEventResponse = await app.inject({
286+
method: "GET",
287+
url: `/api/v1/events/${eventId}`,
288+
headers: {
289+
Authorization: `Bearer ${testJwt}`,
290+
},
291+
});
292+
293+
expect(specificEventResponse.statusCode).toBe(200);
294+
expect(specificEventResponse.headers.etag).toBe("1");
295+
});
296+
297+
test("ETags should be deleted when events are deleted", async () => {
298+
// Setup
299+
(app as any).nodeCache.flushAll();
300+
ddbMock.reset();
301+
smMock.reset();
302+
vi.useFakeTimers();
303+
304+
// Mock secrets manager
305+
smMock.on(GetSecretValueCommand).resolves({
306+
SecretString: secretJson,
307+
});
308+
309+
// Mock successful DynamoDB operations
310+
ddbMock.on(PutItemCommand).resolves({});
311+
ddbMock.on(ScanCommand).resolves({
312+
Items: [],
313+
});
314+
315+
const testJwt = createJwt(undefined, "0");
316+
317+
// 1. Create an event
318+
const eventResponse = await supertest(app.server)
319+
.post("/api/v1/events")
320+
.set("Authorization", `Bearer ${testJwt}`)
321+
.send({
322+
description: "Test event for deletion",
323+
host: "Social Committee",
324+
location: "Siebel Center",
325+
start: "2024-09-25T18:00:00",
326+
title: "Event to delete",
327+
featured: false,
328+
});
329+
330+
expect(eventResponse.statusCode).toBe(201);
331+
const eventId = eventResponse.body.id;
332+
333+
// Mock GetItemCommand to return the event
334+
ddbMock.on(GetItemCommand).resolves({
335+
Item: marshall({
336+
id: eventId,
337+
title: "Event to delete",
338+
description: "Test event for deletion",
339+
host: "Social Committee",
340+
location: "Siebel Center",
341+
start: "2024-09-25T18:00:00",
342+
featured: false,
343+
}),
344+
});
345+
346+
// 2. Verify the event's etag exists (should be 1)
347+
const eventBeforeDelete = await app.inject({
348+
method: "GET",
349+
url: `/api/v1/events/${eventId}`,
350+
headers: {
351+
Authorization: `Bearer ${testJwt}`,
352+
},
353+
});
354+
355+
expect(eventBeforeDelete.statusCode).toBe(200);
356+
expect(eventBeforeDelete.headers.etag).toBe("1");
357+
358+
// 3. Delete the event
359+
const deleteResponse = await supertest(app.server)
360+
.delete(`/api/v1/events/${eventId}`)
361+
.set("Authorization", `Bearer ${testJwt}`);
362+
363+
expect(deleteResponse.statusCode).toBe(201);
364+
365+
// 4. Verify the event no longer exists (should return 404)
366+
// Change the mock to return empty response (simulating deleted event)
367+
ddbMock.on(GetItemCommand).resolves({
368+
Item: undefined,
369+
});
370+
371+
const eventAfterDelete = await app.inject({
372+
method: "GET",
373+
url: `/api/v1/events/${eventId}`,
374+
headers: {
375+
Authorization: `Bearer ${testJwt}`,
376+
},
377+
});
378+
379+
expect(eventAfterDelete.statusCode).toBe(404);
380+
381+
// 5. Check that all-events etag is incremented to 2
382+
// (1 for creation, 2 for deletion)
383+
const allEventsResponse = await app.inject({
384+
method: "GET",
385+
url: "/api/v1/events",
386+
headers: {
387+
Authorization: `Bearer ${testJwt}`,
388+
},
389+
});
390+
391+
expect(allEventsResponse.statusCode).toBe(200);
392+
expect(allEventsResponse.headers.etag).toBe("2");
393+
});
394+
395+
test("ETags for different events should be independent", async () => {
396+
// Setup
397+
(app as any).nodeCache.flushAll();
398+
ddbMock.reset();
399+
smMock.reset();
400+
vi.useFakeTimers();
401+
402+
// Mock secrets manager
403+
smMock.on(GetSecretValueCommand).resolves({
404+
SecretString: secretJson,
405+
});
406+
407+
// Mock successful DynamoDB operations
408+
ddbMock.on(PutItemCommand).resolves({});
409+
410+
// Mock ScanCommand to return empty Items array initially
411+
ddbMock.on(ScanCommand).resolves({
412+
Items: [],
413+
});
414+
415+
const testJwt = createJwt(undefined, "0");
416+
417+
// 1. Check initial etag for all events is 0
418+
const initialAllResponse = await app.inject({
419+
method: "GET",
420+
url: "/api/v1/events",
421+
headers: {
422+
Authorization: `Bearer ${testJwt}`,
423+
},
424+
});
425+
426+
expect(initialAllResponse.statusCode).toBe(200);
427+
expect(initialAllResponse.headers.etag).toBe("0");
428+
429+
// 2. Create first event
430+
const event1Response = await supertest(app.server)
431+
.post("/api/v1/events")
432+
.set("Authorization", `Bearer ${testJwt}`)
433+
.send({
434+
description: "First test event",
435+
host: "Social Committee",
436+
location: "Siebel Center",
437+
start: "2024-09-25T18:00:00",
438+
title: "Event 1",
439+
featured: false,
440+
});
441+
442+
expect(event1Response.statusCode).toBe(201);
443+
const event1Id = event1Response.body.id;
444+
445+
// 3. Create second event
446+
const event2Response = await supertest(app.server)
447+
.post("/api/v1/events")
448+
.set("Authorization", `Bearer ${testJwt}`)
449+
.send({
450+
description: "Second test event",
451+
host: "Infrastructure Committee",
452+
location: "ECEB",
453+
start: "2024-09-26T18:00:00",
454+
title: "Event 2",
455+
featured: false,
456+
});
457+
458+
expect(event2Response.statusCode).toBe(201);
459+
const event2Id = event2Response.body.id;
460+
461+
// Update GetItemCommand mock to handle different events
462+
ddbMock.on(GetItemCommand).callsFake((params) => {
463+
if (params.Key && params.Key.id) {
464+
const eventId = params.Key.id.S;
465+
466+
if (eventId === event1Id) {
467+
return Promise.resolve({
468+
Item: marshall({
469+
id: event1Id,
470+
title: "Event 1",
471+
description: "First test event",
472+
host: "Social Committee",
473+
location: "Siebel Center",
474+
start: "2024-09-25T18:00:00",
475+
featured: false,
476+
}),
477+
});
478+
} else if (eventId === event2Id) {
479+
return Promise.resolve({
480+
Item: marshall({
481+
id: event2Id,
482+
title: "Event 2",
483+
description: "Second test event",
484+
host: "Infrastructure Committee",
485+
location: "ECEB",
486+
start: "2024-09-26T18:00:00",
487+
featured: false,
488+
}),
489+
});
490+
}
491+
}
492+
493+
return Promise.resolve({});
494+
});
495+
496+
// 4. Check that all events etag is now 2 (incremented twice)
497+
const allEventsResponse = await app.inject({
498+
method: "GET",
499+
url: "/api/v1/events",
500+
headers: {
501+
Authorization: `Bearer ${testJwt}`,
502+
},
503+
});
504+
505+
expect(allEventsResponse.statusCode).toBe(200);
506+
expect(allEventsResponse.headers.etag).toBe("2");
507+
508+
// 5. Check first event etag is 1
509+
const event1Response2 = await app.inject({
510+
method: "GET",
511+
url: `/api/v1/events/${event1Id}`,
512+
headers: {
513+
Authorization: `Bearer ${testJwt}`,
514+
},
515+
});
516+
517+
expect(event1Response2.statusCode).toBe(200);
518+
expect(event1Response2.headers.etag).toBe("1");
519+
520+
// 6. Check second event etag is also 1
521+
const event2Response2 = await app.inject({
522+
method: "GET",
523+
url: `/api/v1/events/${event2Id}`,
524+
headers: {
525+
Authorization: `Bearer ${testJwt}`,
526+
},
527+
});
528+
529+
expect(event2Response2.statusCode).toBe(200);
530+
expect(event2Response2.headers.etag).toBe("1");
531+
});
532+
});
533+
195534
afterAll(async () => {
196535
await app.close();
197536
vi.useRealTimers();
@@ -200,5 +539,6 @@ beforeEach(() => {
200539
(app as any).nodeCache.flushAll();
201540
ddbMock.reset();
202541
smMock.reset();
542+
vi.clearAllMocks();
203543
vi.useFakeTimers();
204544
});

0 commit comments

Comments
 (0)