11import { Test } from '@nestjs/testing' ;
2- import { INestApplication , ValidationPipe } from '@nestjs/common' ;
2+ import { INestApplication , ValidationPipe , VersioningType } from '@nestjs/common' ;
33import request , { Response as SupertestResponse } from 'supertest' ;
44import { AppModule } from 'src/app.module' ;
55import { PrismaService } from 'src/prisma/prisma.service' ;
66import { App } from 'supertest/types' ;
7-
7+ jest . setTimeout ( 30000 ) ;
88type ApiResponse < T > = {
99 success : boolean ;
1010 data : T ;
@@ -19,7 +19,6 @@ type CampaignResponseDto = {
1919} ;
2020
2121function bodyAs < T > ( res : SupertestResponse ) : ApiResponse < T > {
22- // supertest Response.body is `any`; we cast once here to satisfy strict ESLint rules
2322 return res . body as ApiResponse < T > ;
2423}
2524
@@ -28,6 +27,9 @@ describe('Campaigns (e2e)', () => {
2827 let prisma : PrismaService ;
2928
3029 const base = '/api/v1/campaigns' ;
30+ const testApiKey = 'e2e-test-key-0001' ;
31+ const testApiKeyHash = '7cd155083be719224524695fc6e61cf3747b99dd3f6260e392f1b3b69577dcd9' ;
32+ const authHeader = { 'X-Api-Key' : testApiKey } as Record < string , string > ;
3133
3234 beforeAll ( async ( ) => {
3335 const moduleRef = await Test . createTestingModule ( {
@@ -36,6 +38,13 @@ describe('Campaigns (e2e)', () => {
3638
3739 app = moduleRef . createNestApplication ( ) ;
3840
41+ app . setGlobalPrefix ( 'api' ) ;
42+ app . enableVersioning ( {
43+ type : VersioningType . URI ,
44+ defaultVersion : '1' ,
45+ prefix : 'v' ,
46+ } ) ;
47+
3948 app . useGlobalPipes (
4049 new ValidationPipe ( {
4150 whitelist : true ,
@@ -46,19 +55,36 @@ describe('Campaigns (e2e)', () => {
4655
4756 await app . init ( ) ;
4857 prisma = app . get ( PrismaService ) ;
58+
59+ await prisma . apiKey . upsert ( {
60+ where : { keyHash : testApiKeyHash } ,
61+ update : { revokedAt : null } ,
62+ create : {
63+ key : testApiKey ,
64+ keyHash : testApiKeyHash ,
65+ keyPreview : testApiKey . slice ( 0 , 8 ) ,
66+ role : 'admin' ,
67+ } ,
68+ } ) ;
4969 } ) ;
5070
5171 beforeEach ( async ( ) => {
72+ await prisma . claim . deleteMany ( ) ;
73+ await prisma . balanceLedger . deleteMany ( ) ;
74+ await prisma . aidPackage . deleteMany ( ) ;
5275 await prisma . campaign . deleteMany ( ) ;
5376 } ) ;
5477
5578 afterAll ( async ( ) => {
79+ await prisma . apiKey . deleteMany ( { where : { keyHash : testApiKeyHash } } ) ;
5680 await app . close ( ) ;
81+ await new Promise ( resolve => setTimeout ( resolve , 2000 ) ) ;
5782 } ) ;
5883
5984 it ( 'POST /campaigns creates a campaign' , async ( ) => {
6085 const res = await request ( app . getHttpServer ( ) )
6186 . post ( base )
87+ . set ( authHeader )
6288 . send ( { name : 'Test Campaign' , budget : 1000 } )
6389 . expect ( 201 ) ;
6490
@@ -72,25 +98,29 @@ describe('Campaigns (e2e)', () => {
7298 it ( 'POST /campaigns rejects missing required fields' , async ( ) => {
7399 await request ( app . getHttpServer ( ) )
74100 . post ( base )
101+ . set ( authHeader )
75102 . send ( { budget : 1000 } )
76103 . expect ( 400 ) ;
77104
78105 await request ( app . getHttpServer ( ) )
79106 . post ( base )
107+ . set ( authHeader )
80108 . send ( { name : 'Missing Budget' } )
81109 . expect ( 400 ) ;
82110 } ) ;
83111
84112 it ( 'POST /campaigns rejects invalid budgets' , async ( ) => {
85113 await request ( app . getHttpServer ( ) )
86114 . post ( base )
115+ . set ( authHeader )
87116 . send ( { name : 'Bad Budget' , budget : - 1 } )
88117 . expect ( 400 ) ;
89118 } ) ;
90119
91120 it ( 'PATCH /campaigns/:id/archive is idempotent' , async ( ) => {
92121 const createdRes = await request ( app . getHttpServer ( ) )
93122 . post ( base )
123+ . set ( authHeader )
94124 . send ( { name : 'Archive Me' , budget : 10 } )
95125 . expect ( 201 ) ;
96126
@@ -99,6 +129,7 @@ describe('Campaigns (e2e)', () => {
99129
100130 const firstRes = await request ( app . getHttpServer ( ) )
101131 . patch ( `${ base } /${ id } /archive` )
132+ . set ( authHeader )
102133 . expect ( 200 ) ;
103134
104135 const firstBody = bodyAs < CampaignResponseDto > ( firstRes ) ;
@@ -108,6 +139,7 @@ describe('Campaigns (e2e)', () => {
108139
109140 const secondRes = await request ( app . getHttpServer ( ) )
110141 . patch ( `${ base } /${ id } /archive` )
142+ . set ( authHeader )
111143 . expect ( 200 ) ;
112144
113145 const secondBody = bodyAs < CampaignResponseDto > ( secondRes ) ;
@@ -120,10 +152,14 @@ describe('Campaigns (e2e)', () => {
120152 it ( 'GET /campaigns returns a list' , async ( ) => {
121153 await request ( app . getHttpServer ( ) )
122154 . post ( base )
155+ . set ( authHeader )
123156 . send ( { name : 'List Me' , budget : 5 } )
124157 . expect ( 201 ) ;
125158
126- const res = await request ( app . getHttpServer ( ) ) . get ( base ) . expect ( 200 ) ;
159+ const res = await request ( app . getHttpServer ( ) )
160+ . get ( base )
161+ . set ( authHeader )
162+ . expect ( 200 ) ;
127163
128164 const body = bodyAs < CampaignResponseDto [ ] > ( res ) ;
129165
@@ -135,6 +171,39 @@ describe('Campaigns (e2e)', () => {
135171 it ( 'GET /campaigns/:id returns 404 for missing campaign' , async ( ) => {
136172 await request ( app . getHttpServer ( ) )
137173 . get ( `${ base } /does-not-exist` )
174+ . set ( authHeader )
138175 . expect ( 404 ) ;
139176 } ) ;
140- } ) ;
177+
178+ describe ( 'Campaigns HTTP cache' , ( ) => {
179+ it ( 'GET /campaigns returns Cache-Control with max-age=30' , async ( ) => {
180+ const res = await request ( app . getHttpServer ( ) )
181+ . get ( base )
182+ . set ( authHeader )
183+ . expect ( 200 ) ;
184+
185+ const cc = res . headers [ 'cache-control' ] ;
186+ expect ( cc ) . toBeDefined ( ) ;
187+ expect ( cc ) . toContain ( 'max-age=30' ) ;
188+ expect ( cc ) . toContain ( 'private' ) ;
189+ } ) ;
190+
191+ it ( 'second call within TTL returns 304 with X-Edge-Cache-Status: hit' , async ( ) => {
192+ const res1 = await request ( app . getHttpServer ( ) )
193+ . get ( base )
194+ . set ( authHeader )
195+ . expect ( 200 ) ;
196+
197+ const etag = res1 . headers [ 'etag' ] ;
198+ expect ( etag ) . toBeDefined ( ) ;
199+
200+ const res2 = await request ( app . getHttpServer ( ) )
201+ . get ( base )
202+ . set ( authHeader )
203+ . set ( 'If-None-Match' , etag )
204+ . expect ( 304 ) ;
205+
206+ expect ( res2 . headers [ 'x-edge-cache-status' ] ) . toBe ( 'hit' ) ;
207+ } ) ;
208+ } ) ;
209+ } ) ;
0 commit comments