Files
xget/test/unit/worker-regressions.test.js
T

719 lines
22 KiB
JavaScript

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('does not cache host-bound PyPI rewritten HTML responses', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('<a href="https://files.pythonhosted.org/packages/demo.whl">demo</a>', {
status: 200,
headers: { 'Content-Type': 'text/html; charset=utf-8' }
})
);
const response = await worker.fetch(
new Request('https://mirror.example/pypi/simple/demo/'),
{},
executionContext
);
const body = await response.text();
expect(response.status).toBe(200);
expect(body).toContain('https://mirror.example/pypi/files/packages/demo.whl');
expect(response.headers.get('Cache-Control')).toBe('no-store');
expect(cacheDefault.put).not.toHaveBeenCalled();
});
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');
});
it('returns Docker registry version metadata for /v2/ probes', async () => {
const response = await worker.fetch(
new Request('https://example.com/v2/'),
{},
executionContext
);
expect(response.status).toBe(200);
expect(response.headers.get('Docker-Distribution-Api-Version')).toBe('registry/2.0');
expect(response.headers.get('X-Performance-Metrics')).toBeNull();
expect(await response.text()).toBe('{}');
});
it('redirects unknown platforms and bare platform prefixes to the homepage', async () => {
const unknownPlatform = await worker.fetch(
new Request('https://example.com/not-a-platform/resource'),
{},
executionContext
);
const barePlatform = await worker.fetch(
new Request('https://example.com/gh/', { method: 'GET' }),
{},
executionContext
);
expect(unknownPlatform.status).toBe(302);
expect(unknownPlatform.headers.get('Location')).toBe('https://github.com/xixu-me/Xget');
expect(barePlatform.status).toBe(302);
expect(barePlatform.headers.get('Location')).toBe('https://github.com/xixu-me/Xget');
});
it('rejects Docker requests that do not use a /cr/ prefix', async () => {
const response = await worker.fetch(
new Request('https://example.com/v2/library/nginx/manifests/latest'),
{},
executionContext
);
expect(response.status).toBe(400);
expect(await response.text()).toContain('/cr/ prefix');
});
it('rejects disallowed CORS preflight methods before proxying upstream', async () => {
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo', {
method: 'OPTIONS',
headers: {
Origin: 'https://app.example.com',
'Access-Control-Request-Method': 'POST'
}
}),
{ ALLOWED_ORIGINS: 'https://app.example.com' },
executionContext
);
expect(response.status).toBe(405);
expect(await response.text()).toBe('Method not allowed');
});
it('serves cached responses without proxying upstream', async () => {
cacheDefault.match.mockResolvedValueOnce(
new Response('cached-body', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('should-not-run', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt'),
{},
executionContext
);
const metrics = JSON.parse(response.headers.get('X-Performance-Metrics') || '{}');
expect(response.status).toBe(200);
expect(await response.text()).toBe('cached-body');
expect(metrics).toHaveProperty('cache_hit');
expect(fetchSpy).not.toHaveBeenCalled();
});
it('reuses cached full content for range requests when a ranged entry is absent', async () => {
cacheDefault.match.mockResolvedValueOnce(null).mockResolvedValueOnce(
new Response('full-body', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('should-not-run', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt', {
headers: { Range: 'bytes=0-3' }
}),
{},
executionContext
);
const metrics = JSON.parse(response.headers.get('X-Performance-Metrics') || '{}');
expect(response.status).toBe(200);
expect(await response.text()).toBe('full-body');
expect(metrics).toHaveProperty('cache_hit_full_content');
expect(fetchSpy).not.toHaveBeenCalled();
});
it('falls back to upstream fetch when cache lookup throws', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
cacheDefault.match.mockRejectedValueOnce(new Error('cache-down'));
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('ok', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt'),
{},
executionContext
);
expect(response.status).toBe(200);
expect(fetchSpy).toHaveBeenCalledTimes(1);
expect(warnSpy).toHaveBeenCalledWith('Cache API unavailable:', expect.any(Error));
});
it('configures Git passthrough headers for upload-pack requests', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('', {
status: 200,
headers: { 'Content-Type': 'application/x-git-upload-pack-result' }
})
);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo.git/git-upload-pack', {
method: 'POST'
}),
{},
executionContext
);
const upstreamHeaders = new Headers(fetchSpy.mock.calls[0][1]?.headers);
expect(response.status).toBe(200);
expect(upstreamHeaders.get('User-Agent')).toBe('git/2.34.1');
expect(upstreamHeaders.get('Content-Type')).toBe('application/x-git-upload-pack-request');
});
it('derives HEAD content length from a range probe when the upstream omits it', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
if (init?.method === 'HEAD') {
return new Response(null, {
status: 200,
headers: { 'Content-Type': 'text/plain' }
});
}
expect(new Headers(init?.headers).get('Range')).toBe('bytes=0-0');
return new Response(null, {
status: 206,
headers: { 'Content-Range': 'bytes 0-0/123' }
});
});
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt', { method: 'HEAD' }),
{},
executionContext
);
expect(response.status).toBe(200);
expect(response.headers.get('Content-Length')).toBe('123');
expect(fetchSpy).toHaveBeenCalledTimes(2);
});
it('uses a successful GET probe to recover missing HEAD content length', async () => {
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
if (init?.method === 'HEAD') {
return new Response(null, {
status: 200,
headers: { 'Content-Type': 'text/plain' }
});
}
return new Response(null, {
status: 200,
headers: { 'Content-Length': '321' }
});
});
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt', { method: 'HEAD' }),
{},
executionContext
);
expect(response.status).toBe(200);
expect(response.headers.get('Content-Length')).toBe('321');
});
it('wraps upstream client errors in detailed JSON responses', async () => {
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('teapot', {
status: 418,
headers: { 'Content-Type': 'text/plain' }
})
);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt'),
{ MAX_RETRIES: '1', RETRY_DELAY_MS: '0' },
executionContext
);
const body = await response.json();
expect(response.status).toBe(418);
expect(body.error).toContain('Upstream server error (418): teapot');
});
it('retries upstream 5xx responses before succeeding', async () => {
const fetchSpy = vi
.spyOn(globalThis, 'fetch')
.mockResolvedValueOnce(
new Response('busy', {
status: 503,
headers: { 'Content-Type': 'text/plain' }
})
)
.mockResolvedValueOnce(
new Response('ok', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt'),
{ MAX_RETRIES: '2', RETRY_DELAY_MS: '0' },
executionContext
);
expect(response.status).toBe(200);
expect(fetchSpy).toHaveBeenCalledTimes(2);
});
it('retries rejected upstream fetches before succeeding', async () => {
const fetchSpy = vi
.spyOn(globalThis, 'fetch')
.mockRejectedValueOnce(new Error('temporary-network-failure'))
.mockResolvedValueOnce(
new Response('ok', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt'),
{ MAX_RETRIES: '2', RETRY_DELAY_MS: '0' },
executionContext
);
expect(response.status).toBe(200);
expect(fetchSpy).toHaveBeenCalledTimes(2);
});
it('times out requests when the abort timer fires', async () => {
const timeoutToken = { id: 'abort-timeout' };
const clearTimeoutSpy = vi.fn();
vi.stubGlobal(
'setTimeout',
vi.fn((callback, delay) => {
void delay;
callback();
return timeoutToken;
})
);
vi.stubGlobal('clearTimeout', clearTimeoutSpy);
vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
if (init?.signal?.aborted) {
const error = new Error(`Aborted before fetching ${String(input)}`);
error.name = 'AbortError';
throw error;
}
return new Response('unexpected-success', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
});
});
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt'),
{ MAX_RETRIES: '2', RETRY_DELAY_MS: '0', TIMEOUT_SECONDS: '1' },
executionContext
);
expect(response.status).toBe(408);
expect(await response.text()).toBe('Request timeout');
expect(clearTimeoutSpy).toHaveBeenCalledWith(timeoutToken);
});
it('falls back to default retries when retry configuration is invalid', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('ok', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt'),
{ MAX_RETRIES: '-1' },
executionContext
);
expect(response.status).toBe(200);
expect(await response.text()).toBe('ok');
expect(fetchSpy).toHaveBeenCalledTimes(1);
});
it('logs and recovers when request setup throws unexpectedly', async () => {
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
const redirectSpy = vi.spyOn(Response, 'redirect').mockImplementation(() => {
throw new Error('boom');
});
const response = await worker.fetch(new Request('https://example.com/'), {}, executionContext);
expect(response.status).toBe(500);
expect(await response.text()).toBe('Internal Server Error');
expect(errorSpy).toHaveBeenCalledWith('Error handling request:', expect.any(Error));
expect(redirectSpy).toHaveBeenCalled();
});
it('retries Docker requests with an anonymous token and follows redirects on success', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input, init) => {
const url = String(input);
const headers = new Headers(init?.headers);
if (url === 'https://ghcr.io/v2/private/repo/manifests/latest') {
if (!headers.has('Authorization')) {
return new Response('', {
status: 401,
headers: {
'WWW-Authenticate': 'Bearer realm="https://ghcr.io/token",service="ghcr.io"'
}
});
}
expect(headers.get('Authorization')).toBe('Bearer token-123');
return new Response(null, {
status: 302,
headers: { Location: 'https://pkg.example.com/manifest' }
});
}
if (url.startsWith('https://ghcr.io/token')) {
return new Response(JSON.stringify({ token: 'token-123' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
if (url === 'https://pkg.example.com/manifest') {
expect(headers.get('Authorization')).toBeNull();
return new Response('', {
status: 200,
headers: { 'Content-Length': '0' }
});
}
throw new Error(`Unexpected fetch URL: ${url}`);
});
const response = await worker.fetch(
new Request('https://example.com/cr/ghcr/v2/private/repo/manifests/latest', {
headers: { Accept: 'application/vnd.docker.distribution.manifest.v2+json' }
}),
{},
executionContext
);
expect(response.status).toBe(200);
expect(fetchSpy).toHaveBeenCalledTimes(4);
});
it('warns and falls back to a Docker auth challenge when token negotiation fails', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('', {
status: 401,
headers: { 'WWW-Authenticate': 'Bearer realm="https://ghcr.io/token"' }
})
);
const response = await worker.fetch(
new Request('https://example.com/cr/ghcr/v2/private/repo/manifests/latest', {
headers: { Accept: 'application/vnd.docker.distribution.manifest.v2+json' }
}),
{},
executionContext
);
expect(response.status).toBe(401);
expect(response.headers.get('WWW-Authenticate')).toBe(
'Bearer realm="https://example.com/cr/ghcr/v2/auth",service="Xget"'
);
expect(warnSpy).toHaveBeenCalledWith('Token fetch failed:', expect.any(Error));
});
it('returns a ranged response after caching the full upstream body', async () => {
cacheDefault.match
.mockResolvedValueOnce(null)
.mockResolvedValueOnce(null)
.mockResolvedValueOnce(
new Response('xy', {
status: 206,
headers: { 'Content-Range': 'bytes 0-1/6' }
})
);
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('xyz123', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.bin', {
headers: { Range: 'bytes=0-1' }
}),
{},
executionContext
);
const metrics = JSON.parse(response.headers.get('X-Performance-Metrics') || '{}');
expect(response.status).toBe(206);
expect(metrics).toHaveProperty('range_cache_hit_after_full_cache');
});
it('warns when cache writes fail without waitUntil support', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
cacheDefault.put.mockRejectedValueOnce(new Error('cache-put-down'));
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('cached', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt'),
{},
/** @type {ExecutionContext} */ ({})
);
await Promise.resolve();
expect(response.status).toBe(200);
expect(warnSpy).toHaveBeenCalledWith('Cache put failed:', expect.any(Error));
});
it('warns when post-store cache lookups fail for range requests', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
cacheDefault.match
.mockResolvedValueOnce(null)
.mockResolvedValueOnce(null)
.mockRejectedValueOnce(new Error('range-cache-down'));
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response('abcdef', {
status: 200,
headers: { 'Content-Type': 'text/plain' }
})
);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.bin', {
headers: { Range: 'bytes=0-1' }
}),
{},
executionContext
);
expect(response.status).toBe(200);
expect(warnSpy).toHaveBeenCalledWith('Cache put/match failed:', expect.any(Error));
});
it('copies upstream content length from non-standard header objects when needed', async () => {
const upstreamHeaders = {
/**
* Reads an upstream header value.
* @param {string} name
*/
get(name) {
const header = name.toLowerCase();
if (header === 'content-type') {
return 'text/plain';
}
if (header === 'content-length') {
return '777';
}
return null;
},
*[Symbol.iterator]() {
yield ['Content-Type', 'text/plain'];
}
};
const fakeResponse = /** @type {Response} */ ({
body: null,
headers: upstreamHeaders,
ok: true,
status: 200,
statusText: 'OK',
text: async () => 'ok'
});
vi.spyOn(globalThis, 'fetch').mockResolvedValue(fakeResponse);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt'),
{},
executionContext
);
expect(response.status).toBe(200);
expect(response.headers.get('Content-Length')).toBe('777');
});
it('warns when upstream content length cannot be read during response finalization', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const upstreamHeaders = {
/**
* Reads an upstream header value.
* @param {string} name
*/
get(name) {
if (name.toLowerCase() === 'content-type') {
return 'text/plain';
}
throw new Error('content-length unavailable');
},
*[Symbol.iterator]() {
yield ['Content-Type', 'text/plain'];
}
};
const fakeResponse = /** @type {Response} */ ({
body: null,
headers: upstreamHeaders,
ok: true,
status: 200,
statusText: 'OK',
text: async () => 'ok'
});
vi.spyOn(globalThis, 'fetch').mockResolvedValue(fakeResponse);
const response = await worker.fetch(
new Request('https://example.com/gh/user/repo/file.txt'),
{},
executionContext
);
expect(response.status).toBe(200);
expect(warnSpy).toHaveBeenCalledWith('Could not set Content-Length header:', expect.any(Error));
});
});