103 lines
3.5 KiB
TypeScript
103 lines
3.5 KiB
TypeScript
import nock from 'nock';
|
|
|
|
process.env.KEYCLOAK_BASE_URL = 'http://keycloak.test';
|
|
process.env.KEYCLOAK_REALM = 'scouthub';
|
|
process.env.KEYCLOAK_ORG_SERVICE_CLIENT_ID = 'test-client';
|
|
process.env.KEYCLOAK_ORG_SERVICE_CLIENT_SECRET = 'test-secret';
|
|
process.env.DATABASE_URL = 'postgresql://user:pass@localhost:5432/scouthub_home_test';
|
|
process.env.FRONTEND_BASE_URL = 'http://localhost:4200';
|
|
|
|
import { getAccessToken, resetTokenCache } from '../../../src/keycloak-admin/tokenManager';
|
|
|
|
const KEYCLOAK_HOST = 'http://keycloak.test';
|
|
const TOKEN_PATH = '/realms/scouthub/protocol/openid-connect/token';
|
|
|
|
function mockTokenEndpoint(accessToken: string, expiresInSeconds: number) {
|
|
return nock(KEYCLOAK_HOST)
|
|
.post(TOKEN_PATH)
|
|
.reply(200, { access_token: accessToken, expires_in: expiresInSeconds, token_type: 'Bearer' });
|
|
}
|
|
|
|
// Il modulo calcola la scadenza con Date.now(): per simulare il passare del
|
|
// tempo mockiamo solo Date.now (non i timer), così le richieste HTTP mockate
|
|
// da nock continuano a risolversi normalmente sull'event loop reale.
|
|
let nowSpy: jest.SpyInstance<number, []> | undefined;
|
|
|
|
function advanceTimeBy(ms: number): void {
|
|
const current = nowSpy ? (nowSpy.getMockImplementation()?.() ?? Date.now()) : Date.now();
|
|
nowSpy = jest.spyOn(Date, 'now').mockReturnValue(current + ms);
|
|
}
|
|
|
|
beforeEach(() => {
|
|
resetTokenCache();
|
|
});
|
|
|
|
afterEach(() => {
|
|
nowSpy?.mockRestore();
|
|
nowSpy = undefined;
|
|
nock.cleanAll();
|
|
});
|
|
|
|
describe('keycloak-admin tokenManager', () => {
|
|
test("ottiene un access token dall'endpoint client_credentials", async () => {
|
|
const scope = mockTokenEndpoint('token-1', 300);
|
|
|
|
const token = await getAccessToken();
|
|
|
|
expect(token).toBe('token-1');
|
|
expect(scope.isDone()).toBe(true);
|
|
});
|
|
|
|
test('riutilizza il token in cache invece di richiederne uno nuovo ad ogni chiamata', async () => {
|
|
const scope = mockTokenEndpoint('token-1', 300);
|
|
|
|
const first = await getAccessToken();
|
|
const second = await getAccessToken();
|
|
const third = await getAccessToken();
|
|
|
|
expect(first).toBe('token-1');
|
|
expect(second).toBe('token-1');
|
|
expect(third).toBe('token-1');
|
|
// Un solo interceptor registrato (consumato una volta sola): se il codice
|
|
// avesse richiesto un nuovo token per ogni chiamata, la seconda/terza
|
|
// sarebbero fallite per mancanza di un match su nock.
|
|
expect(scope.isDone()).toBe(true);
|
|
});
|
|
|
|
test('rinnova il token quando è vicino alla scadenza (entro il margine di sicurezza)', async () => {
|
|
mockTokenEndpoint('token-1', 300);
|
|
await getAccessToken();
|
|
|
|
// 295s dopo: mancano 5s alla scadenza reale, sotto al margine di sicurezza di 10s.
|
|
advanceTimeBy(295_000);
|
|
|
|
const renewalScope = mockTokenEndpoint('token-2', 300);
|
|
const renewed = await getAccessToken();
|
|
|
|
expect(renewed).toBe('token-2');
|
|
expect(renewalScope.isDone()).toBe(true);
|
|
});
|
|
|
|
test('non rinnova il token se la scadenza è ancora lontana', async () => {
|
|
mockTokenEndpoint('token-1', 300);
|
|
await getAccessToken();
|
|
|
|
advanceTimeBy(60_000); // ben dentro la validità dei 300s
|
|
|
|
const token = await getAccessToken();
|
|
|
|
expect(token).toBe('token-1');
|
|
});
|
|
|
|
test('richieste concorrenti senza token in cache generano una sola chiamata HTTP', async () => {
|
|
const scope = mockTokenEndpoint('token-1', 300);
|
|
|
|
const [a, b, c] = await Promise.all([getAccessToken(), getAccessToken(), getAccessToken()]);
|
|
|
|
expect(a).toBe('token-1');
|
|
expect(b).toBe('token-1');
|
|
expect(c).toBe('token-1');
|
|
expect(scope.isDone()).toBe(true);
|
|
});
|
|
});
|