Skip to content

Commit 881ac57

Browse files
committed
chore(test): init unit tests for oidc client
1 parent 0394a6f commit 881ac57

File tree

6 files changed

+311
-33
lines changed

6 files changed

+311
-33
lines changed
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { afterEach, describe, expect, it, Mock, vi } from 'vitest';
2+
3+
import * as initWorker from '../initWorker';
4+
import { Oidc, OidcAuthorizationServiceConfiguration } from '../oidc';
5+
import { AuthorityConfiguration, OidcConfiguration, TokenAutomaticRenewMode } from '../types';
6+
7+
vi.mock('../initWorker');
8+
9+
describe.only('OIDC service', () => {
10+
const authorityConfigurationMock: AuthorityConfiguration = {
11+
issuer: 'test_issuer',
12+
authorization_endpoint: 'test_authorization_endpoint',
13+
token_endpoint: 'test_token_endpoint',
14+
revocation_endpoint: 'test_revocation_endpoint',
15+
end_session_endpoint: 'test_end_session_endpoint', // optional
16+
userinfo_endpoint: 'test_userinfo_endpoint', // optional
17+
check_session_iframe: 'test_check_session_iframe', // optional
18+
};
19+
20+
const oidcConfigMock: OidcConfiguration = {
21+
client_id: 'test_client_id',
22+
redirect_uri: 'test_redirect_uri',
23+
silent_redirect_uri: 'test_silent_redirect_uri', // optional
24+
silent_login_uri: 'test_silent_login_uri', // optional
25+
silent_login_timeout: 1000, // optional
26+
scope: 'openid tenant_id email profile offline_access',
27+
authority: 'test_authority',
28+
authority_time_cache_wellknowurl_in_second: 1000, // optional
29+
authority_timeout_wellknowurl_in_millisecond: 1000, // optional
30+
authority_configuration: undefined, // optional
31+
refresh_time_before_tokens_expiration_in_second: 1000, // optional
32+
token_automatic_renew_mode: TokenAutomaticRenewMode.AutomaticBeforeTokenExpiration, // optional
33+
token_request_timeout: 1000, // optional
34+
service_worker_relative_url: 'test_service_worker_relative_url', // optional
35+
service_worker_register: vi.fn().mockResolvedValue({} as ServiceWorkerRegistration), // optional
36+
service_worker_keep_alive_path: 'test_service_worker_keep_alive_path', // optional
37+
service_worker_activate: () => true, // optional
38+
service_worker_only: true, // optional
39+
service_worker_convert_all_requests_to_cors: true, // optional
40+
service_worker_update_require_callback: vi.fn().mockResolvedValue(void 0), // optional
41+
extras: {}, // optional
42+
token_request_extras: {}, // optional
43+
// storage?: Storage;
44+
monitor_session: true, // optional
45+
token_renew_mode: 'test_token_renew_mode', // optional
46+
logout_tokens_to_invalidate: ['access_token', 'refresh_token'], // optional
47+
// demonstrating_proof_of_possession: false, // optional
48+
// demonstrating_proof_of_possession_configuration?: DemonstratingProofOfPossessionConfiguration;
49+
preload_user_info: false, // optional
50+
};
51+
52+
const oidcConfigMockWithAuthorityConfiguration: OidcConfiguration = {
53+
...oidcConfigMock,
54+
authority_configuration: authorityConfigurationMock,
55+
};
56+
57+
const fetchMock = vi.fn();
58+
59+
const createStorageMock = (): Storage => {
60+
const storage = {
61+
getItem(key: string) {
62+
const value = this[key];
63+
return typeof value === 'undefined' ? null : value;
64+
},
65+
setItem(key: string, value: unknown) {
66+
this[key] = value;
67+
this.length = Object.keys(this).length - 6; // kind'a ignore mock methods and props
68+
},
69+
removeItem: function (key: string) {
70+
return delete this[key];
71+
},
72+
length: 0,
73+
key: () => {
74+
return null;
75+
},
76+
clear() {
77+
window.localStorage = window.sessionStorage = createStorageMock();
78+
},
79+
};
80+
81+
return storage;
82+
};
83+
84+
window.localStorage = window.sessionStorage = createStorageMock();
85+
86+
afterEach(() => {
87+
vi.clearAllMocks();
88+
89+
window.localStorage.clear();
90+
});
91+
92+
describe('init flow', () => {
93+
it('should create new oidc instance', async () => {
94+
const sut = new Oidc(
95+
oidcConfigMockWithAuthorityConfiguration,
96+
'test_oidc_client_id',
97+
() => fetchMock,
98+
);
99+
100+
expect(sut).toBeDefined();
101+
});
102+
103+
it('should init oidc instance with predefined authority_configuration', async () => {
104+
const sut = new Oidc(
105+
oidcConfigMockWithAuthorityConfiguration,
106+
'test_oidc_client_id',
107+
() => fetchMock,
108+
);
109+
110+
expect(sut.initPromise).toBeDefined();
111+
112+
const result = await sut.initPromise;
113+
114+
expect(sut.initPromise).toBeNull();
115+
116+
expect(result).toEqual(new OidcAuthorizationServiceConfiguration(authorityConfigurationMock));
117+
});
118+
119+
it('should init oidc instance with fetched authority_configuration and enabled service worker', async () => {
120+
fetchMock.mockResolvedValue({
121+
status: 200,
122+
json: vi.fn().mockResolvedValue(authorityConfigurationMock),
123+
});
124+
125+
// we don't care about the return value of initWorker.initWorkerAsync
126+
// as it is used only as boolean flag to set storage to local storage or not
127+
(initWorker.initWorkerAsync as Mock<any, any>).mockResolvedValue({});
128+
129+
const sut = new Oidc(oidcConfigMock, 'test_oidc_client_id', () => fetchMock);
130+
131+
expect(sut.initPromise).toBeDefined();
132+
133+
const result = await sut.initPromise;
134+
135+
expect(result).toEqual(new OidcAuthorizationServiceConfiguration(authorityConfigurationMock));
136+
137+
expect(sut.initPromise).toBeNull();
138+
139+
// oh this side effects... can we avoid them and make it better?
140+
const localCache = JSON.parse(
141+
window.localStorage.getItem(`oidc.server:${oidcConfigMock.authority}`),
142+
).result;
143+
144+
expect(localCache).toEqual(authorityConfigurationMock);
145+
expect(fetchMock).toHaveBeenCalledOnce();
146+
expect(fetchMock).toHaveBeenCalledWith(
147+
'test_authority/.well-known/openid-configuration',
148+
expect.anything(),
149+
);
150+
});
151+
152+
// TODO: cache.ts has second level side-effect, so this test is impacted by previous one
153+
// as it is not possible to refresh/clear that cache at current moment of time
154+
it.skip('should take authority_configuration from local storage on subsequent initAsync calls', async () => {
155+
fetchMock.mockResolvedValue({
156+
status: 200,
157+
json: vi.fn().mockResolvedValue(authorityConfigurationMock),
158+
});
159+
160+
// we don't care about the return value of initWorker.initWorkerAsync
161+
// as it is used only as boolean flag to set storage to local storage or not
162+
(initWorker.initWorkerAsync as Mock<any, any>).mockResolvedValue({});
163+
164+
const sut = new Oidc(oidcConfigMock, 'test_oidc_client_id', () => fetchMock);
165+
166+
await sut.initPromise;
167+
168+
// internal cache.ts makes some wildest magic,
169+
// so any subsequential call could obtain the authority_configuration from internal cache or null
170+
// no other options. Sounds like a bug. What's a point of localStorage/sessionStorage cache then?
171+
expect(fetchMock).toHaveBeenCalledOnce();
172+
173+
// const secondCallResult = await sut.initAsync(oidcConfigMock.authority, null);
174+
175+
// expect(fetchMock).toHaveBeenCalledOnce();
176+
// expect(secondCallResult).toEqual(new OidcAuthorizationServiceConfiguration(authorityConfigurationMock));
177+
});
178+
});
179+
});

