@@ -2,7 +2,7 @@ import { headers } from 'next/headers';
2
2
import type { MockedFunction } from 'vitest' ;
3
3
import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest' ;
4
4
5
- import { collectKeylessMetadata , formatMetadataHeaders } from '../server/keyless-custom-headers' ;
5
+ import { CI_ENV_VARS , collectKeylessMetadata , formatMetadataHeaders } from '../server/keyless-custom-headers' ;
6
6
7
7
// Default mock headers for keyless-custom-headers.ts
8
8
const defaultMockHeaders = new Headers ( {
@@ -64,6 +64,13 @@ interface MockHeaders {
64
64
values ( ) : IterableIterator < string > ;
65
65
}
66
66
67
+ // Helper function to clear all CI environment variables
68
+ function clearAllCIEnvironmentVariables ( ) : void {
69
+ CI_ENV_VARS . forEach ( indicator => {
70
+ vi . stubEnv ( indicator , undefined ) ;
71
+ } ) ;
72
+ }
73
+
67
74
// Helper function to create custom header mocks for specific tests
68
75
function createMockHeaders ( customHeaders : Record < string , string | null > = { } ) : MockHeaders {
69
76
const defaultHeadersObj : Record < string , string > = { } ;
@@ -134,6 +141,7 @@ describe('keyless-custom-headers', () => {
134
141
xPort : '3000' ,
135
142
xProtocol : 'https' ,
136
143
xClerkAuthStatus : 'signed-out' ,
144
+ isCI : true ,
137
145
} ;
138
146
139
147
const result = formatMetadataHeaders ( metadata ) ;
@@ -149,6 +157,7 @@ describe('keyless-custom-headers', () => {
149
157
expect ( result . get ( 'Clerk-X-Port' ) ) . toBe ( '3000' ) ;
150
158
expect ( result . get ( 'Clerk-X-Protocol' ) ) . toBe ( 'https' ) ;
151
159
expect ( result . get ( 'Clerk-Auth-Status' ) ) . toBe ( 'signed-out' ) ;
160
+ expect ( result . get ( 'Clerk-Is-CI' ) ) . toBe ( 'true' ) ;
152
161
} ) ;
153
162
154
163
it ( 'should handle missing optional fields gracefully' , ( ) => {
@@ -159,6 +168,7 @@ describe('keyless-custom-headers', () => {
159
168
xPort : '3000' ,
160
169
xProtocol : 'https' ,
161
170
xClerkAuthStatus : 'signed-out' ,
171
+ isCI : false ,
162
172
// Missing: nodeVersion, nextVersion, npmConfigUserAgent, port
163
173
} ;
164
174
@@ -177,6 +187,7 @@ describe('keyless-custom-headers', () => {
177
187
expect ( result . get ( 'Clerk-Next-Version' ) ) . toBeNull ( ) ;
178
188
expect ( result . get ( 'Clerk-NPM-Config-User-Agent' ) ) . toBeNull ( ) ;
179
189
expect ( result . get ( 'Clerk-Node-Port' ) ) . toBeNull ( ) ;
190
+ expect ( result . get ( 'Clerk-Is-CI' ) ) . toBeNull ( ) ;
180
191
} ) ;
181
192
182
193
it ( 'should handle undefined values for optional fields' , ( ) => {
@@ -191,6 +202,7 @@ describe('keyless-custom-headers', () => {
191
202
xPort : 'test-x-port' ,
192
203
xProtocol : 'test-x-protocol' ,
193
204
xClerkAuthStatus : 'test-auth-status' ,
205
+ isCI : false ,
194
206
} ;
195
207
196
208
const result = formatMetadataHeaders ( metadata ) ;
@@ -208,6 +220,7 @@ describe('keyless-custom-headers', () => {
208
220
expect ( result . get ( 'Clerk-X-Port' ) ) . toBe ( 'test-x-port' ) ;
209
221
expect ( result . get ( 'Clerk-X-Protocol' ) ) . toBe ( 'test-x-protocol' ) ;
210
222
expect ( result . get ( 'Clerk-Auth-Status' ) ) . toBe ( 'test-auth-status' ) ;
223
+ expect ( result . get ( 'Clerk-Is-CI' ) ) . toBeNull ( ) ;
211
224
} ) ;
212
225
213
226
it ( 'should handle empty string values' , ( ) => {
@@ -222,6 +235,7 @@ describe('keyless-custom-headers', () => {
222
235
xPort : '' ,
223
236
xProtocol : '' ,
224
237
xClerkAuthStatus : '' ,
238
+ isCI : false ,
225
239
} ;
226
240
227
241
const result = formatMetadataHeaders ( metadata ) ;
@@ -237,6 +251,7 @@ describe('keyless-custom-headers', () => {
237
251
expect ( result . get ( 'Clerk-X-Port' ) ) . toBeNull ( ) ;
238
252
expect ( result . get ( 'Clerk-X-Protocol' ) ) . toBeNull ( ) ;
239
253
expect ( result . get ( 'Clerk-Auth-Status' ) ) . toBeNull ( ) ;
254
+ expect ( result . get ( 'Clerk-Is-CI' ) ) . toBeNull ( ) ;
240
255
} ) ;
241
256
} ) ;
242
257
@@ -274,6 +289,9 @@ describe('keyless-custom-headers', () => {
274
289
} ) ;
275
290
276
291
it ( 'should collect metadata with all fields present' , async ( ) => {
292
+ // Clear all CI environment variables first to ensure clean test state
293
+ clearAllCIEnvironmentVariables ( ) ;
294
+
277
295
// Setup environment variables
278
296
vi . stubEnv ( 'PORT' , '3000' ) ;
279
297
vi . stubEnv ( 'npm_config_user_agent' , 'npm/9.8.1 node/v18.17.0 darwin x64' ) ;
@@ -334,6 +352,7 @@ describe('keyless-custom-headers', () => {
334
352
xHost : 'example.com' ,
335
353
xProtocol : 'https' ,
336
354
xClerkAuthStatus : 'signed-out' ,
355
+ isCI : false ,
337
356
} ) ;
338
357
339
358
// Restore original values
@@ -419,9 +438,96 @@ describe('keyless-custom-headers', () => {
419
438
expect ( result . xHost ) . toBe ( 'forwarded-test-host.example.com' ) ;
420
439
expect ( result . xProtocol ) . toBe ( 'https' ) ;
421
440
} ) ;
441
+
442
+ it ( 'should detect CI environment with truthy values' , async ( ) => {
443
+ const truthyValues = [ '1' , 'true' , '0.1' ] ;
444
+ const ciPlatforms = [ 'CI' , 'GITHUB_ACTIONS' , 'VERCEL' ] ;
445
+
446
+ for ( const platform of ciPlatforms ) {
447
+ for ( const value of truthyValues ) {
448
+ // Clear all environment variables first to prevent any CI leakage
449
+ vi . unstubAllEnvs ( ) ;
450
+ // Explicitly clear all known CI environment variables
451
+ clearAllCIEnvironmentVariables ( ) ;
452
+ // Then stub only the current platform with the test value
453
+ vi . stubEnv ( platform , value ) ;
454
+
455
+ // Recreate headers mock for each iteration to avoid state pollution
456
+ mockHeaders . mockImplementation ( async ( ) => createMockHeaders ( ) ) ;
457
+
458
+ const result = await collectKeylessMetadata ( ) ;
459
+ expect ( result . isCI ) . toBe ( true ) ;
460
+ }
461
+ }
462
+ } ) ;
463
+
464
+ it ( 'should not detect CI environment with falsy values' , async ( ) => {
465
+ const falsyValues = [ '0' , 'false' , '' ] ;
466
+ const ciPlatforms = [ 'CI' , 'GITHUB_ACTIONS' ] ;
467
+
468
+ for ( const platform of ciPlatforms ) {
469
+ for ( const value of falsyValues ) {
470
+ // Clear all environment variables first to prevent any CI leakage
471
+ vi . unstubAllEnvs ( ) ;
472
+ // Explicitly clear all known CI environment variables
473
+ clearAllCIEnvironmentVariables ( ) ;
474
+ // Then stub only the current platform with the test value
475
+ vi . stubEnv ( platform , value ) ;
476
+
477
+ // Recreate headers mock for each iteration to avoid state pollution
478
+ mockHeaders . mockImplementation ( async ( ) => createMockHeaders ( ) ) ;
479
+
480
+ const result = await collectKeylessMetadata ( ) ;
481
+ expect ( result . isCI ) . toBe ( false ) ;
482
+ }
483
+ }
484
+ } ) ;
485
+
486
+ it ( 'should not detect CI environment when no CI indicators are present' , async ( ) => {
487
+ // Clear all CI-related environment variables
488
+ clearAllCIEnvironmentVariables ( ) ;
489
+
490
+ const result = await collectKeylessMetadata ( ) ;
491
+
492
+ expect ( result . isCI ) . toBe ( false ) ;
493
+ } ) ;
494
+
495
+ it ( 'should only add Clerk-Is-CI header when isCI is true' , ( ) => {
496
+ const metadataWithCI = {
497
+ userAgent : 'test-user-agent' ,
498
+ host : 'test-host' ,
499
+ xHost : 'test-x-host' ,
500
+ xPort : 'test-x-port' ,
501
+ xProtocol : 'test-x-protocol' ,
502
+ xClerkAuthStatus : 'test-auth-status' ,
503
+ isCI : true ,
504
+ } ;
505
+
506
+ const metadataWithoutCI = {
507
+ userAgent : 'test-user-agent' ,
508
+ host : 'test-host' ,
509
+ xHost : 'test-x-host' ,
510
+ xPort : 'test-x-port' ,
511
+ xProtocol : 'test-x-protocol' ,
512
+ xClerkAuthStatus : 'test-auth-status' ,
513
+ isCI : false ,
514
+ } ;
515
+
516
+ const resultWithCI = formatMetadataHeaders ( metadataWithCI ) ;
517
+ const resultWithoutCI = formatMetadataHeaders ( metadataWithoutCI ) ;
518
+
519
+ // When isCI is true, header should be set to 'true'
520
+ expect ( resultWithCI . get ( 'Clerk-Is-CI' ) ) . toBe ( 'true' ) ;
521
+
522
+ // When isCI is false, header should not be set
523
+ expect ( resultWithoutCI . get ( 'Clerk-Is-CI' ) ) . toBeNull ( ) ;
524
+ } ) ;
422
525
} ) ;
423
526
424
527
it ( 'should format metadata collected from collectKeylessMetadata correctly' , async ( ) => {
528
+ // Clear all CI environment variables first to ensure clean test state
529
+ clearAllCIEnvironmentVariables ( ) ;
530
+
425
531
// Setup environment
426
532
vi . stubEnv ( 'PORT' , '4000' ) ;
427
533
vi . stubEnv ( 'npm_config_user_agent' , 'test-npm-agent' ) ;
@@ -475,5 +581,67 @@ describe('keyless-custom-headers', () => {
475
581
expect ( headers . get ( 'Clerk-X-Protocol' ) ) . toBe ( 'https' ) ;
476
582
expect ( headers . get ( 'Clerk-Auth-Status' ) ) . toBe ( 'integration-status' ) ;
477
583
expect ( headers . get ( 'Clerk-NPM-Config-User-Agent' ) ) . toBe ( 'test-npm-agent' ) ;
584
+ expect ( headers . get ( 'Clerk-Is-CI' ) ) . toBeNull ( ) ; // Should be null when no CI environment is detected
585
+ } ) ;
586
+
587
+ it ( 'should format metadata with CI environment detected correctly' , async ( ) => {
588
+ // Reset mock to ensure clean state from previous test
589
+ mockHeaders . mockReset ( ) ;
590
+
591
+ // Setup environment with CI detection
592
+ vi . stubEnv ( 'PORT' , '4000' ) ;
593
+ vi . stubEnv ( 'npm_config_user_agent' , 'test-npm-agent' ) ;
594
+ vi . stubEnv ( 'CI' , '1' ) ; // Set CI environment variable
595
+
596
+ const mockHeaderStore = new Headers ( {
597
+ 'User-Agent' : 'Integration-Test-Agent' ,
598
+ host : 'localhost:4000' ,
599
+ 'x-forwarded-port' : '4000' ,
600
+ 'x-forwarded-host' : 'integration-forwarded-host' ,
601
+ 'x-forwarded-proto' : 'https' ,
602
+ 'x-clerk-auth-status' : 'integration-status' ,
603
+ } ) ;
604
+
605
+ mockHeaders . mockResolvedValue ( {
606
+ get : ( key : string ) => mockHeaderStore . get ( key ) || null ,
607
+ has : ( key : string ) => mockHeaderStore . has ( key ) ,
608
+ forEach : ( ) => { } ,
609
+ entries : function * ( ) {
610
+ const headerEntries : [ string , string ] [ ] = [ ] ;
611
+ mockHeaderStore . forEach ( ( value , key ) => headerEntries . push ( [ key , value ] ) ) ;
612
+ for ( const entry of headerEntries ) {
613
+ yield entry ;
614
+ }
615
+ } ,
616
+ keys : function * ( ) {
617
+ const headerKeys : string [ ] = [ ] ;
618
+ mockHeaderStore . forEach ( ( _ , key ) => headerKeys . push ( key ) ) ;
619
+ for ( const key of headerKeys ) {
620
+ yield key ;
621
+ }
622
+ } ,
623
+ values : function * ( ) {
624
+ const headerValues : string [ ] = [ ] ;
625
+ mockHeaderStore . forEach ( value => headerValues . push ( value ) ) ;
626
+ for ( const value of headerValues ) {
627
+ yield value ;
628
+ }
629
+ } ,
630
+ } as MockHeaders ) ;
631
+
632
+ // Collect metadata and format headers
633
+ const metadata = await collectKeylessMetadata ( ) ;
634
+ const headers = formatMetadataHeaders ( metadata ) ;
635
+
636
+ // Verify the full pipeline works correctly with CI detection
637
+ expect ( headers . get ( 'Clerk-Client-User-Agent' ) ) . toBe ( 'Integration-Test-Agent' ) ;
638
+ expect ( headers . get ( 'Clerk-Client-Host' ) ) . toBe ( 'localhost:4000' ) ;
639
+ expect ( headers . get ( 'Clerk-Node-Port' ) ) . toBe ( '4000' ) ;
640
+ expect ( headers . get ( 'Clerk-X-Port' ) ) . toBe ( '4000' ) ;
641
+ expect ( headers . get ( 'Clerk-X-Host' ) ) . toBe ( 'integration-forwarded-host' ) ;
642
+ expect ( headers . get ( 'Clerk-X-Protocol' ) ) . toBe ( 'https' ) ;
643
+ expect ( headers . get ( 'Clerk-Auth-Status' ) ) . toBe ( 'integration-status' ) ;
644
+ expect ( headers . get ( 'Clerk-NPM-Config-User-Agent' ) ) . toBe ( 'test-npm-agent' ) ;
645
+ expect ( headers . get ( 'Clerk-Is-CI' ) ) . toBe ( 'true' ) ; // Should be 'true' when CI environment is detected
478
646
} ) ;
479
647
} ) ;
0 commit comments