Refactor app instance to Keycloak SSO
This commit is contained in:
116
app-instance/frontend/lib/keycloak-oidc.test.ts
Normal file
116
app-instance/frontend/lib/keycloak-oidc.test.ts
Normal file
@ -0,0 +1,116 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user