packages/oidc-client/src/cache.ts

Lines changed: 91 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,103 @@
1-
const fetchFromIssuerCache = {};
2-
3-
export const getFromCache = (localStorageKey, storage = window.sessionStorage, timeCacheSecond) => {
4-
if (!fetchFromIssuerCache[localStorageKey]) {
5-
if (storage) {
6-
const cacheJson = storage.getItem(localStorageKey);
7-
if (cacheJson) {
8-
fetchFromIssuerCache[localStorageKey] = JSON.parse(cacheJson);
9-
}
10-
}
1+
const fetchFromIssuerCache: Record<string, InternalCacheItem<any>> = {};
2+
3+
type InternalCacheItem<T> = {
4+
result: T;
5+
timestamp: number;
6+
};
7+
8+
const getResultOrNullIfExpired = <T extends object>(
9+
cachedItem: InternalCacheItem<T> | undefined,
10+
timeCacheSecond: number,
11+
): T | null => {
12+
if (!cachedItem) {
13+
return null;
1114
}
15+
1216
const oneHourMinisecond = 1000 * timeCacheSecond;
13-
// @ts-ignore
14-
if (
15-
fetchFromIssuerCache[localStorageKey] &&
16-
fetchFromIssuerCache[localStorageKey].timestamp + oneHourMinisecond > Date.now()
17-
) {
18-
return fetchFromIssuerCache[localStorageKey].result;
17+
if (cachedItem.timestamp + oneHourMinisecond > Date.now()) {
18+
return cachedItem.result as T;
1919
}
20+
2021
return null;
2122
};
2223

23-
export const setCache = (localStorageKey, result, storage = window.sessionStorage) => {
24+
export const getFromCache = <T extends object>(
25+
localStorageKey: string,
26+
storage: Storage = window.sessionStorage,
27+
timeCacheSecond: number,
28+
): T => {
29+
const fromStorage =
30+
storage &&
31+
storage.getItem(localStorageKey) &&
32+
(JSON.parse(storage.getItem(localStorageKey)) as InternalCacheItem<T> | undefined);
33+
34+
const fromLocalStorage = fetchFromIssuerCache[localStorageKey];
35+
36+
return (
37+
getResultOrNullIfExpired<T>(fromStorage, timeCacheSecond) ||
38+
getResultOrNullIfExpired<T>(fromLocalStorage, timeCacheSecond) ||
39+
null
40+
);
41+
};
42+
43+
export const setCache = <T extends object>(
44+
localStorageKey: string,
45+
result: T,
46+
storage: Storage = window.sessionStorage,
47+
): void => {
2448
const timestamp = Date.now();
2549
fetchFromIssuerCache[localStorageKey] = { result, timestamp };
50+
2651
if (storage) {
2752
storage.setItem(localStorageKey, JSON.stringify({ result, timestamp }));
2853
}
2954
};
55+
56+
export const clearCache = (
57+
localStorageKey?: string,
58+
storage: Storage = window.sessionStorage,
59+
): void => {
60+
if (!localStorageKey) {
61+
for (const key in fetchFromIssuerCache) {
62+
storage.removeItem(key);
63+
delete fetchFromIssuerCache[localStorageKey];
64+
}
65+
}
66+
delete fetchFromIssuerCache[localStorageKey];
67+
storage.removeItem(localStorageKey);
68+
};
69+
70+
// // TODO: refactor this function to be less side-effecty
71+
// // getFromCache has a secrec internal side-effect, which keeps fetchFromIssuer inside internal object
72+
// // which leads to case when cache is never retrieved from storage, but just returned from internal object
73+
// // and even more, if object is expired, but exists in internal object, function will return symple null for ever.
74+
// // only way to get actual data - setCache with same key to override timestamp
75+
// export const getFromCache = (localStorageKey, storage = window.sessionStorage, timeCacheSecond) => {
76+
// if (!fetchFromIssuerCache[localStorageKey]) {
77+
// if (storage) {
78+
// const cacheJson = storage.getItem(localStorageKey);
79+
// if (cacheJson) {
80+
// fetchFromIssuerCache[localStorageKey] = JSON.parse(cacheJson);
81+
// }
82+
// }
83+
// }
84+
// const oneHourMinisecond = 1000 * timeCacheSecond;
85+
// // @ts-ignore
86+
// if (
87+
// fetchFromIssuerCache[localStorageKey] &&
88+
// fetchFromIssuerCache[localStorageKey].timestamp + oneHourMinisecond > Date.now()
89+
// ) {
90+
// return fetchFromIssuerCache[localStorageKey].result;
91+
// }
92+
// return null;
93+
// };
94+
95+
// // what is the point of setting value into storage if it is never accessed later in getFromCache?
96+
// // fetchFromIssuerCache existence prevents access to chached data in storage
97+
// export const setCache = (localStorageKey, result, storage = window.sessionStorage) => {
98+
// const timestamp = Date.now();
99+
// fetchFromIssuerCache[localStorageKey] = { result, timestamp };
100+
// if (storage) {
101+
// storage.setItem(localStorageKey, JSON.stringify({ result, timestamp }));
102+
// }
103+
// };

packages/oidc-client/src/initWorker.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ILOidcLocation } from './location';
2+
import { OidcAuthorizationServiceConfiguration } from './oidc';
23
import { parseOriginalTokens } from './parseTokens.js';
34
import timer from './timer.js';
45
import { OidcConfiguration } from './types.js';
@@ -122,7 +123,7 @@ export const initWorkerAsync = async (configuration, configurationName) => {
122123
return sendMessageAsync(registration)({ type: 'clear', data: { status }, configurationName });
123124
};
124125
const initAsync = async (
125-
oidcServerConfiguration,
126+
oidcServerConfiguration: OidcAuthorizationServiceConfiguration,
126127
where,
127128
oidcConfiguration: OidcConfiguration,
128129
) => {

packages/oidc-client/src/oidc.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { CheckSessionIFrame } from './checkSessionIFrame.js';
33
import { base64urlOfHashOfASCIIEncodingAsync } from './crypto';
44
import { eventNames } from './events.js';
55
import { initSession } from './initSession.js';
6-
import { defaultServiceWorkerUpdateRequireCallback, initWorkerAsync } from './initWorker.js';
6+
import { defaultServiceWorkerUpdateRequireCallback, initWorkerAsync } from './initWorker';
77
import { activateServiceWorker } from './initWorkerOption';
88
import {
99
defaultDemonstratingProofOfPossessionConfiguration,
@@ -37,16 +37,26 @@ export interface OidcAuthorizationServiceConfigurationJson {
3737
issuer: string;
3838
}
3939

40+
export type OidcAuthorizationServiceConfigurationResponse = {
41+
authorization_endpoint: string;
42+
end_session_endpoint: string;
43+
revocation_endpoint: string;
44+
token_endpoint: string;
45+
userinfo_endpoint: string;
46+
check_session_iframe: string;
47+
issuer: string;
48+
};
49+
4050
export class OidcAuthorizationServiceConfiguration {
41-
private checkSessionIframe: string;
42-
private issuer: string;
43-
private authorizationEndpoint: string;
44-
private tokenEndpoint: string;
45-
private revocationEndpoint: string;
46-
private userInfoEndpoint: string;
47-
private endSessionEndpoint: string;
48-
49-
constructor(request: any) {
51+
public checkSessionIframe: string;
52+
public issuer: string;
53+
public authorizationEndpoint: string;
54+
public tokenEndpoint: string;
55+
public revocationEndpoint: string;
56+
public userInfoEndpoint: string;
57+
public endSessionEndpoint: string;
58+
59+
constructor(request: OidcAuthorizationServiceConfigurationResponse) {
5060
this.authorizationEndpoint = request.authorization_endpoint;
5161
this.tokenEndpoint = request.token_endpoint;
5262
this.revocationEndpoint = request.revocation_endpoint;
@@ -234,8 +244,11 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
234244
}
235245
}
236246

237-
initPromise = null;
238-
async initAsync(authority: string, authorityConfiguration: AuthorityConfiguration) {
247+
initPromise: null | Promise<OidcAuthorizationServiceConfiguration> = null;
248+
async initAsync(
249+
authority: string,
250+
authorityConfiguration?: AuthorityConfiguration,
251+
): Promise<OidcAuthorizationServiceConfiguration> {
239252
if (this.initPromise !== null) {
240253
return this.initPromise;
241254
}

0 commit comments

Comments
 (0)