Skip to content

Commit 43b46d7

Browse files
Update test suite
1 parent 62c4697 commit 43b46d7

File tree

2 files changed

+200
-30
lines changed

2 files changed

+200
-30
lines changed

packages/nextjs/src/__tests__/keyless-custom-headers.test.ts

Lines changed: 169 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { headers } from 'next/headers';
22
import type { MockedFunction } from 'vitest';
33
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
44

5-
import { collectKeylessMetadata, formatMetadataHeaders } from '../server/keyless-custom-headers';
5+
import { CI_ENV_VARS, collectKeylessMetadata, formatMetadataHeaders } from '../server/keyless-custom-headers';
66

77
// Default mock headers for keyless-custom-headers.ts
88
const defaultMockHeaders = new Headers({
@@ -64,6 +64,13 @@ interface MockHeaders {
6464
values(): IterableIterator<string>;
6565
}
6666

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+
6774
// Helper function to create custom header mocks for specific tests
6875
function createMockHeaders(customHeaders: Record<string, string | null> = {}): MockHeaders {
6976
const defaultHeadersObj: Record<string, string> = {};
@@ -134,6 +141,7 @@ describe('keyless-custom-headers', () => {
134141
xPort: '3000',
135142
xProtocol: 'https',
136143
xClerkAuthStatus: 'signed-out',
144+
isCI: true,
137145
};
138146

139147
const result = formatMetadataHeaders(metadata);
@@ -149,6 +157,7 @@ describe('keyless-custom-headers', () => {
149157
expect(result.get('Clerk-X-Port')).toBe('3000');
150158
expect(result.get('Clerk-X-Protocol')).toBe('https');
151159
expect(result.get('Clerk-Auth-Status')).toBe('signed-out');
160+
expect(result.get('Clerk-Is-CI')).toBe('true');
152161
});
153162

154163
it('should handle missing optional fields gracefully', () => {
@@ -159,6 +168,7 @@ describe('keyless-custom-headers', () => {
159168
xPort: '3000',
160169
xProtocol: 'https',
161170
xClerkAuthStatus: 'signed-out',
171+
isCI: false,
162172
// Missing: nodeVersion, nextVersion, npmConfigUserAgent, port
163173
};
164174

@@ -177,6 +187,7 @@ describe('keyless-custom-headers', () => {
177187
expect(result.get('Clerk-Next-Version')).toBeNull();
178188
expect(result.get('Clerk-NPM-Config-User-Agent')).toBeNull();
179189
expect(result.get('Clerk-Node-Port')).toBeNull();
190+
expect(result.get('Clerk-Is-CI')).toBeNull();
180191
});
181192

182193
it('should handle undefined values for optional fields', () => {
@@ -191,6 +202,7 @@ describe('keyless-custom-headers', () => {
191202
xPort: 'test-x-port',
192203
xProtocol: 'test-x-protocol',
193204
xClerkAuthStatus: 'test-auth-status',
205+
isCI: false,
194206
};
195207

196208
const result = formatMetadataHeaders(metadata);
@@ -208,6 +220,7 @@ describe('keyless-custom-headers', () => {
208220
expect(result.get('Clerk-X-Port')).toBe('test-x-port');
209221
expect(result.get('Clerk-X-Protocol')).toBe('test-x-protocol');
210222
expect(result.get('Clerk-Auth-Status')).toBe('test-auth-status');
223+
expect(result.get('Clerk-Is-CI')).toBeNull();
211224
});
212225

213226
it('should handle empty string values', () => {
@@ -222,6 +235,7 @@ describe('keyless-custom-headers', () => {
222235
xPort: '',
223236
xProtocol: '',
224237
xClerkAuthStatus: '',
238+
isCI: false,
225239
};
226240

227241
const result = formatMetadataHeaders(metadata);
@@ -237,6 +251,7 @@ describe('keyless-custom-headers', () => {
237251
expect(result.get('Clerk-X-Port')).toBeNull();
238252
expect(result.get('Clerk-X-Protocol')).toBeNull();
239253
expect(result.get('Clerk-Auth-Status')).toBeNull();
254+
expect(result.get('Clerk-Is-CI')).toBeNull();
240255
});
241256
});
242257

@@ -274,6 +289,9 @@ describe('keyless-custom-headers', () => {
274289
});
275290

276291
it('should collect metadata with all fields present', async () => {
292+
// Clear all CI environment variables first to ensure clean test state
293+
clearAllCIEnvironmentVariables();
294+
277295
// Setup environment variables
278296
vi.stubEnv('PORT', '3000');
279297
vi.stubEnv('npm_config_user_agent', 'npm/9.8.1 node/v18.17.0 darwin x64');
@@ -334,6 +352,7 @@ describe('keyless-custom-headers', () => {
334352
xHost: 'example.com',
335353
xProtocol: 'https',
336354
xClerkAuthStatus: 'signed-out',
355+
isCI: false,
337356
});
338357

339358
// Restore original values
@@ -419,9 +438,96 @@ describe('keyless-custom-headers', () => {
419438
expect(result.xHost).toBe('forwarded-test-host.example.com');
420439
expect(result.xProtocol).toBe('https');
421440
});
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+
});
422525
});
423526

424527
it('should format metadata collected from collectKeylessMetadata correctly', async () => {
528+
// Clear all CI environment variables first to ensure clean test state
529+
clearAllCIEnvironmentVariables();
530+
425531
// Setup environment
426532
vi.stubEnv('PORT', '4000');
427533
vi.stubEnv('npm_config_user_agent', 'test-npm-agent');
@@ -475,5 +581,67 @@ describe('keyless-custom-headers', () => {
475581
expect(headers.get('Clerk-X-Protocol')).toBe('https');
476582
expect(headers.get('Clerk-Auth-Status')).toBe('integration-status');
477583
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
478646
});
479647
});

packages/nextjs/src/server/keyless-custom-headers.ts

Lines changed: 31 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { headers } from 'next/headers';
44

5-
interface MetadataHeaders {
5+
export interface MetadataHeaders {
66
nodeVersion?: string;
77
nextVersion?: string;
88
npmConfigUserAgent?: string;
@@ -37,38 +37,40 @@ export async function collectKeylessMetadata(): Promise<MetadataHeaders> {
3737
};
3838
}
3939

40+
// Common CI environment variables
41+
export const CI_ENV_VARS = [
42+
'CI',
43+
'CONTINUOUS_INTEGRATION',
44+
'BUILD_NUMBER',
45+
'BUILD_ID',
46+
'BUILDKITE',
47+
'CIRCLECI',
48+
'GITHUB_ACTIONS',
49+
'GITLAB_CI',
50+
'JENKINS_URL',
51+
'TRAVIS',
52+
'APPVEYOR',
53+
'WERCKER',
54+
'DRONE',
55+
'CODESHIP',
56+
'SEMAPHORE',
57+
'SHIPPABLE',
58+
'TEAMCITY_VERSION',
59+
'BAMBOO_BUILDKEY',
60+
'GO_PIPELINE_NAME',
61+
'TF_BUILD',
62+
'SYSTEM_TEAMFOUNDATIONCOLLECTIONURI',
63+
'BITBUCKET_BUILD_NUMBER',
64+
'HEROKU_TEST_RUN_ID',
65+
'VERCEL',
66+
'NETLIFY',
67+
];
68+
4069
/**
4170
* Detects if the application is running in a CI environment
4271
*/
4372
function detectCIEnvironment(): boolean {
44-
// Common CI environment variables
45-
const ciIndicators = [
46-
'CI',
47-
'CONTINUOUS_INTEGRATION',
48-
'BUILD_NUMBER',
49-
'BUILD_ID',
50-
'BUILDKITE',
51-
'CIRCLECI',
52-
'GITHUB_ACTIONS',
53-
'GITLAB_CI',
54-
'JENKINS_URL',
55-
'TRAVIS',
56-
'APPVEYOR',
57-
'WERCKER',
58-
'DRONE',
59-
'CODESHIP',
60-
'SEMAPHORE',
61-
'SHIPPABLE',
62-
'TEAMCITY_VERSION',
63-
'BAMBOO_BUILDKEY',
64-
'GO_PIPELINE_NAME',
65-
'TF_BUILD',
66-
'SYSTEM_TEAMFOUNDATIONCOLLECTIONURI',
67-
'BITBUCKET_BUILD_NUMBER',
68-
'HEROKU_TEST_RUN_ID',
69-
'VERCEL',
70-
'NETLIFY',
71-
];
73+
const ciIndicators = CI_ENV_VARS;
7274

7375
const falsyValues = new Set<string>(['', 'false', '0', 'no']);
7476

0 commit comments

Comments
 (0)