fix: harden proxy error handling and regression tests
This commit is contained in:
1 parent
7340cb481f
commit
7bec13b3e5
3 files changed
+184
-388
No files matched your search
+43
-360
@@ -1,130 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
// Mock utility functions for testing
|
||||
// These would normally be imported from actual utility modules
|
||||
|
||||
/**
|
||||
* Check if request is a Git request
|
||||
* @param {Request} request - Request object
|
||||
* @param {URL} url - URL object
|
||||
* @returns {boolean} True if Git request
|
||||
*/
|
||||
function isGitRequest(request, url) {
|
||||
// Check for Git-specific endpoints
|
||||
if (url.pathname.endsWith('/info/refs')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname.endsWith('/git-upload-pack') || url.pathname.endsWith('/git-receive-pack')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for Git user agents
|
||||
const userAgent = request.headers.get('User-Agent') || '';
|
||||
if (userAgent.includes('git/') || userAgent.startsWith('git/')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for Git-specific query parameters
|
||||
if (url.searchParams.has('service')) {
|
||||
const service = url.searchParams.get('service');
|
||||
return service === 'git-upload-pack' || service === 'git-receive-pack';
|
||||
}
|
||||
|
||||
// Check for Git-specific content types
|
||||
const contentType = request.headers.get('Content-Type') || '';
|
||||
if (contentType.includes('git-upload-pack') || contentType.includes('git-receive-pack')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if request is a Git LFS request
|
||||
* @param {Request} request - Request object
|
||||
* @param {URL} url - URL object
|
||||
* @returns {boolean} True if Git LFS request
|
||||
*/
|
||||
function isGitLFSRequest(request, url) {
|
||||
// Check for LFS-specific endpoints
|
||||
if (url.pathname.includes('/info/lfs')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname.includes('/objects/batch')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for LFS object storage endpoints (SHA-256 hash is 64 hex characters)
|
||||
if (url.pathname.match(/\/objects\/[a-fA-F0-9]{64}$/)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for LFS user agent
|
||||
const userAgent = request.headers.get('User-Agent') || '';
|
||||
if (userAgent.includes('git-lfs')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate request method and path
|
||||
* @param {Request} request - Request object
|
||||
* @param {URL} url - URL object
|
||||
* @returns {{valid: boolean, error?: string, status?: number}} Validation result
|
||||
*/
|
||||
function validateRequest(request, url) {
|
||||
const CONFIG = {
|
||||
SECURITY: {
|
||||
ALLOWED_METHODS: ['GET', 'HEAD'],
|
||||
MAX_PATH_LENGTH: 2048
|
||||
}
|
||||
};
|
||||
|
||||
// Allow POST method for Git operations
|
||||
const allowedMethods = isGitRequest(request, url)
|
||||
? ['GET', 'HEAD', 'POST']
|
||||
: CONFIG.SECURITY.ALLOWED_METHODS;
|
||||
|
||||
if (!allowedMethods.includes(request.method)) {
|
||||
return { valid: false, error: 'Method not allowed', status: 405 };
|
||||
}
|
||||
|
||||
if (url.pathname.length > CONFIG.SECURITY.MAX_PATH_LENGTH) {
|
||||
return { valid: false, error: 'Path too long', status: 414 };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Add security headers to response headers
|
||||
* @param {Headers} headers - Response headers
|
||||
* @returns {Headers} Headers with security headers added
|
||||
*/
|
||||
function addSecurityHeaders(headers) {
|
||||
headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
|
||||
headers.set('X-Frame-Options', 'DENY');
|
||||
headers.set('X-XSS-Protection', '1; mode=block');
|
||||
headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
headers.set('Content-Security-Policy', "default-src 'none'; img-src 'self'; script-src 'none'");
|
||||
headers.set('Permissions-Policy', 'interest-cohort=()');
|
||||
return headers;
|
||||
}
|
||||
import { createConfig } from '../../src/config/index.js';
|
||||
import { isGitLFSRequest, isGitRequest } from '../../src/protocols/git.js';
|
||||
import {
|
||||
addSecurityHeaders,
|
||||
createErrorResponse,
|
||||
resolveAllowedOrigin
|
||||
} from '../../src/utils/security.js';
|
||||
import { getAllowedMethods, validateRequest } from '../../src/utils/validation.js';
|
||||
|
||||
describe('Utility Functions', () => {
|
||||
describe('isGitRequest', () => {
|
||||
@@ -135,20 +18,6 @@ describe('Utility Functions', () => {
|
||||
expect(isGitRequest(request, url)).toBe(true);
|
||||
});
|
||||
|
||||
it('should identify Git upload-pack requests', () => {
|
||||
const request = new Request('https://example.com/repo.git/git-upload-pack');
|
||||
const url = new URL(request.url);
|
||||
|
||||
expect(isGitRequest(request, url)).toBe(true);
|
||||
});
|
||||
|
||||
it('should identify Git receive-pack requests', () => {
|
||||
const request = new Request('https://example.com/repo.git/git-receive-pack');
|
||||
const url = new URL(request.url);
|
||||
|
||||
expect(isGitRequest(request, url)).toBe(true);
|
||||
});
|
||||
|
||||
it('should identify Git requests by User-Agent', () => {
|
||||
const request = new Request('https://example.com/repo.git', {
|
||||
headers: { 'User-Agent': 'git/2.34.1' }
|
||||
@@ -158,46 +27,15 @@ describe('Utility Functions', () => {
|
||||
expect(isGitRequest(request, url)).toBe(true);
|
||||
});
|
||||
|
||||
it('should identify Git requests by service parameter', () => {
|
||||
const request = new Request('https://example.com/repo.git/info/refs?service=git-upload-pack');
|
||||
const url = new URL(request.url);
|
||||
|
||||
expect(isGitRequest(request, url)).toBe(true);
|
||||
});
|
||||
|
||||
it('should identify Git requests by content type', () => {
|
||||
const request = new Request('https://example.com/repo.git/git-upload-pack', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-git-upload-pack-request' }
|
||||
});
|
||||
const url = new URL(request.url);
|
||||
|
||||
expect(isGitRequest(request, url)).toBe(true);
|
||||
});
|
||||
|
||||
it('should not identify regular file requests as Git', () => {
|
||||
const request = new Request('https://example.com/repo/file.txt');
|
||||
const url = new URL(request.url);
|
||||
|
||||
expect(isGitRequest(request, url)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle edge cases gracefully', () => {
|
||||
const request = new Request('https://example.com/');
|
||||
const url = new URL(request.url);
|
||||
|
||||
expect(isGitRequest(request, url)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isGitLFSRequest', () => {
|
||||
it('should identify LFS info/lfs requests', () => {
|
||||
const request = new Request('https://example.com/repo.git/info/lfs');
|
||||
const url = new URL(request.url);
|
||||
|
||||
expect(isGitLFSRequest(request, url)).toBe(true);
|
||||
});
|
||||
|
||||
it('should identify LFS batch API requests', () => {
|
||||
const request = new Request('https://example.com/repo.git/objects/batch', {
|
||||
method: 'POST',
|
||||
@@ -208,34 +46,6 @@ describe('Utility Functions', () => {
|
||||
expect(isGitLFSRequest(request, url)).toBe(true);
|
||||
});
|
||||
|
||||
it('should identify LFS object storage requests by path', () => {
|
||||
const request = new Request(
|
||||
'https://example.com/repo.git/objects/a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd'
|
||||
);
|
||||
const url = new URL(request.url);
|
||||
|
||||
expect(isGitLFSRequest(request, url)).toBe(true);
|
||||
});
|
||||
|
||||
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' }
|
||||
});
|
||||
const url = new URL(request.url);
|
||||
|
||||
expect(isGitLFSRequest(request, url)).toBe(true);
|
||||
});
|
||||
|
||||
it('should identify LFS requests by Content-Type header', () => {
|
||||
const request = new Request('https://example.com/repo.git/objects/batch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/vnd.git-lfs+json' }
|
||||
});
|
||||
const url = new URL(request.url);
|
||||
|
||||
expect(isGitLFSRequest(request, url)).toBe(true);
|
||||
});
|
||||
|
||||
it('should identify LFS requests by User-Agent', () => {
|
||||
const request = new Request('https://example.com/repo.git', {
|
||||
headers: { 'User-Agent': 'git-lfs/3.0.0 (GitHub; darwin amd64; go 1.17.2)' }
|
||||
@@ -251,20 +61,6 @@ describe('Utility Functions', () => {
|
||||
|
||||
expect(isGitLFSRequest(request, url)).toBe(false);
|
||||
});
|
||||
|
||||
it('should not identify standard Git requests as LFS', () => {
|
||||
const request = new Request('https://example.com/repo.git/info/refs');
|
||||
const url = new URL(request.url);
|
||||
|
||||
expect(isGitLFSRequest(request, url)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle edge cases gracefully', () => {
|
||||
const request = new Request('https://example.com/');
|
||||
const url = new URL(request.url);
|
||||
|
||||
expect(isGitLFSRequest(request, url)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateRequest', () => {
|
||||
@@ -272,27 +68,10 @@ describe('Utility Functions', () => {
|
||||
const request = new Request('https://example.com/test', { method: 'GET' });
|
||||
const url = new URL(request.url);
|
||||
|
||||
const result = validateRequest(request, url);
|
||||
const result = validateRequest(request, url, createConfig());
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow HEAD requests', () => {
|
||||
const request = new Request('https://example.com/test', { method: 'HEAD' });
|
||||
const url = new URL(request.url);
|
||||
|
||||
const result = validateRequest(request, url);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject PUT requests for non-Git operations', () => {
|
||||
const request = new Request('https://example.com/test', { method: 'PUT' });
|
||||
const url = new URL(request.url);
|
||||
|
||||
const result = validateRequest(request, url);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.status).toBe(405);
|
||||
});
|
||||
|
||||
it('should allow POST requests for Git operations', () => {
|
||||
const request = new Request('https://example.com/repo.git/git-upload-pack', {
|
||||
method: 'POST',
|
||||
@@ -300,27 +79,27 @@ describe('Utility Functions', () => {
|
||||
});
|
||||
const url = new URL(request.url);
|
||||
|
||||
const result = validateRequest(request, url);
|
||||
const result = validateRequest(request, url, createConfig());
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject extremely long paths', () => {
|
||||
const longPath = `/${'a'.repeat(3000)}`;
|
||||
const request = new Request(`https://example.com${longPath}`);
|
||||
it('should reject encoded traversal attempts against the production validator', () => {
|
||||
const request = new Request('https://example.com/gh/user/repo/%2e%2e%2fsecret');
|
||||
const url = new URL(request.url);
|
||||
|
||||
const result = validateRequest(request, url);
|
||||
const result = validateRequest(request, url, createConfig());
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.status).toBe(414);
|
||||
expect(result.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
it('should accept normal length paths', () => {
|
||||
const normalPath = '/gh/microsoft/vscode/archive/refs/heads/main.zip';
|
||||
const request = new Request(`https://example.com${normalPath}`);
|
||||
describe('getAllowedMethods', () => {
|
||||
it('should respect configured methods for regular requests', () => {
|
||||
const config = createConfig({ ALLOWED_METHODS: 'GET,HEAD,POST' });
|
||||
const request = new Request('https://example.com/gh/test/repo/issues', { method: 'POST' });
|
||||
const url = new URL(request.url);
|
||||
|
||||
const result = validateRequest(request, url);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(getAllowedMethods(request, url, config)).toEqual(['GET', 'HEAD', 'POST']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -329,41 +108,12 @@ describe('Utility Functions', () => {
|
||||
const headers = new Headers();
|
||||
const result = addSecurityHeaders(headers);
|
||||
|
||||
expect(result.get('Strict-Transport-Security')).toBeTruthy();
|
||||
expect(result.get('Strict-Transport-Security')).toContain('max-age=31536000');
|
||||
expect(result.get('X-Frame-Options')).toBe('DENY');
|
||||
expect(result.get('X-XSS-Protection')).toBe('1; mode=block');
|
||||
expect(result.get('Referrer-Policy')).toBe('strict-origin-when-cross-origin');
|
||||
expect(result.get('Content-Security-Policy')).toBeTruthy();
|
||||
expect(result.get('Permissions-Policy')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should set HSTS with proper directives', () => {
|
||||
const headers = new Headers();
|
||||
const result = addSecurityHeaders(headers);
|
||||
|
||||
const hsts = result.get('Strict-Transport-Security');
|
||||
expect(hsts).toContain('max-age=31536000');
|
||||
expect(hsts).toContain('includeSubDomains');
|
||||
expect(hsts).toContain('preload');
|
||||
});
|
||||
|
||||
it('should set CSP with restrictive policy', () => {
|
||||
const headers = new Headers();
|
||||
const result = addSecurityHeaders(headers);
|
||||
|
||||
const csp = result.get('Content-Security-Policy');
|
||||
expect(csp).toContain("default-src 'none'");
|
||||
expect(csp).toContain("script-src 'none'");
|
||||
});
|
||||
|
||||
it('should not overwrite existing headers', () => {
|
||||
const headers = new Headers();
|
||||
headers.set('X-Custom-Header', 'custom-value');
|
||||
|
||||
const result = addSecurityHeaders(headers);
|
||||
|
||||
expect(result.get('X-Custom-Header')).toBe('custom-value');
|
||||
expect(result.get('X-Frame-Options')).toBe('DENY');
|
||||
expect(result.get('Content-Security-Policy')).toContain("default-src 'none'");
|
||||
expect(result.get('Permissions-Policy')).toContain('interest-cohort=()');
|
||||
});
|
||||
|
||||
it('should return the same Headers object', () => {
|
||||
@@ -374,101 +124,34 @@ describe('Utility Functions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('URL and Path Utilities', () => {
|
||||
it('should handle URL parsing correctly', () => {
|
||||
const testUrls = [
|
||||
'https://example.com/gh/user/repo/file.txt',
|
||||
'https://example.com/gl/group/project/-/blob/main/README.md',
|
||||
'https://example.com/hf/microsoft/model/resolve/main/config.json'
|
||||
];
|
||||
|
||||
testUrls.forEach(urlString => {
|
||||
expect(() => new URL(urlString)).not.toThrow();
|
||||
|
||||
const url = new URL(urlString);
|
||||
expect(url.protocol).toBe('https:');
|
||||
expect(url.hostname).toBe('example.com');
|
||||
expect(url.pathname).toBeTruthy();
|
||||
describe('resolveAllowedOrigin', () => {
|
||||
it('should return the matching origin from the production config', () => {
|
||||
const config = createConfig({ ALLOWED_ORIGINS: 'https://app.example.com' });
|
||||
const request = new Request('https://example.com/gh/test/repo', {
|
||||
headers: { Origin: 'https://app.example.com' }
|
||||
});
|
||||
|
||||
expect(resolveAllowedOrigin(request, config)).toBe('https://app.example.com');
|
||||
});
|
||||
|
||||
it('should handle query parameters correctly', () => {
|
||||
const url = new URL('https://example.com/gh/repo?ref=main&path=src');
|
||||
it('should reject origins that are not configured', () => {
|
||||
const config = createConfig({ ALLOWED_ORIGINS: 'https://app.example.com' });
|
||||
const request = new Request('https://example.com/gh/test/repo', {
|
||||
headers: { Origin: 'https://evil.example.com' }
|
||||
});
|
||||
|
||||
expect(url.searchParams.get('ref')).toBe('main');
|
||||
expect(url.searchParams.get('path')).toBe('src');
|
||||
expect(url.searchParams.has('nonexistent')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle URL fragments correctly', () => {
|
||||
const url = new URL('https://example.com/gh/repo/README.md#section');
|
||||
|
||||
expect(url.hash).toBe('#section');
|
||||
expect(url.pathname).toBe('/gh/repo/README.md');
|
||||
expect(resolveAllowedOrigin(request, config)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Request and Response Utilities', () => {
|
||||
it('should create requests with proper headers', () => {
|
||||
const request = new Request('https://example.com/test', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'User-Agent': 'Xget/1.0',
|
||||
Accept: 'application/json'
|
||||
}
|
||||
});
|
||||
describe('createErrorResponse', () => {
|
||||
it('should create a plain-text error response with security headers', async () => {
|
||||
const response = createErrorResponse('Bad Request', 400);
|
||||
|
||||
expect(request.method).toBe('GET');
|
||||
expect(request.headers.get('User-Agent')).toBe('Xget/1.0');
|
||||
expect(request.headers.get('Accept')).toBe('application/json');
|
||||
});
|
||||
|
||||
it('should handle request cloning', () => {
|
||||
const originalRequest = new Request('https://example.com/test', {
|
||||
method: 'POST',
|
||||
body: 'test data',
|
||||
headers: { 'Content-Type': 'text/plain' }
|
||||
});
|
||||
|
||||
const clonedRequest = originalRequest.clone();
|
||||
|
||||
expect(clonedRequest.method).toBe(originalRequest.method);
|
||||
expect(clonedRequest.url).toBe(originalRequest.url);
|
||||
expect(clonedRequest.headers.get('Content-Type')).toBe('text/plain');
|
||||
});
|
||||
|
||||
it('should create responses with proper status codes', () => {
|
||||
const responses = [
|
||||
new Response('OK', { status: 200 }),
|
||||
new Response('Not Found', { status: 404 }),
|
||||
new Response('Server Error', { status: 500 })
|
||||
];
|
||||
|
||||
expect(responses[0].status).toBe(200);
|
||||
expect(responses[1].status).toBe(404);
|
||||
expect(responses[2].status).toBe(500);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling Utilities', () => {
|
||||
it('should create proper error responses', () => {
|
||||
const errorResponse = new Response('Bad Request', {
|
||||
status: 400,
|
||||
statusText: 'Bad Request',
|
||||
headers: { 'Content-Type': 'text/plain' }
|
||||
});
|
||||
|
||||
expect(errorResponse.status).toBe(400);
|
||||
expect(errorResponse.statusText).toBe('Bad Request');
|
||||
expect(errorResponse.headers.get('Content-Type')).toBe('text/plain');
|
||||
});
|
||||
|
||||
it('should handle async error scenarios', async () => {
|
||||
const asyncFunction = async () => {
|
||||
throw new Error('Test error');
|
||||
};
|
||||
|
||||
await expect(asyncFunction()).rejects.toThrow('Test error');
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.headers.get('Content-Type')).toBe('text/plain');
|
||||
expect(response.headers.get('X-Frame-Options')).toBe('DENY');
|
||||
expect(await response.text()).toBe('Bad Request');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import worker from '../../src/index.js';
|
||||
|
||||
/** @type {ExecutionContext} */
|
||||
const executionContext = {
|
||||
waitUntil() {},
|
||||
passThroughOnException() {}
|
||||
};
|
||||
|
||||
describe('Worker regression coverage', () => {
|
||||
/** @type {{ match: ReturnType<typeof vi.fn>, put: ReturnType<typeof vi.fn> }} */
|
||||
let cacheDefault;
|
||||
|
||||
beforeEach(() => {
|
||||
cacheDefault = {
|
||||
match: vi.fn(async () => null),
|
||||
put: vi.fn(async () => undefined)
|
||||
};
|
||||
|
||||
vi.stubGlobal('caches', {
|
||||
default: cacheDefault
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('does not leak thrown upstream error details to clients', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('secret-upstream-detail'));
|
||||
|
||||
const response = await worker.fetch(
|
||||
new Request('https://example.com/gh/user/repo/file.txt'),
|
||||
{ MAX_RETRIES: '1', RETRY_DELAY_MS: '0', TIMEOUT_SECONDS: '1' },
|
||||
executionContext
|
||||
);
|
||||
|
||||
const body = await response.text();
|
||||
|
||||
expect(response.status).toBe(502);
|
||||
expect(body).not.toContain('secret-upstream-detail');
|
||||
expect(body).not.toContain('Failed after');
|
||||
});
|
||||
|
||||
it('clears timeout handles when upstream fetch rejects', async () => {
|
||||
const timeoutToken = { id: 'timeout-token' };
|
||||
const setTimeoutSpy = vi.fn(() => timeoutToken);
|
||||
const clearTimeoutSpy = vi.fn();
|
||||
|
||||
vi.stubGlobal('setTimeout', setTimeoutSpy);
|
||||
vi.stubGlobal('clearTimeout', clearTimeoutSpy);
|
||||
vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('boom'));
|
||||
|
||||
const response = await worker.fetch(
|
||||
new Request('https://example.com/gh/user/repo/file.txt'),
|
||||
{ MAX_RETRIES: '1', RETRY_DELAY_MS: '0', TIMEOUT_SECONDS: '5' },
|
||||
executionContext
|
||||
);
|
||||
|
||||
expect(response.status).toBe(502);
|
||||
expect(setTimeoutSpy).toHaveBeenCalled();
|
||||
expect(clearTimeoutSpy).toHaveBeenCalledWith(timeoutToken);
|
||||
});
|
||||
|
||||
it('forwards body and content type for configured non-protocol POST requests', async () => {
|
||||
/** @type {{ url: string, method: string | undefined, body: string | null, contentType: string | null, cf: unknown }} */
|
||||
let observed = {
|
||||
url: '',
|
||||
method: undefined,
|
||||
body: null,
|
||||
contentType: null,
|
||||
cf: undefined
|
||||
};
|
||||
|
||||
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
|
||||
observed = {
|
||||
url: String(input),
|
||||
method: init?.method,
|
||||
body: init?.body ? await new Response(init.body).text() : null,
|
||||
contentType: new Headers(init?.headers).get('Content-Type'),
|
||||
cf: /** @type {RequestInit & { cf?: unknown }} */ (init || {}).cf
|
||||
};
|
||||
|
||||
return new Response('created', {
|
||||
status: 201,
|
||||
headers: { 'Content-Type': 'text/plain' }
|
||||
});
|
||||
});
|
||||
|
||||
const response = await worker.fetch(
|
||||
new Request('https://example.com/gh/user/repo/issues', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title: 'test' })
|
||||
}),
|
||||
{ ALLOWED_METHODS: 'GET,HEAD,POST' },
|
||||
executionContext
|
||||
);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(observed).toEqual({
|
||||
url: 'https://github.com/user/repo/issues',
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ title: 'test' }),
|
||||
contentType: 'application/json',
|
||||
cf: undefined
|
||||
});
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
||||
expect(cacheDefault.match).not.toHaveBeenCalled();
|
||||
expect(response.headers.get('Cache-Control')).toBe('no-store');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user