import { describe, expect, it } from 'vitest'; import { createConfig } from '../../src/config/index.js'; import { isGitLFSRequest, isGitRequest } from '../../src/protocols/git.js'; import { addCorsHeaders, addSecurityHeaders, createErrorResponse, resolveAllowedOrigin } from '../../src/utils/security.js'; import { getAllowedMethods, isDockerRequest, validateRequest } from '../../src/utils/validation.js'; describe('Utility Functions', () => { describe('isGitRequest', () => { it('should identify Git info/refs requests', () => { const request = new Request('https://example.com/repo.git/info/refs'); const url = new URL(request.url); expect(isGitRequest(request, url)).toBe(true); }); it('should identify Git requests by User-Agent', () => { const request = new Request('https://example.com/repo.git', { headers: { 'User-Agent': 'git/2.34.1' } }); const url = new URL(request.url); expect(isGitRequest(request, url)).toBe(true); }); it('should not identify regular file requests as Git', () => { const request = new Request('https://example.com/repo/file.txt'); const url = new URL(request.url); expect(isGitRequest(request, url)).toBe(false); }); }); describe('isGitLFSRequest', () => { it('should identify LFS batch API requests', () => { const request = new Request('https://example.com/repo.git/objects/batch', { method: 'POST', headers: { 'Content-Type': 'application/vnd.git-lfs+json' } }); const url = new URL(request.url); expect(isGitLFSRequest(request, url)).toBe(true); }); it('should identify LFS requests by User-Agent', () => { const request = new Request('https://example.com/repo.git', { headers: { 'User-Agent': 'git-lfs/3.0.0 (GitHub; darwin amd64; go 1.17.2)' } }); const url = new URL(request.url); expect(isGitLFSRequest(request, url)).toBe(true); }); it('should not identify regular file requests as LFS', () => { const request = new Request('https://example.com/repo/file.txt'); const url = new URL(request.url); expect(isGitLFSRequest(request, url)).toBe(false); }); }); describe('validateRequest', () => { it('should allow GET requests', () => { const request = new Request('https://example.com/test', { method: 'GET' }); const url = new URL(request.url); const result = validateRequest(request, url, createConfig()); expect(result.valid).toBe(true); }); it('should allow POST requests for Git operations', () => { const request = new Request('https://example.com/repo.git/git-upload-pack', { method: 'POST', headers: { 'User-Agent': 'git/2.34.1' } }); const url = new URL(request.url); const result = validateRequest(request, url, createConfig()); expect(result.valid).toBe(true); }); it('should reject encoded traversal attempts against the production validator', () => { const request = new Request('https://example.com/gh/user/repo/%2e%2e%2fsecret'); const url = new URL(request.url); const result = validateRequest(request, url, createConfig()); expect(result.valid).toBe(false); expect(result.status).toBe(400); }); it('should reject raw traversal sequences from the original request URL', () => { const request = /** @type {Request} */ ({ headers: new Headers(), method: 'GET', url: 'https://example.com/gh/user/repo/../secret' }); const url = new URL('https://example.com/gh/user/secret'); const result = validateRequest(request, url, createConfig()); expect(result.valid).toBe(false); expect(result.status).toBe(400); }); it('should reject paths containing ASCII control characters', () => { const baseUrl = new URL('https://example.com/gh/user/repo/%00file'); const request = /** @type {Request} */ ({ headers: new Headers(), method: 'GET', url: 'https://example.com/gh/user/repo/%00file' }); const url = /** @type {URL} */ ({ origin: 'https://example.com', pathname: '/gh/user/repo/\u0000file', searchParams: baseUrl.searchParams }); const result = validateRequest(request, url, createConfig()); expect(result.valid).toBe(false); expect(result.status).toBe(400); }); it('should reject malformed percent-encoded paths', () => { const baseUrl = new URL('https://example.com/gh/user/repo/%E0%A4%A'); const request = /** @type {Request} */ ({ headers: new Headers(), method: 'GET', url: 'https://example.com/gh/user/repo/%E0%A4%A' }); const url = /** @type {URL} */ ({ origin: 'https://example.com', pathname: '/gh/user/repo/%E0%A4%A', searchParams: baseUrl.searchParams }); const result = validateRequest(request, url, createConfig()); expect(result.valid).toBe(false); expect(result.status).toBe(400); }); it('should reject unsupported methods for regular requests', () => { const request = new Request('https://example.com/gh/user/repo/file.txt', { method: 'PATCH' }); const url = new URL(request.url); const result = validateRequest(request, url, createConfig()); expect(result.valid).toBe(false); expect(result.status).toBe(405); }); it('should reject paths longer than the configured maximum', () => { const request = new Request(`https://example.com/gh/${'a'.repeat(200)}`); const url = new URL(request.url); const result = validateRequest(request, url, createConfig({ MAX_PATH_LENGTH: '32' })); expect(result.valid).toBe(false); expect(result.status).toBe(414); }); }); describe('getAllowedMethods', () => { it('should respect configured methods for regular requests', () => { const config = createConfig({ ALLOWED_METHODS: 'GET,HEAD,POST' }); const request = new Request('https://example.com/gh/test/repo/issues', { method: 'POST' }); const url = new URL(request.url); expect(getAllowedMethods(request, url, config)).toEqual(['GET', 'HEAD', 'POST']); }); it('should allow mutating methods for Hugging Face API endpoints', () => { const request = new Request('https://example.com/hf/token', { method: 'DELETE' }); const url = new URL(request.url); expect(getAllowedMethods(request, url)).toEqual([ 'GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE' ]); }); }); describe('isDockerRequest', () => { it('should identify canonical registry API paths', () => { const request = new Request('https://example.com/cr/ghcr/v2/demo/manifests/latest'); const url = new URL(request.url); expect(isDockerRequest(request, url)).toBe(true); }); it('should identify Docker requests by user agent or manifest headers', () => { const userAgentRequest = new Request('https://example.com/cr/docker/library/nginx', { headers: { 'User-Agent': 'docker/27.0.0' } }); const acceptRequest = new Request('https://example.com/cr/docker/library/nginx', { headers: { Accept: 'application/vnd.oci.image.manifest.v1+json' } }); const contentTypeRequest = new Request('https://example.com/cr/docker/library/nginx', { headers: { 'Content-Type': 'application/vnd.docker.distribution.manifest.v2+json' } }); expect(isDockerRequest(userAgentRequest, new URL(userAgentRequest.url))).toBe(true); expect(isDockerRequest(acceptRequest, new URL(acceptRequest.url))).toBe(true); expect(isDockerRequest(contentTypeRequest, new URL(contentTypeRequest.url))).toBe(true); }); it('should not treat generic /cr/ requests as Docker traffic without registry hints', () => { const request = new Request('https://example.com/cr/docker/library/nginx/readme'); const url = new URL(request.url); expect(isDockerRequest(request, url)).toBe(false); }); }); describe('addSecurityHeaders', () => { it('should add all required security headers', () => { const headers = new Headers(); const result = addSecurityHeaders(headers); expect(result.get('Strict-Transport-Security')).toContain('max-age=31536000'); expect(result.get('X-Frame-Options')).toBe('DENY'); expect(result.get('X-XSS-Protection')).toBe('1; mode=block'); expect(result.get('Referrer-Policy')).toBe('strict-origin-when-cross-origin'); expect(result.get('Content-Security-Policy')).toContain("default-src 'none'"); expect(result.get('Permissions-Policy')).toContain('interest-cohort=()'); }); it('should return the same Headers object', () => { const headers = new Headers(); const result = addSecurityHeaders(headers); expect(result).toBe(headers); }); }); describe('resolveAllowedOrigin', () => { it('should return the matching origin from the production config', () => { const config = createConfig({ ALLOWED_ORIGINS: 'https://app.example.com' }); const request = new Request('https://example.com/gh/test/repo', { headers: { Origin: 'https://app.example.com' } }); expect(resolveAllowedOrigin(request, config)).toBe('https://app.example.com'); }); it('should reject origins that are not configured', () => { const config = createConfig({ ALLOWED_ORIGINS: 'https://app.example.com' }); const request = new Request('https://example.com/gh/test/repo', { headers: { Origin: 'https://evil.example.com' } }); expect(resolveAllowedOrigin(request, config)).toBeNull(); }); it('should allow any origin when wildcard CORS is configured', () => { const config = createConfig({ ALLOWED_ORIGINS: '*' }); const request = new Request('https://example.com/gh/test/repo', { headers: { Origin: 'https://app.example.com' } }); expect(resolveAllowedOrigin(request, config)).toBe('*'); }); }); describe('addCorsHeaders', () => { it('should append allow headers and preserve existing Vary values', () => { const config = createConfig({ ALLOWED_ORIGINS: '*' }); const request = new Request('https://example.com/gh/test/repo', { headers: { Origin: 'https://app.example.com', 'Access-Control-Request-Headers': 'X-Test-Header' } }); const headers = addCorsHeaders(new Headers({ Vary: 'Accept-Encoding' }), request, config); expect(headers.get('Access-Control-Allow-Origin')).toBe('*'); expect(headers.get('Access-Control-Allow-Headers')).toBe('X-Test-Header'); expect(headers.get('Vary')).toBe('Accept-Encoding, Origin'); }); }); describe('createErrorResponse', () => { it('should create a plain-text error response with security headers', async () => { const response = createErrorResponse('Bad Request', 400); expect(response.status).toBe(400); expect(response.headers.get('Content-Type')).toBe('text/plain'); expect(response.headers.get('X-Frame-Options')).toBe('DENY'); expect(await response.text()).toBe('Bad Request'); }); it('should create detailed JSON error responses when requested', async () => { const response = createErrorResponse('Unauthorized', 401, true); const body = await response.json(); expect(response.headers.get('Content-Type')).toBe('application/json'); expect(body).toMatchObject({ error: 'Unauthorized', status: 401 }); expect(body.timestamp).toBeTruthy(); }); }); });