Skip to content

Commit 2d97dd8

Browse files
committed
chore(test): init unit tests for oidc client
1 parent 32a2af3 commit 2d97dd8

File tree

5 files changed

+205
-11
lines changed

5 files changed

+205
-11
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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
const fetchFromIssuerCache = {};
22

3+
// TODO: refactor this function to be less side-effecty
4+
// getFromCache has a secrec internal side-effect, which keeps fetchFromIssuer inside internal object
5+
// which leads to case when cache is never retrieved from storage, but just returned from internal object
6+
// and even more, if object is expired, but exists in internal object, function will return symple null for ever.
7+
// only way to get actual data - setCache with same key to override timestamp
38
export const getFromCache = (localStorageKey, storage = window.sessionStorage, timeCacheSecond) => {
49
if (!fetchFromIssuerCache[localStorageKey]) {
510
if (storage) {
@@ -20,6 +25,8 @@ export const getFromCache = (localStorageKey, storage = window.sessionStorage, t
2025
return null;
2126
};
2227

28+
// what is the point of setting value into storage if it is never accessed later in getFromCache?
29+
// fetchFromIssuerCache existence prevents access to chached data in storage
2330
export const setCache = (localStorageKey, result, storage = window.sessionStorage) => {
2431
const timestamp = Date.now();
2532
fetchFromIssuerCache[localStorageKey] = { result, timestamp };

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: 13 additions & 10 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,
@@ -38,13 +38,13 @@ export interface OidcAuthorizationServiceConfigurationJson {
3838
}
3939

4040
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;
41+
public checkSessionIframe: string;
42+
public issuer: string;
43+
public authorizationEndpoint: string;
44+
public tokenEndpoint: string;
45+
public revocationEndpoint: string;
46+
public userInfoEndpoint: string;
47+
public endSessionEndpoint: string;
4848

4949
constructor(request: any) {
5050
this.authorizationEndpoint = request.authorization_endpoint;
@@ -234,8 +234,11 @@ Please checkout that you are using OIDC hook inside a <OidcProvider configuratio
234234
}
235235
}
236236

237-
initPromise = null;
238-
async initAsync(authority: string, authorityConfiguration: AuthorityConfiguration) {
237+
initPromise: null | Promise<OidcAuthorizationServiceConfiguration> = null;
238+
async initAsync(
239+
authority: string,
240+
authorityConfiguration?: AuthorityConfiguration,
241+
): Promise<OidcAuthorizationServiceConfiguration> {
239242
if (this.initPromise !== null) {
240243
return this.initPromise;
241244
}

packages/oidc-client/vite.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,8 @@ export default defineConfig({
1818
resolve: {
1919
preserveSymlinks: true, // https://github.com/vitejs/vite/issues/11657
2020
},
21+
test: {
22+
globals: true,
23+
environment: 'jsdom',
24+
},
2125
});

0 commit comments

Comments
 (0)