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
153 lines
5.8 KiB
JavaScript
153 lines
5.8 KiB
JavaScript
/**
|
|
* Tests for services/api.js — fetchWithErrorHandling 401 -> login redirect.
|
|
*
|
|
* fetchWithErrorHandling is the shared entry point for the core API surface
|
|
* (research start/status/details/logs/history/report, config saves, deletes).
|
|
* It throws on any non-2xx, so callers never see a bare 401. These tests lock
|
|
* in the behavior that an expired session on an internal API call bounces the
|
|
* user to /auth/login (preserving where they were) instead of surfacing an
|
|
* opaque "API Error: 401", while the /auth/* flow and external URLs are left
|
|
* untouched to avoid redirect loops and false positives.
|
|
*/
|
|
|
|
import '@js/services/api.js';
|
|
|
|
const { fetchWithErrorHandling, shouldRedirectToLoginOn401 } = window.api;
|
|
|
|
describe('shouldRedirectToLoginOn401', () => {
|
|
const originalLocation = window.location;
|
|
|
|
beforeEach(() => {
|
|
delete window.location;
|
|
window.location = { href: 'http://localhost/dashboard', pathname: '/dashboard', search: '' };
|
|
});
|
|
|
|
afterEach(() => {
|
|
window.location = originalLocation;
|
|
});
|
|
|
|
it('redirects for an internal API URL', () => {
|
|
expect(shouldRedirectToLoginOn401('/api/history')).toBe(true);
|
|
});
|
|
|
|
it('does not redirect for an external URL', () => {
|
|
expect(shouldRedirectToLoginOn401('https://api.example.com/foo')).toBe(false);
|
|
});
|
|
|
|
it('does not redirect for a protocol-relative (cross-origin) URL', () => {
|
|
expect(shouldRedirectToLoginOn401('//api.example.com/foo')).toBe(false);
|
|
});
|
|
|
|
it('does not redirect for a backslash-bypass URL that resolves cross-origin', () => {
|
|
// `/\evil.com` is normalized by browsers to https://evil.com (cross-origin)
|
|
expect(shouldRedirectToLoginOn401('/\\evil.com')).toBe(false);
|
|
});
|
|
|
|
it('does not redirect for a request to /auth/*', () => {
|
|
expect(shouldRedirectToLoginOn401('/auth/check')).toBe(false);
|
|
});
|
|
|
|
it('does not redirect while already on an /auth/* page', () => {
|
|
window.location.pathname = '/auth/login';
|
|
expect(shouldRedirectToLoginOn401('/api/history')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('fetchWithErrorHandling — 401 handling', () => {
|
|
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/dashboard',
|
|
pathname: '/dashboard',
|
|
search: '',
|
|
hash: '',
|
|
};
|
|
});
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = originalFetch;
|
|
window.location = originalLocation;
|
|
});
|
|
|
|
it('returns parsed JSON on 200 without redirecting', async () => {
|
|
globalThis.fetch = vi.fn(() =>
|
|
Promise.resolve(new Response('{"ok":true}', { status: 200 }))
|
|
);
|
|
const data = await fetchWithErrorHandling('/api/history');
|
|
expect(data).toEqual({ ok: true });
|
|
expect(window.location.href).toBe('http://localhost/dashboard');
|
|
});
|
|
|
|
it('redirects to /auth/login with next= on 401 for an internal URL', async () => {
|
|
globalThis.fetch = vi.fn(() =>
|
|
Promise.resolve(
|
|
new Response('{"error":"Authentication required"}', { status: 401 })
|
|
)
|
|
);
|
|
|
|
// On the redirect path the call returns a never-resolving Promise, so
|
|
// race it against a short timeout to keep the test from hanging.
|
|
const result = await Promise.race([
|
|
fetchWithErrorHandling('/api/history'),
|
|
new Promise((resolve) => setTimeout(() => resolve('timeout'), 50)),
|
|
]);
|
|
|
|
expect(result).toBe('timeout'); // never resolved
|
|
expect(window.location.href).toBe(
|
|
`/auth/login?next=${encodeURIComponent('/dashboard')}`
|
|
);
|
|
});
|
|
|
|
it('preserves the query string and hash in next= (so #logs-style state survives re-login)', async () => {
|
|
window.location.pathname = '/settings';
|
|
window.location.search = '?tab=embeddings';
|
|
window.location.hash = '#logs';
|
|
window.location.href = 'http://localhost/settings?tab=embeddings#logs';
|
|
globalThis.fetch = vi.fn(() =>
|
|
Promise.resolve(new Response('', { status: 401 }))
|
|
);
|
|
|
|
await Promise.race([
|
|
fetchWithErrorHandling('/api/history'),
|
|
new Promise((resolve) => setTimeout(() => resolve('timeout'), 50)),
|
|
]);
|
|
|
|
expect(window.location.href).toBe(
|
|
`/auth/login?next=${encodeURIComponent('/settings?tab=embeddings#logs')}`
|
|
);
|
|
});
|
|
|
|
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('{"error":"bad creds"}', { status: 401 }))
|
|
);
|
|
|
|
await expect(fetchWithErrorHandling('/api/history')).rejects.toThrow();
|
|
expect(window.location.href).toBe('http://localhost/auth/login');
|
|
});
|
|
|
|
it('does not redirect on 401 when the request itself targets /auth/*', async () => {
|
|
globalThis.fetch = vi.fn(() =>
|
|
Promise.resolve(new Response('{"error":"bad creds"}', { status: 401 }))
|
|
);
|
|
|
|
await expect(fetchWithErrorHandling('/auth/check')).rejects.toThrow();
|
|
expect(window.location.href).toBe('http://localhost/dashboard');
|
|
});
|
|
|
|
it('does not redirect on a non-401 error and throws the message', async () => {
|
|
globalThis.fetch = vi.fn(() =>
|
|
Promise.resolve(new Response('{"error":"boom"}', { status: 500 }))
|
|
);
|
|
|
|
await expect(fetchWithErrorHandling('/api/history')).rejects.toThrow('boom');
|
|
expect(window.location.href).toBe('http://localhost/dashboard');
|
|
});
|
|
});
|