@@ -22,10 +22,8 @@ const jwt_secret = secretObject["jwt_key"];
2222vi . stubEnv ( "JwtSigningKey" , jwt_secret ) ;
2323
2424// Mock the Discord client to prevent the actual Discord API call
25- vi . mock ( "../../src/api/functions/discord.js" , async ( importOriginal ) => {
26- const mod = await importOriginal ( ) ;
25+ vi . mock ( "../../src/api/functions/discord.js" , async ( ) => {
2726 return {
28- ...mod ,
2927 updateDiscord : vi . fn ( ) . mockResolvedValue ( { } ) ,
3028 } ;
3129} ) ;
@@ -120,6 +118,258 @@ test("ETag should increment after event creation", async () => {
120118 expect ( specificEventResponse . headers . etag ) . toBe ( "1" ) ;
121119} ) ;
122120
121+ test ( "Should return 304 Not Modified when If-None-Match header matches ETag" , async ( ) => {
122+ // Setup
123+ ( app as any ) . nodeCache . flushAll ( ) ;
124+ ddbMock . reset ( ) ;
125+ smMock . reset ( ) ;
126+ vi . useFakeTimers ( ) ;
127+
128+ // Mock secrets manager
129+ smMock . on ( GetSecretValueCommand ) . resolves ( {
130+ SecretString : secretJson ,
131+ } ) ;
132+
133+ // Mock successful DynamoDB operations
134+ ddbMock . on ( PutItemCommand ) . resolves ( { } ) ;
135+
136+ // Mock ScanCommand to return empty Items array
137+ ddbMock . on ( ScanCommand ) . resolves ( {
138+ Items : [ ] ,
139+ } ) ;
140+
141+ const testJwt = createJwt ( undefined , "0" ) ;
142+
143+ // 1. First GET request to establish ETag
144+ const initialResponse = await app . inject ( {
145+ method : "GET" ,
146+ url : "/api/v1/events" ,
147+ headers : {
148+ Authorization : `Bearer ${ testJwt } ` ,
149+ } ,
150+ } ) ;
151+
152+ expect ( initialResponse . statusCode ) . toBe ( 200 ) ;
153+ expect ( initialResponse . headers . etag ) . toBe ( "0" ) ;
154+
155+ // 2. Second GET request with If-None-Match header matching the ETag
156+ const conditionalResponse = await app . inject ( {
157+ method : "GET" ,
158+ url : "/api/v1/events" ,
159+ headers : {
160+ Authorization : `Bearer ${ testJwt } ` ,
161+ "If-None-Match" : "0" ,
162+ } ,
163+ } ) ;
164+
165+ // Expect 304 Not Modified
166+ expect ( conditionalResponse . statusCode ) . toBe ( 304 ) ;
167+ expect ( conditionalResponse . headers . etag ) . toBe ( "0" ) ;
168+ expect ( conditionalResponse . body ) . toBe ( "" ) ; // Empty body on 304
169+ } ) ;
170+
171+ test ( "Should return 304 Not Modified when If-None-Match header matches quoted ETag" , async ( ) => {
172+ // Setup
173+ ( app as any ) . nodeCache . flushAll ( ) ;
174+ ddbMock . reset ( ) ;
175+ smMock . reset ( ) ;
176+ vi . useFakeTimers ( ) ;
177+
178+ // Mock secrets manager
179+ smMock . on ( GetSecretValueCommand ) . resolves ( {
180+ SecretString : secretJson ,
181+ } ) ;
182+
183+ // Mock successful DynamoDB operations
184+ ddbMock . on ( PutItemCommand ) . resolves ( { } ) ;
185+
186+ // Mock ScanCommand to return empty Items array
187+ ddbMock . on ( ScanCommand ) . resolves ( {
188+ Items : [ ] ,
189+ } ) ;
190+
191+ const testJwt = createJwt ( undefined , "0" ) ;
192+
193+ // 1. First GET request to establish ETag
194+ const initialResponse = await app . inject ( {
195+ method : "GET" ,
196+ url : "/api/v1/events" ,
197+ headers : {
198+ Authorization : `Bearer ${ testJwt } ` ,
199+ } ,
200+ } ) ;
201+
202+ expect ( initialResponse . statusCode ) . toBe ( 200 ) ;
203+ expect ( initialResponse . headers . etag ) . toBe ( "0" ) ;
204+
205+ // 2. Second GET request with quoted If-None-Match header
206+ const conditionalResponse = await app . inject ( {
207+ method : "GET" ,
208+ url : "/api/v1/events" ,
209+ headers : {
210+ Authorization : `Bearer ${ testJwt } ` ,
211+ "If-None-Match" : '"0"' ,
212+ } ,
213+ } ) ;
214+
215+ // Expect 304 Not Modified
216+ expect ( conditionalResponse . statusCode ) . toBe ( 304 ) ;
217+ expect ( conditionalResponse . headers . etag ) . toBe ( "0" ) ;
218+ expect ( conditionalResponse . body ) . toBe ( "" ) ; // Empty body on 304
219+ } ) ;
220+
221+ test ( "Should NOT return 304 when ETag has changed" , async ( ) => {
222+ // Setup
223+ ( app as any ) . nodeCache . flushAll ( ) ;
224+ ddbMock . reset ( ) ;
225+ smMock . reset ( ) ;
226+ vi . useFakeTimers ( ) ;
227+
228+ // Mock secrets manager
229+ smMock . on ( GetSecretValueCommand ) . resolves ( {
230+ SecretString : secretJson ,
231+ } ) ;
232+
233+ // Mock successful DynamoDB operations
234+ ddbMock . on ( PutItemCommand ) . resolves ( { } ) ;
235+
236+ // Mock ScanCommand to return empty Items array
237+ ddbMock . on ( ScanCommand ) . resolves ( {
238+ Items : [ ] ,
239+ } ) ;
240+
241+ const testJwt = createJwt ( undefined , "0" ) ;
242+
243+ // 1. Initial GET to establish ETag
244+ const initialResponse = await app . inject ( {
245+ method : "GET" ,
246+ url : "/api/v1/events" ,
247+ headers : {
248+ Authorization : `Bearer ${ testJwt } ` ,
249+ } ,
250+ } ) ;
251+
252+ expect ( initialResponse . statusCode ) . toBe ( 200 ) ;
253+ expect ( initialResponse . headers . etag ) . toBe ( "0" ) ;
254+
255+ // 2. Create a new event to change the ETag
256+ const eventResponse = await supertest ( app . server )
257+ . post ( "/api/v1/events" )
258+ . set ( "Authorization" , `Bearer ${ testJwt } ` )
259+ . send ( {
260+ description : "Test event to change ETag" ,
261+ host : "Social Committee" ,
262+ location : "Siebel Center" ,
263+ start : "2024-09-25T18:00:00" ,
264+ title : "ETag Change Test" ,
265+ featured : false ,
266+ } ) ;
267+
268+ expect ( eventResponse . statusCode ) . toBe ( 201 ) ;
269+ const eventId = eventResponse . body . id ;
270+
271+ // Mock GetItemCommand to return the event we just created
272+ ddbMock . on ( GetItemCommand ) . resolves ( {
273+ Item : marshall ( {
274+ id : eventId ,
275+ title : "ETag Change Test" ,
276+ description : "Test event to change ETag" ,
277+ host : "Social Committee" ,
278+ location : "Siebel Center" ,
279+ start : "2024-09-25T18:00:00" ,
280+ featured : false ,
281+ } ) ,
282+ } ) ;
283+
284+ // 3. Make conditional request with old ETag
285+ const conditionalResponse = await app . inject ( {
286+ method : "GET" ,
287+ url : "/api/v1/events" ,
288+ headers : {
289+ Authorization : `Bearer ${ testJwt } ` ,
290+ "If-None-Match" : "0" ,
291+ } ,
292+ } ) ;
293+
294+ // Expect 200 OK (not 304) since ETag has changed
295+ expect ( conditionalResponse . statusCode ) . toBe ( 200 ) ;
296+ expect ( conditionalResponse . headers . etag ) . toBe ( "1" ) ;
297+ expect ( conditionalResponse . body ) . not . toBe ( "" ) ; // Should have body content
298+ } ) ;
299+
300+ test ( "Should handle 304 responses for individual event endpoints" , async ( ) => {
301+ // Setup
302+ ( app as any ) . nodeCache . flushAll ( ) ;
303+ ddbMock . reset ( ) ;
304+ smMock . reset ( ) ;
305+ vi . useFakeTimers ( ) ;
306+
307+ // Mock secrets manager
308+ smMock . on ( GetSecretValueCommand ) . resolves ( {
309+ SecretString : secretJson ,
310+ } ) ;
311+
312+ // Mock successful DynamoDB operations
313+ ddbMock . on ( PutItemCommand ) . resolves ( { } ) ;
314+
315+ // Create an event
316+ const testJwt = createJwt ( undefined , "0" ) ;
317+ const eventResponse = await supertest ( app . server )
318+ . post ( "/api/v1/events" )
319+ . set ( "Authorization" , `Bearer ${ testJwt } ` )
320+ . send ( {
321+ description : "Individual event test" ,
322+ host : "Social Committee" ,
323+ location : "Siebel Center" ,
324+ start : "2024-09-25T18:00:00" ,
325+ title : "ETag Individual Test" ,
326+ featured : false ,
327+ } ) ;
328+
329+ expect ( eventResponse . statusCode ) . toBe ( 201 ) ;
330+ const eventId = eventResponse . body . id ;
331+
332+ // Mock GetItemCommand to return the event
333+ ddbMock . on ( GetItemCommand ) . resolves ( {
334+ Item : marshall ( {
335+ id : eventId ,
336+ title : "ETag Individual Test" ,
337+ description : "Individual event test" ,
338+ host : "Social Committee" ,
339+ location : "Siebel Center" ,
340+ start : "2024-09-25T18:00:00" ,
341+ featured : false ,
342+ } ) ,
343+ } ) ;
344+
345+ // 1. First GET to establish ETag
346+ const initialEventResponse = await app . inject ( {
347+ method : "GET" ,
348+ url : `/api/v1/events/${ eventId } ` ,
349+ headers : {
350+ Authorization : `Bearer ${ testJwt } ` ,
351+ } ,
352+ } ) ;
353+
354+ expect ( initialEventResponse . statusCode ) . toBe ( 200 ) ;
355+ expect ( initialEventResponse . headers . etag ) . toBe ( "1" ) ;
356+
357+ // 2. Second GET with matching If-None-Match
358+ const conditionalEventResponse = await app . inject ( {
359+ method : "GET" ,
360+ url : `/api/v1/events/${ eventId } ` ,
361+ headers : {
362+ Authorization : `Bearer ${ testJwt } ` ,
363+ "If-None-Match" : "1" ,
364+ } ,
365+ } ) ;
366+
367+ // Expect 304 Not Modified
368+ expect ( conditionalEventResponse . statusCode ) . toBe ( 304 ) ;
369+ expect ( conditionalEventResponse . headers . etag ) . toBe ( "1" ) ;
370+ expect ( conditionalEventResponse . body ) . toBe ( "" ) ;
371+ } ) ;
372+
123373afterAll ( async ( ) => {
124374 await app . close ( ) ;
125375 vi . useRealTimers ( ) ;
0 commit comments