Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

152 lines
5.4 KiB
JavaScript

/**
* Tests for security/safe-fetch.js
*
* Tests the safeFetch wrapper that validates URLs before making
* fetch requests, blocking unsafe external URLs.
*/
// Load url-validator first (safe-fetch depends on it)
import '@js/security/url-validator.js';
import '@js/security/safe-fetch.js';
// safeFetchWithAuth reuses the shared 401 helpers exposed on window.api
import '@js/services/api.js';
describe('safeFetch', () => {
const originalFetch = globalThis.fetch;
beforeEach(() => {
globalThis.fetch = vi.fn(() =>
Promise.resolve(new Response('ok', { status: 200 }))
);
});
afterEach(() => {
globalThis.fetch = originalFetch;
});
it('allows internal URLs starting with /', async () => {
await window.safeFetch('/api/research/1');
expect(globalThis.fetch).toHaveBeenCalledWith('/api/research/1', {});
});
it('allows internal URLs with custom options', async () => {
const opts = { method: 'POST', body: '{}' };
await window.safeFetch('/api/start', opts);
expect(globalThis.fetch).toHaveBeenCalledWith('/api/start', opts);
});
it('allows safe external URLs (https)', async () => {
await window.safeFetch('https://example.com/api');
expect(globalThis.fetch).toHaveBeenCalled();
});
it('allows safe external URLs (http)', async () => {
await window.safeFetch('http://example.com/api');
expect(globalThis.fetch).toHaveBeenCalled();
});
it('blocks javascript: URLs', async () => {
await expect(window.safeFetch('javascript:alert(1)'))
.rejects.toThrow('Blocked unsafe URL');
});
it('blocks data: URLs', async () => {
await expect(window.safeFetch('data:text/html,<h1>xss</h1>'))
.rejects.toThrow('Blocked unsafe URL');
});
it('blocks vbscript: URLs', async () => {
await expect(window.safeFetch('vbscript:msgbox'))
.rejects.toThrow('Blocked unsafe URL');
});
it('does not call fetch for blocked URLs', async () => {
try {
await window.safeFetch('javascript:void(0)');
} catch {}
expect(globalThis.fetch).not.toHaveBeenCalled();
});
});
describe('safeFetchWithAuth', () => {
const originalFetch = globalThis.fetch;
const originalLocation = window.location;
beforeEach(() => {
// Writable stub so the redirect assignment doesn't navigate the runner.
delete window.location;
window.location = {
href: 'http://localhost/library/collections',
pathname: '/library/collections',
search: '',
hash: '',
};
});
afterEach(() => {
globalThis.fetch = originalFetch;
window.location = originalLocation;
});
it('returns the Response unchanged on 200 (no redirect)', async () => {
globalThis.fetch = vi.fn(() =>
Promise.resolve(new Response('{"ok":true}', { status: 200 }))
);
const response = await window.safeFetchWithAuth('/library/api/collections');
expect(response.status).toBe(200);
expect(window.location.href).toBe('http://localhost/library/collections');
});
it('redirects to /auth/login with next= on 401 for an internal URL', async () => {
globalThis.fetch = vi.fn(() =>
Promise.resolve(new Response('{"error":"auth"}', { status: 401 }))
);
// On the redirect path the call returns a never-resolving Promise.
const result = await Promise.race([
window.safeFetchWithAuth('/library/api/collections'),
new Promise((resolve) => setTimeout(() => resolve('timeout'), 50)),
]);
expect(result).toBe('timeout'); // never resolved
expect(window.location.href).toBe(
`/auth/login?next=${encodeURIComponent('/library/collections')}`
);
});
it('returns the Response unchanged on a non-401 error (no redirect)', async () => {
globalThis.fetch = vi.fn(() =>
Promise.resolve(new Response('{"error":"boom"}', { status: 500 }))
);
const response = await window.safeFetchWithAuth('/library/api/collections');
expect(response.status).toBe(500);
expect(window.location.href).toBe('http://localhost/library/collections');
});
it('does not redirect on 401 from an external URL', async () => {
globalThis.fetch = vi.fn(() =>
Promise.resolve(new Response('', { status: 401 }))
);
const response = await window.safeFetchWithAuth('https://api.example.com/foo');
expect(response.status).toBe(401);
expect(window.location.href).toBe('http://localhost/library/collections');
});
it('does not redirect on 401 while already on an /auth/* page', async () => {
window.location.pathname = '/auth/login';
window.location.href = 'http://localhost/auth/login';
globalThis.fetch = vi.fn(() =>
Promise.resolve(new Response('', { status: 401 }))
);
const response = await window.safeFetchWithAuth('/library/api/collections');
expect(response.status).toBe(401);
expect(window.location.href).toBe('http://localhost/auth/login');
});
it('still validates URLs (blocks javascript:)', async () => {
await expect(
window.safeFetchWithAuth('javascript:alert(1)')
).rejects.toThrow('Blocked unsafe URL');
});
});