Files
beaver_project/app-instance/frontend/lib/keycloak-oidc.test.ts

117 lines
4.1 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import {
KEYCLOAK_LOGIN_STATE_KEY,
KEYCLOAK_LOGOUT_IN_PROGRESS_KEY,
buildKeycloakAuthorizeUrl,
buildKeycloakLogoutUrl,
clearKeycloakLogoutInProgress,
createCodeChallenge,
isKeycloakLogoutInProgress,
markKeycloakLogoutInProgress,
parseAuthCallbackUrl,
} from './keycloak-oidc';
function createStorage() {
const data = new Map<string, string>();
return {
getItem: (key: string) => data.get(key) ?? null,
setItem: (key: string, value: string) => data.set(key, value),
removeItem: (key: string) => data.delete(key),
clear: () => data.clear(),
};
}
const testSessionStorage = createStorage();
const testWindow = {
location: new URL('http://172.19.0.245:18080/login'),
sessionStorage: testSessionStorage,
crypto: globalThis.crypto,
};
Object.defineProperty(globalThis, 'window', {
configurable: true,
value: testWindow,
});
Object.defineProperty(globalThis, 'sessionStorage', {
configurable: true,
value: testSessionStorage,
});
function setWindowLocation(url: string) {
testWindow.location = new URL(url);
}
describe('keycloak oidc helpers', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-06-15T08:00:00.000Z'));
testSessionStorage.clear();
setWindowLocation('http://172.19.0.245:18080/login');
});
afterEach(() => {
vi.useRealTimers();
testSessionStorage.clear();
});
test('builds a Keycloak authorization URL using code flow with PKCE and nonce', () => {
const url = new URL(buildKeycloakAuthorizeUrl({
state: 'state-1',
nonce: 'nonce-1',
codeChallenge: 'challenge-1',
nextPath: '/files',
}));
expect(url.origin + url.pathname).toBe('https://keycloak.bwgdi.com/realms/beaver/protocol/openid-connect/auth');
expect(url.searchParams.get('client_id')).toBe('beaver-agnet');
expect(url.searchParams.get('response_type')).toBe('code');
expect(url.searchParams.get('scope')).toBe('openid profile email');
expect(url.searchParams.get('redirect_uri')).toBe('http://172.19.0.245:18080/auth/callback');
expect(url.searchParams.get('state')).toBe('state-1');
expect(url.searchParams.get('nonce')).toBe('nonce-1');
expect(url.searchParams.get('code_challenge')).toBe('challenge-1');
expect(url.searchParams.get('code_challenge_method')).toBe('S256');
});
test('creates the RFC 7636 S256 challenge without requiring https WebCrypto', async () => {
const challenge = await createCodeChallenge('dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk');
expect(challenge).toBe('E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM');
});
test('builds a logout URL with post_logout_redirect_uri and id_token_hint', () => {
const url = new URL(buildKeycloakLogoutUrl('id-token-1'));
expect(url.origin + url.pathname).toBe('https://keycloak.bwgdi.com/realms/beaver/protocol/openid-connect/logout');
expect(url.searchParams.get('client_id')).toBe('beaver-agnet');
expect(url.searchParams.get('id_token_hint')).toBe('id-token-1');
expect(url.searchParams.get('post_logout_redirect_uri')).toBe('http://172.19.0.245:18080/logout/callback');
});
test('tracks logout progress briefly to prevent immediate login restart', () => {
expect(isKeycloakLogoutInProgress()).toBe(false);
markKeycloakLogoutInProgress();
expect(testSessionStorage.getItem(KEYCLOAK_LOGOUT_IN_PROGRESS_KEY)).toBe('1781510400000');
expect(isKeycloakLogoutInProgress()).toBe(true);
vi.advanceTimersByTime(121_000);
expect(isKeycloakLogoutInProgress()).toBe(false);
});
test('parses callback code and state from the current URL', () => {
setWindowLocation('http://172.19.0.245:18080/auth/callback?code=abc&state=xyz');
expect(parseAuthCallbackUrl()).toEqual({ code: 'abc', state: 'xyz', error: '', errorDescription: '' });
});
test('exports stable session storage keys', () => {
expect(KEYCLOAK_LOGIN_STATE_KEY).toBe('beaver_keycloak_login_state');
clearKeycloakLogoutInProgress();
expect(testSessionStorage.getItem(KEYCLOAK_LOGOUT_IN_PROGRESS_KEY)).toBeNull();
});
});