Files
dashy/tests/unit/request-auth.test.js
2026-07-02 12:51:15 +01:00

145 lines
5.0 KiB
JavaScript

import {
describe, it, expect, beforeEach, afterEach, vi,
} from 'vitest';
const part = (value) => Buffer.from(JSON.stringify(value)).toString('base64url');
const jwt = (claims) => `${part({ alg: 'RS256' })}.${part(claims)}.sig`;
const goodToken = () => jwt({ exp: Math.floor(Date.now() / 1000) + 60 });
const expiredToken = () => jwt({ exp: Math.floor(Date.now() / 1000) - 60 });
async function loadRequest({
oidc = false,
keycloak = false,
renew = vi.fn(),
basic = { headers: { Authorization: 'Basic abc' } },
} = {}) {
vi.resetModules();
vi.doMock('@/utils/auth/Auth', () => ({ makeBasicAuthHeaders: vi.fn(() => basic) }));
vi.doMock('@/utils/auth/OidcAuth', () => ({
isOidcEnabled: vi.fn(() => oidc),
getOidcAuth: vi.fn(() => ({ renewForApiRequest: renew })),
}));
vi.doMock('@/utils/auth/KeycloakAuth', () => ({
isKeycloakEnabled: vi.fn(() => keycloak),
getKeycloakAuth: vi.fn(() => ({ renewForApiRequest: renew })),
}));
vi.doMock('@/utils/logging/CoolConsole', () => ({
statusMsg: vi.fn(),
statusErrorMsg: vi.fn(),
}));
return (await import('@/utils/request')).default;
}
describe('request auth headers', () => {
beforeEach(() => {
vi.restoreAllMocks();
localStorage.getItem.mockReset();
global.fetch = vi.fn(() => Promise.resolve(new Response('{"ok":true}', { status: 200 })));
});
afterEach(() => {
vi.doUnmock('@/utils/auth/Auth');
vi.doUnmock('@/utils/auth/OidcAuth');
vi.doUnmock('@/utils/auth/KeycloakAuth');
vi.doUnmock('@/utils/logging/CoolConsole');
});
it('keeps Basic fallback for non-SSO local requests', async () => {
localStorage.getItem.mockReturnValue(null);
const request = await loadRequest();
await request.get('/status-check');
expect(fetch).toHaveBeenCalledWith('/status-check', expect.objectContaining({
headers: { Authorization: 'Basic abc' },
}));
});
it('does not fall back to Basic when OIDC is enabled and bearer is unusable', async () => {
localStorage.getItem.mockReturnValue(expiredToken());
const request = await loadRequest({ oidc: true });
await request.get('/status-check');
expect(fetch).toHaveBeenCalledWith('/status-check', expect.objectContaining({
headers: {},
}));
});
it('sends bearer for SSO when a usable token exists', async () => {
const token = goodToken();
localStorage.getItem.mockReturnValue(token);
const request = await loadRequest({ oidc: true });
await request.get('/status-check');
expect(fetch).toHaveBeenCalledWith('/status-check', expect.objectContaining({
headers: { Authorization: `Bearer ${token}` },
}));
});
it('renews once and retries a local SSO request after 401', async () => {
let token = expiredToken();
const fresh = goodToken();
localStorage.getItem.mockImplementation(() => token);
fetch
.mockResolvedValueOnce(new Response('Unauthorized', { status: 401 }))
.mockResolvedValueOnce(new Response('{"ok":true}', { status: 200 }));
const renew = vi.fn(async () => {
token = fresh;
return true;
});
const request = await loadRequest({ oidc: true, renew });
await request.get('/status-check');
expect(renew).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenNthCalledWith(1, '/status-check', expect.objectContaining({
headers: {},
}));
expect(fetch).toHaveBeenNthCalledWith(2, '/status-check', expect.objectContaining({
headers: { Authorization: `Bearer ${fresh}` },
}));
});
it('does not renew again when the retried request still returns 401', async () => {
localStorage.getItem.mockImplementation(() => expiredToken());
fetch
.mockResolvedValueOnce(new Response('Unauthorized', { status: 401 }))
.mockResolvedValueOnce(new Response('Unauthorized', { status: 401 }));
const renew = vi.fn(async () => true);
const request = await loadRequest({ oidc: true, renew });
await expect(request.get('/status-check')).rejects.toThrow();
expect(renew).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenCalledTimes(2);
});
it('does not retry when renewal fails', async () => {
localStorage.getItem.mockImplementation(() => expiredToken());
fetch.mockResolvedValueOnce(new Response('Unauthorized', { status: 401 }));
const renew = vi.fn(async () => false);
const request = await loadRequest({ oidc: true, renew });
await expect(request.get('/status-check')).rejects.toThrow();
expect(renew).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenCalledTimes(1);
});
it('never attaches a bearer or renews for external requests', async () => {
localStorage.getItem.mockReturnValue(goodToken());
const renew = vi.fn();
fetch.mockResolvedValueOnce(new Response('Unauthorized', { status: 401 }));
const request = await loadRequest({ oidc: true, renew });
await expect(request.get('https://api.example.com/data')).rejects.toThrow();
expect(renew).not.toHaveBeenCalled();
expect(fetch).toHaveBeenCalledWith('https://api.example.com/data', expect.objectContaining({
headers: {},
}));
});
});