gameglue 4.0.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +275 -275
- package/babel.config.cjs +5 -5
- package/coverage/auth.js.html +525 -525
- package/coverage/base.css +224 -224
- package/coverage/block-navigation.js +87 -87
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +175 -175
- package/coverage/index.js.html +309 -309
- package/coverage/lcov-report/auth.js.html +525 -525
- package/coverage/lcov-report/base.css +224 -224
- package/coverage/lcov-report/block-navigation.js +87 -87
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/index.html +175 -175
- package/coverage/lcov-report/index.js.html +309 -309
- package/coverage/lcov-report/listener.js.html +528 -528
- package/coverage/lcov-report/prettify.css +1 -1
- package/coverage/lcov-report/prettify.js +2 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +210 -210
- package/coverage/lcov-report/user.js.html +117 -117
- package/coverage/lcov-report/utils.js.html +117 -117
- package/coverage/lcov.info +391 -391
- package/coverage/listener.js.html +528 -528
- package/coverage/prettify.css +1 -1
- package/coverage/prettify.js +2 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -210
- package/coverage/user.js.html +117 -117
- package/coverage/utils.js.html +117 -117
- package/dist/gg.cjs.js +1 -1
- package/dist/gg.cjs.js.map +1 -1
- package/dist/gg.esm.js +1 -1
- package/dist/gg.esm.js.map +1 -1
- package/dist/gg.umd.js +1 -1
- package/dist/gg.umd.js.map +1 -1
- package/examples/certs/cert.pem +19 -19
- package/examples/certs/key.pem +28 -28
- package/examples/flight-dashboard.html +431 -431
- package/examples/server.js +99 -99
- package/examples/telemetry-validator.html +1410 -1410
- package/jest.config.cjs +33 -33
- package/package.json +56 -56
- package/rollup.config.js +57 -57
- package/src/auth.js +255 -255
- package/src/auth.spec.js +481 -481
- package/src/index.js +168 -168
- package/src/listener.js +196 -193
- package/src/listener.spec.js +598 -598
- package/src/presence_listener.js +112 -112
- package/src/test/fixtures.js +106 -106
- package/src/test/setup.js +51 -51
- package/src/utils.js +63 -63
- package/src/utils.spec.js +78 -78
- package/types/index.d.ts +338 -338
- package/webpack.config.js +15 -15
package/src/auth.spec.js
CHANGED
|
@@ -1,481 +1,481 @@
|
|
|
1
|
-
const { GameGlueAuth } = require('./auth');
|
|
2
|
-
const { storage } = require('./utils');
|
|
3
|
-
const {
|
|
4
|
-
mockConfig,
|
|
5
|
-
validAccessToken,
|
|
6
|
-
expiredAccessToken,
|
|
7
|
-
validRefreshToken,
|
|
8
|
-
createMockJwt
|
|
9
|
-
} = require('./test/fixtures');
|
|
10
|
-
|
|
11
|
-
// Mock the oidc-client-ts module
|
|
12
|
-
jest.mock('oidc-client-ts', () => ({
|
|
13
|
-
OidcClient: jest.fn().mockImplementation(() => ({
|
|
14
|
-
createSigninRequest: jest.fn().mockResolvedValue({ url: 'https://auth.example.com/login' }),
|
|
15
|
-
processSigninResponse: jest.fn().mockResolvedValue({
|
|
16
|
-
access_token: 'new-access-token',
|
|
17
|
-
refresh_token: 'new-refresh-token'
|
|
18
|
-
})
|
|
19
|
-
}))
|
|
20
|
-
}));
|
|
21
|
-
|
|
22
|
-
// Mock fetch for token refresh
|
|
23
|
-
global.fetch = jest.fn();
|
|
24
|
-
|
|
25
|
-
// Suppress console output during tests
|
|
26
|
-
const originalConsoleLog = console.log;
|
|
27
|
-
const originalConsoleError = console.error;
|
|
28
|
-
|
|
29
|
-
describe('GameGlueAuth', () => {
|
|
30
|
-
let auth;
|
|
31
|
-
|
|
32
|
-
beforeEach(() => {
|
|
33
|
-
auth = new GameGlueAuth(mockConfig);
|
|
34
|
-
jest.clearAllMocks();
|
|
35
|
-
jest.useFakeTimers();
|
|
36
|
-
// Suppress console output
|
|
37
|
-
console.log = jest.fn();
|
|
38
|
-
console.error = jest.fn();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
afterEach(() => {
|
|
42
|
-
// Restore console
|
|
43
|
-
console.log = originalConsoleLog;
|
|
44
|
-
console.error = originalConsoleError;
|
|
45
|
-
jest.useRealTimers();
|
|
46
|
-
// Clear storage
|
|
47
|
-
storage.remove('gg-auth-token');
|
|
48
|
-
storage.remove('gg-refresh-token');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe('constructor', () => {
|
|
52
|
-
it('should initialize with correct OIDC settings', () => {
|
|
53
|
-
expect(auth._oidcSettings).toMatchObject({
|
|
54
|
-
authority: 'https://auth.gameglue.gg/realms/GameGlue',
|
|
55
|
-
client_id: 'test-client-id',
|
|
56
|
-
redirect_uri: 'http://localhost:3000/callback',
|
|
57
|
-
response_type: 'code',
|
|
58
|
-
scope: 'openid msfs:read gg:broadcast',
|
|
59
|
-
response_mode: 'fragment',
|
|
60
|
-
filterProtocolClaims: true
|
|
61
|
-
});
|
|
62
|
-
// post_logout_redirect_uri is based on window.location.href which varies by environment
|
|
63
|
-
expect(auth._oidcSettings.post_logout_redirect_uri).toBeDefined();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('should use window.location.href as default redirect_uri', () => {
|
|
67
|
-
const configNoRedirect = { clientId: 'test', scopes: [] };
|
|
68
|
-
const authNoRedirect = new GameGlueAuth(configNoRedirect);
|
|
69
|
-
|
|
70
|
-
// The redirect_uri should be based on window.location.href (varies by environment)
|
|
71
|
-
expect(authNoRedirect._oidcSettings.redirect_uri).toMatch(/^http:\/\/localhost/);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('should handle empty scopes array', () => {
|
|
75
|
-
const configEmptyScopes = { clientId: 'test', scopes: [] };
|
|
76
|
-
const authEmptyScopes = new GameGlueAuth(configEmptyScopes);
|
|
77
|
-
|
|
78
|
-
expect(authEmptyScopes._oidcSettings.scope).toBe('openid ');
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('should handle undefined scopes', () => {
|
|
82
|
-
const configNoScopes = { clientId: 'test' };
|
|
83
|
-
const authNoScopes = new GameGlueAuth(configNoScopes);
|
|
84
|
-
|
|
85
|
-
expect(authNoScopes._oidcSettings.scope).toBe('openid ');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('should remove trailing slashes from redirect_uri', () => {
|
|
89
|
-
const configWithSlash = {
|
|
90
|
-
clientId: 'test',
|
|
91
|
-
redirect_uri: 'http://localhost:3000/callback/'
|
|
92
|
-
};
|
|
93
|
-
const authWithSlash = new GameGlueAuth(configWithSlash);
|
|
94
|
-
|
|
95
|
-
expect(authWithSlash._oidcSettings.redirect_uri).toBe('http://localhost:3000/callback');
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
describe('_setAccessToken / getAccessToken', () => {
|
|
100
|
-
it('should store and retrieve access token', () => {
|
|
101
|
-
auth._setAccessToken(validAccessToken);
|
|
102
|
-
const retrieved = auth.getAccessToken();
|
|
103
|
-
|
|
104
|
-
expect(retrieved).toBe(validAccessToken);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('should set up token refresh timeout when setting token', () => {
|
|
108
|
-
auth._setAccessToken(validAccessToken);
|
|
109
|
-
|
|
110
|
-
// Check that a timeout was scheduled
|
|
111
|
-
expect(jest.getTimerCount()).toBeGreaterThan(0);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('should not set timeout for null token', () => {
|
|
115
|
-
auth._setAccessToken(null);
|
|
116
|
-
|
|
117
|
-
expect(jest.getTimerCount()).toBe(0);
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
describe('_setRefreshToken / _getRefreshToken', () => {
|
|
122
|
-
it('should store and retrieve refresh token', () => {
|
|
123
|
-
auth._setRefreshToken(validRefreshToken);
|
|
124
|
-
const retrieved = auth._getRefreshToken();
|
|
125
|
-
|
|
126
|
-
expect(retrieved).toBe(validRefreshToken);
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
describe('getUser', () => {
|
|
131
|
-
it('should extract user ID from JWT', () => {
|
|
132
|
-
auth._setAccessToken(validAccessToken);
|
|
133
|
-
|
|
134
|
-
const userId = auth.getUser();
|
|
135
|
-
|
|
136
|
-
expect(userId).toBe('user-123');
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('should throw error when not authenticated', () => {
|
|
140
|
-
expect(() => auth.getUser()).toThrow('Not authenticated');
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
describe('_hasValidTokens', () => {
|
|
145
|
-
it('should return false when no token exists', () => {
|
|
146
|
-
const result = auth._hasValidTokens();
|
|
147
|
-
|
|
148
|
-
expect(result).toBe(false);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('should return true for valid non-expired token', () => {
|
|
152
|
-
auth._setAccessToken(validAccessToken);
|
|
153
|
-
|
|
154
|
-
const result = auth._hasValidTokens();
|
|
155
|
-
|
|
156
|
-
expect(result).toBe(true);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('should return false for expired token', () => {
|
|
160
|
-
auth._setAccessToken(expiredAccessToken);
|
|
161
|
-
|
|
162
|
-
const result = auth._hasValidTokens();
|
|
163
|
-
|
|
164
|
-
expect(result).toBe(false);
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
describe('isAuthenticated', () => {
|
|
169
|
-
it('should return false when no token exists', async () => {
|
|
170
|
-
const result = await auth.isAuthenticated();
|
|
171
|
-
|
|
172
|
-
expect(result).toBe(false);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('should return true for valid non-expired token', async () => {
|
|
176
|
-
auth._setAccessToken(validAccessToken);
|
|
177
|
-
|
|
178
|
-
const result = await auth.isAuthenticated();
|
|
179
|
-
|
|
180
|
-
expect(result).toBe(true);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('should return false for expired token', async () => {
|
|
184
|
-
auth._setAccessToken(expiredAccessToken);
|
|
185
|
-
|
|
186
|
-
const result = await auth.isAuthenticated();
|
|
187
|
-
|
|
188
|
-
expect(result).toBe(false);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it('should process callback params if present in URL', async () => {
|
|
192
|
-
window.location.hash = '#state=abc123&code=xyz789';
|
|
193
|
-
|
|
194
|
-
const { OidcClient } = require('oidc-client-ts');
|
|
195
|
-
OidcClient.mockImplementation(() => ({
|
|
196
|
-
createSigninRequest: jest.fn(),
|
|
197
|
-
processSigninResponse: jest.fn().mockResolvedValue({
|
|
198
|
-
access_token: validAccessToken,
|
|
199
|
-
refresh_token: validRefreshToken
|
|
200
|
-
})
|
|
201
|
-
}));
|
|
202
|
-
|
|
203
|
-
const authNew = new GameGlueAuth(mockConfig);
|
|
204
|
-
const result = await authNew.isAuthenticated();
|
|
205
|
-
|
|
206
|
-
expect(result).toBe(true);
|
|
207
|
-
// URL should be cleared
|
|
208
|
-
expect(window.history.replaceState).toHaveBeenCalled();
|
|
209
|
-
});
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
describe('login', () => {
|
|
213
|
-
it('should call createSigninRequest', async () => {
|
|
214
|
-
const createSigninRequest = jest.fn().mockResolvedValue({ url: 'https://auth.example.com/login' });
|
|
215
|
-
const { OidcClient } = require('oidc-client-ts');
|
|
216
|
-
OidcClient.mockImplementation(() => ({
|
|
217
|
-
createSigninRequest,
|
|
218
|
-
processSigninResponse: jest.fn()
|
|
219
|
-
}));
|
|
220
|
-
|
|
221
|
-
const authNew = new GameGlueAuth(mockConfig);
|
|
222
|
-
authNew.login();
|
|
223
|
-
|
|
224
|
-
// Wait for promise to resolve
|
|
225
|
-
await Promise.resolve();
|
|
226
|
-
|
|
227
|
-
expect(createSigninRequest).toHaveBeenCalledWith({ state: { bar: 15 } });
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
describe('logout', () => {
|
|
232
|
-
it('should clear tokens from storage', () => {
|
|
233
|
-
auth._setAccessToken(validAccessToken);
|
|
234
|
-
auth._setRefreshToken(validRefreshToken);
|
|
235
|
-
|
|
236
|
-
auth.logout({ redirect: false });
|
|
237
|
-
|
|
238
|
-
// After remove, storage.get returns undefined in Node environment (in-memory map)
|
|
239
|
-
expect(auth.getAccessToken()).toBeUndefined();
|
|
240
|
-
expect(auth._getRefreshToken()).toBeUndefined();
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('should not clear tokens when redirect: false not passed', () => {
|
|
244
|
-
// We can't easily test the redirect behavior in jsdom, but we can verify
|
|
245
|
-
// the method accepts options and clears tokens regardless
|
|
246
|
-
auth._setAccessToken(validAccessToken);
|
|
247
|
-
auth._setRefreshToken(validRefreshToken);
|
|
248
|
-
|
|
249
|
-
// logout() clears tokens first, then redirects
|
|
250
|
-
auth.logout({ redirect: false });
|
|
251
|
-
|
|
252
|
-
expect(auth.getAccessToken()).toBeUndefined();
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it('should accept options object', () => {
|
|
256
|
-
auth._setAccessToken(validAccessToken);
|
|
257
|
-
|
|
258
|
-
// Should not throw when called with different options
|
|
259
|
-
expect(() => auth.logout({ redirect: false })).not.toThrow();
|
|
260
|
-
});
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
describe('_attemptRefresh', () => {
|
|
264
|
-
it('should call token endpoint with correct parameters', async () => {
|
|
265
|
-
auth._setRefreshToken(validRefreshToken);
|
|
266
|
-
|
|
267
|
-
global.fetch.mockResolvedValueOnce({
|
|
268
|
-
status: 200,
|
|
269
|
-
json: () =>
|
|
270
|
-
Promise.resolve({
|
|
271
|
-
access_token: validAccessToken,
|
|
272
|
-
refresh_token: validRefreshToken
|
|
273
|
-
})
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
await auth._attemptRefresh();
|
|
277
|
-
|
|
278
|
-
expect(global.fetch).toHaveBeenCalledWith(
|
|
279
|
-
'https://auth.gameglue.gg/realms/GameGlue/protocol/openid-connect/token',
|
|
280
|
-
expect.objectContaining({
|
|
281
|
-
method: 'POST',
|
|
282
|
-
headers: {
|
|
283
|
-
'Content-Type': 'application/x-www-form-urlencoded'
|
|
284
|
-
}
|
|
285
|
-
})
|
|
286
|
-
);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it('should update tokens on successful refresh', async () => {
|
|
290
|
-
auth._setRefreshToken(validRefreshToken);
|
|
291
|
-
|
|
292
|
-
const newAccessToken = createMockJwt({ sub: 'user-456' }, 3600);
|
|
293
|
-
const newRefreshToken = createMockJwt({ sub: 'user-456', type: 'refresh' }, 86400);
|
|
294
|
-
|
|
295
|
-
global.fetch.mockResolvedValueOnce({
|
|
296
|
-
status: 200,
|
|
297
|
-
json: () =>
|
|
298
|
-
Promise.resolve({
|
|
299
|
-
access_token: newAccessToken,
|
|
300
|
-
refresh_token: newRefreshToken
|
|
301
|
-
})
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
await auth._attemptRefresh();
|
|
305
|
-
|
|
306
|
-
expect(auth.getAccessToken()).toBe(newAccessToken);
|
|
307
|
-
expect(auth._getRefreshToken()).toBe(newRefreshToken);
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
it('should call refresh callback with access token on successful refresh', async () => {
|
|
311
|
-
const callback = jest.fn();
|
|
312
|
-
auth.onTokenRefreshed(callback);
|
|
313
|
-
auth._setRefreshToken(validRefreshToken);
|
|
314
|
-
|
|
315
|
-
const responseData = {
|
|
316
|
-
access_token: validAccessToken,
|
|
317
|
-
refresh_token: validRefreshToken
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
global.fetch.mockResolvedValueOnce({
|
|
321
|
-
status: 200,
|
|
322
|
-
json: () => Promise.resolve(responseData)
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
await auth._attemptRefresh();
|
|
326
|
-
|
|
327
|
-
expect(callback).toHaveBeenCalledWith(validAccessToken);
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
it('should handle network errors gracefully', async () => {
|
|
331
|
-
auth._setRefreshToken(validRefreshToken);
|
|
332
|
-
global.fetch.mockRejectedValueOnce(new Error('Network error'));
|
|
333
|
-
|
|
334
|
-
// Should not throw
|
|
335
|
-
await expect(auth._attemptRefresh()).resolves.toBeUndefined();
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
it('should not update tokens on non-200 response', async () => {
|
|
339
|
-
auth._setRefreshToken(validRefreshToken);
|
|
340
|
-
const originalAccessToken = auth.getAccessToken();
|
|
341
|
-
|
|
342
|
-
global.fetch.mockResolvedValueOnce({
|
|
343
|
-
status: 400,
|
|
344
|
-
json: () => Promise.resolve({ error: 'invalid_grant' })
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
await auth._attemptRefresh();
|
|
348
|
-
|
|
349
|
-
// Access token should remain unchanged (null in this case)
|
|
350
|
-
expect(auth.getAccessToken()).toBe(originalAccessToken);
|
|
351
|
-
});
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
describe('onTokenRefreshed', () => {
|
|
355
|
-
it('should register callback for token refresh', () => {
|
|
356
|
-
const callback = jest.fn();
|
|
357
|
-
auth.onTokenRefreshed(callback);
|
|
358
|
-
|
|
359
|
-
expect(auth._refreshCallback).toBe(callback);
|
|
360
|
-
});
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
describe('_setTokenRefreshTimeout', () => {
|
|
364
|
-
it('should schedule refresh before token expiration', () => {
|
|
365
|
-
// Token expires in 1 hour (3600 seconds)
|
|
366
|
-
auth._setTokenRefreshTimeout(validAccessToken);
|
|
367
|
-
|
|
368
|
-
// Should have scheduled a timeout
|
|
369
|
-
expect(jest.getTimerCount()).toBe(1);
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
it('should clear previous timeout when setting new one', () => {
|
|
373
|
-
auth._setTokenRefreshTimeout(validAccessToken);
|
|
374
|
-
auth._setTokenRefreshTimeout(validAccessToken);
|
|
375
|
-
|
|
376
|
-
// Should only have one active timeout
|
|
377
|
-
expect(jest.getTimerCount()).toBe(1);
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
it('should not schedule timeout for expired token', () => {
|
|
381
|
-
auth._setTokenRefreshTimeout(expiredAccessToken);
|
|
382
|
-
|
|
383
|
-
// Timer count may be 0 since the timeUntilExp is negative
|
|
384
|
-
// The important thing is it doesn't throw
|
|
385
|
-
});
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
describe('_hasCallbackParams', () => {
|
|
389
|
-
it('should return true when hash contains state and code', () => {
|
|
390
|
-
window.location.hash = '#state=abc123&code=xyz789';
|
|
391
|
-
|
|
392
|
-
expect(auth._hasCallbackParams()).toBe(true);
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
it('should return true when hash contains state and error', () => {
|
|
396
|
-
window.location.hash = '#state=abc123&error=access_denied';
|
|
397
|
-
|
|
398
|
-
expect(auth._hasCallbackParams()).toBe(true);
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
it('should return false when hash does not contain state', () => {
|
|
402
|
-
window.location.hash = '#code=xyz789';
|
|
403
|
-
|
|
404
|
-
expect(auth._hasCallbackParams()).toBe(false);
|
|
405
|
-
});
|
|
406
|
-
|
|
407
|
-
it('should return false when hash is empty', () => {
|
|
408
|
-
window.location.hash = '';
|
|
409
|
-
|
|
410
|
-
expect(auth._hasCallbackParams()).toBe(false);
|
|
411
|
-
});
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
describe('_processCallback', () => {
|
|
415
|
-
it('should process signin response and store tokens', async () => {
|
|
416
|
-
const { OidcClient } = require('oidc-client-ts');
|
|
417
|
-
OidcClient.mockImplementation(() => ({
|
|
418
|
-
processSigninResponse: jest.fn().mockResolvedValue({
|
|
419
|
-
access_token: validAccessToken,
|
|
420
|
-
refresh_token: validRefreshToken
|
|
421
|
-
})
|
|
422
|
-
}));
|
|
423
|
-
|
|
424
|
-
const authNew = new GameGlueAuth(mockConfig);
|
|
425
|
-
await authNew._processCallback();
|
|
426
|
-
|
|
427
|
-
expect(authNew.getAccessToken()).toBe(validAccessToken);
|
|
428
|
-
expect(authNew._getRefreshToken()).toBe(validRefreshToken);
|
|
429
|
-
expect(window.history.replaceState).toHaveBeenCalled();
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
it('should throw on error response', async () => {
|
|
433
|
-
const { OidcClient } = require('oidc-client-ts');
|
|
434
|
-
OidcClient.mockImplementation(() => ({
|
|
435
|
-
processSigninResponse: jest.fn().mockResolvedValue({
|
|
436
|
-
error: 'access_denied',
|
|
437
|
-
access_token: null
|
|
438
|
-
})
|
|
439
|
-
}));
|
|
440
|
-
|
|
441
|
-
const authNew = new GameGlueAuth(mockConfig);
|
|
442
|
-
|
|
443
|
-
await expect(authNew._processCallback()).rejects.toThrow('access_denied');
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
it('should throw when no access token received', async () => {
|
|
447
|
-
const { OidcClient } = require('oidc-client-ts');
|
|
448
|
-
OidcClient.mockImplementation(() => ({
|
|
449
|
-
processSigninResponse: jest.fn().mockResolvedValue({
|
|
450
|
-
access_token: null
|
|
451
|
-
})
|
|
452
|
-
}));
|
|
453
|
-
|
|
454
|
-
const authNew = new GameGlueAuth(mockConfig);
|
|
455
|
-
|
|
456
|
-
await expect(authNew._processCallback()).rejects.toThrow('No access token received');
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
it('should handle concurrent calls (only process once)', async () => {
|
|
460
|
-
const { OidcClient } = require('oidc-client-ts');
|
|
461
|
-
const processSigninResponse = jest.fn().mockResolvedValue({
|
|
462
|
-
access_token: validAccessToken,
|
|
463
|
-
refresh_token: validRefreshToken
|
|
464
|
-
});
|
|
465
|
-
OidcClient.mockImplementation(() => ({
|
|
466
|
-
processSigninResponse
|
|
467
|
-
}));
|
|
468
|
-
|
|
469
|
-
const authNew = new GameGlueAuth(mockConfig);
|
|
470
|
-
|
|
471
|
-
// Call twice concurrently
|
|
472
|
-
await Promise.all([
|
|
473
|
-
authNew._processCallback(),
|
|
474
|
-
authNew._processCallback()
|
|
475
|
-
]);
|
|
476
|
-
|
|
477
|
-
// Should only have been called once due to locking
|
|
478
|
-
expect(processSigninResponse).toHaveBeenCalledTimes(1);
|
|
479
|
-
});
|
|
480
|
-
});
|
|
481
|
-
});
|
|
1
|
+
const { GameGlueAuth } = require('./auth');
|
|
2
|
+
const { storage } = require('./utils');
|
|
3
|
+
const {
|
|
4
|
+
mockConfig,
|
|
5
|
+
validAccessToken,
|
|
6
|
+
expiredAccessToken,
|
|
7
|
+
validRefreshToken,
|
|
8
|
+
createMockJwt
|
|
9
|
+
} = require('./test/fixtures');
|
|
10
|
+
|
|
11
|
+
// Mock the oidc-client-ts module
|
|
12
|
+
jest.mock('oidc-client-ts', () => ({
|
|
13
|
+
OidcClient: jest.fn().mockImplementation(() => ({
|
|
14
|
+
createSigninRequest: jest.fn().mockResolvedValue({ url: 'https://auth.example.com/login' }),
|
|
15
|
+
processSigninResponse: jest.fn().mockResolvedValue({
|
|
16
|
+
access_token: 'new-access-token',
|
|
17
|
+
refresh_token: 'new-refresh-token'
|
|
18
|
+
})
|
|
19
|
+
}))
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
// Mock fetch for token refresh
|
|
23
|
+
global.fetch = jest.fn();
|
|
24
|
+
|
|
25
|
+
// Suppress console output during tests
|
|
26
|
+
const originalConsoleLog = console.log;
|
|
27
|
+
const originalConsoleError = console.error;
|
|
28
|
+
|
|
29
|
+
describe('GameGlueAuth', () => {
|
|
30
|
+
let auth;
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
auth = new GameGlueAuth(mockConfig);
|
|
34
|
+
jest.clearAllMocks();
|
|
35
|
+
jest.useFakeTimers();
|
|
36
|
+
// Suppress console output
|
|
37
|
+
console.log = jest.fn();
|
|
38
|
+
console.error = jest.fn();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
// Restore console
|
|
43
|
+
console.log = originalConsoleLog;
|
|
44
|
+
console.error = originalConsoleError;
|
|
45
|
+
jest.useRealTimers();
|
|
46
|
+
// Clear storage
|
|
47
|
+
storage.remove('gg-auth-token');
|
|
48
|
+
storage.remove('gg-refresh-token');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('constructor', () => {
|
|
52
|
+
it('should initialize with correct OIDC settings', () => {
|
|
53
|
+
expect(auth._oidcSettings).toMatchObject({
|
|
54
|
+
authority: 'https://auth.gameglue.gg/realms/GameGlue',
|
|
55
|
+
client_id: 'test-client-id',
|
|
56
|
+
redirect_uri: 'http://localhost:3000/callback',
|
|
57
|
+
response_type: 'code',
|
|
58
|
+
scope: 'openid msfs:read gg:broadcast',
|
|
59
|
+
response_mode: 'fragment',
|
|
60
|
+
filterProtocolClaims: true
|
|
61
|
+
});
|
|
62
|
+
// post_logout_redirect_uri is based on window.location.href which varies by environment
|
|
63
|
+
expect(auth._oidcSettings.post_logout_redirect_uri).toBeDefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should use window.location.href as default redirect_uri', () => {
|
|
67
|
+
const configNoRedirect = { clientId: 'test', scopes: [] };
|
|
68
|
+
const authNoRedirect = new GameGlueAuth(configNoRedirect);
|
|
69
|
+
|
|
70
|
+
// The redirect_uri should be based on window.location.href (varies by environment)
|
|
71
|
+
expect(authNoRedirect._oidcSettings.redirect_uri).toMatch(/^http:\/\/localhost/);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should handle empty scopes array', () => {
|
|
75
|
+
const configEmptyScopes = { clientId: 'test', scopes: [] };
|
|
76
|
+
const authEmptyScopes = new GameGlueAuth(configEmptyScopes);
|
|
77
|
+
|
|
78
|
+
expect(authEmptyScopes._oidcSettings.scope).toBe('openid ');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should handle undefined scopes', () => {
|
|
82
|
+
const configNoScopes = { clientId: 'test' };
|
|
83
|
+
const authNoScopes = new GameGlueAuth(configNoScopes);
|
|
84
|
+
|
|
85
|
+
expect(authNoScopes._oidcSettings.scope).toBe('openid ');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should remove trailing slashes from redirect_uri', () => {
|
|
89
|
+
const configWithSlash = {
|
|
90
|
+
clientId: 'test',
|
|
91
|
+
redirect_uri: 'http://localhost:3000/callback/'
|
|
92
|
+
};
|
|
93
|
+
const authWithSlash = new GameGlueAuth(configWithSlash);
|
|
94
|
+
|
|
95
|
+
expect(authWithSlash._oidcSettings.redirect_uri).toBe('http://localhost:3000/callback');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('_setAccessToken / getAccessToken', () => {
|
|
100
|
+
it('should store and retrieve access token', () => {
|
|
101
|
+
auth._setAccessToken(validAccessToken);
|
|
102
|
+
const retrieved = auth.getAccessToken();
|
|
103
|
+
|
|
104
|
+
expect(retrieved).toBe(validAccessToken);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should set up token refresh timeout when setting token', () => {
|
|
108
|
+
auth._setAccessToken(validAccessToken);
|
|
109
|
+
|
|
110
|
+
// Check that a timeout was scheduled
|
|
111
|
+
expect(jest.getTimerCount()).toBeGreaterThan(0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should not set timeout for null token', () => {
|
|
115
|
+
auth._setAccessToken(null);
|
|
116
|
+
|
|
117
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('_setRefreshToken / _getRefreshToken', () => {
|
|
122
|
+
it('should store and retrieve refresh token', () => {
|
|
123
|
+
auth._setRefreshToken(validRefreshToken);
|
|
124
|
+
const retrieved = auth._getRefreshToken();
|
|
125
|
+
|
|
126
|
+
expect(retrieved).toBe(validRefreshToken);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('getUser', () => {
|
|
131
|
+
it('should extract user ID from JWT', () => {
|
|
132
|
+
auth._setAccessToken(validAccessToken);
|
|
133
|
+
|
|
134
|
+
const userId = auth.getUser();
|
|
135
|
+
|
|
136
|
+
expect(userId).toBe('user-123');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should throw error when not authenticated', () => {
|
|
140
|
+
expect(() => auth.getUser()).toThrow('Not authenticated');
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('_hasValidTokens', () => {
|
|
145
|
+
it('should return false when no token exists', () => {
|
|
146
|
+
const result = auth._hasValidTokens();
|
|
147
|
+
|
|
148
|
+
expect(result).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should return true for valid non-expired token', () => {
|
|
152
|
+
auth._setAccessToken(validAccessToken);
|
|
153
|
+
|
|
154
|
+
const result = auth._hasValidTokens();
|
|
155
|
+
|
|
156
|
+
expect(result).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should return false for expired token', () => {
|
|
160
|
+
auth._setAccessToken(expiredAccessToken);
|
|
161
|
+
|
|
162
|
+
const result = auth._hasValidTokens();
|
|
163
|
+
|
|
164
|
+
expect(result).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('isAuthenticated', () => {
|
|
169
|
+
it('should return false when no token exists', async () => {
|
|
170
|
+
const result = await auth.isAuthenticated();
|
|
171
|
+
|
|
172
|
+
expect(result).toBe(false);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should return true for valid non-expired token', async () => {
|
|
176
|
+
auth._setAccessToken(validAccessToken);
|
|
177
|
+
|
|
178
|
+
const result = await auth.isAuthenticated();
|
|
179
|
+
|
|
180
|
+
expect(result).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should return false for expired token', async () => {
|
|
184
|
+
auth._setAccessToken(expiredAccessToken);
|
|
185
|
+
|
|
186
|
+
const result = await auth.isAuthenticated();
|
|
187
|
+
|
|
188
|
+
expect(result).toBe(false);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should process callback params if present in URL', async () => {
|
|
192
|
+
window.location.hash = '#state=abc123&code=xyz789';
|
|
193
|
+
|
|
194
|
+
const { OidcClient } = require('oidc-client-ts');
|
|
195
|
+
OidcClient.mockImplementation(() => ({
|
|
196
|
+
createSigninRequest: jest.fn(),
|
|
197
|
+
processSigninResponse: jest.fn().mockResolvedValue({
|
|
198
|
+
access_token: validAccessToken,
|
|
199
|
+
refresh_token: validRefreshToken
|
|
200
|
+
})
|
|
201
|
+
}));
|
|
202
|
+
|
|
203
|
+
const authNew = new GameGlueAuth(mockConfig);
|
|
204
|
+
const result = await authNew.isAuthenticated();
|
|
205
|
+
|
|
206
|
+
expect(result).toBe(true);
|
|
207
|
+
// URL should be cleared
|
|
208
|
+
expect(window.history.replaceState).toHaveBeenCalled();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('login', () => {
|
|
213
|
+
it('should call createSigninRequest', async () => {
|
|
214
|
+
const createSigninRequest = jest.fn().mockResolvedValue({ url: 'https://auth.example.com/login' });
|
|
215
|
+
const { OidcClient } = require('oidc-client-ts');
|
|
216
|
+
OidcClient.mockImplementation(() => ({
|
|
217
|
+
createSigninRequest,
|
|
218
|
+
processSigninResponse: jest.fn()
|
|
219
|
+
}));
|
|
220
|
+
|
|
221
|
+
const authNew = new GameGlueAuth(mockConfig);
|
|
222
|
+
authNew.login();
|
|
223
|
+
|
|
224
|
+
// Wait for promise to resolve
|
|
225
|
+
await Promise.resolve();
|
|
226
|
+
|
|
227
|
+
expect(createSigninRequest).toHaveBeenCalledWith({ state: { bar: 15 } });
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('logout', () => {
|
|
232
|
+
it('should clear tokens from storage', () => {
|
|
233
|
+
auth._setAccessToken(validAccessToken);
|
|
234
|
+
auth._setRefreshToken(validRefreshToken);
|
|
235
|
+
|
|
236
|
+
auth.logout({ redirect: false });
|
|
237
|
+
|
|
238
|
+
// After remove, storage.get returns undefined in Node environment (in-memory map)
|
|
239
|
+
expect(auth.getAccessToken()).toBeUndefined();
|
|
240
|
+
expect(auth._getRefreshToken()).toBeUndefined();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should not clear tokens when redirect: false not passed', () => {
|
|
244
|
+
// We can't easily test the redirect behavior in jsdom, but we can verify
|
|
245
|
+
// the method accepts options and clears tokens regardless
|
|
246
|
+
auth._setAccessToken(validAccessToken);
|
|
247
|
+
auth._setRefreshToken(validRefreshToken);
|
|
248
|
+
|
|
249
|
+
// logout() clears tokens first, then redirects
|
|
250
|
+
auth.logout({ redirect: false });
|
|
251
|
+
|
|
252
|
+
expect(auth.getAccessToken()).toBeUndefined();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should accept options object', () => {
|
|
256
|
+
auth._setAccessToken(validAccessToken);
|
|
257
|
+
|
|
258
|
+
// Should not throw when called with different options
|
|
259
|
+
expect(() => auth.logout({ redirect: false })).not.toThrow();
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
describe('_attemptRefresh', () => {
|
|
264
|
+
it('should call token endpoint with correct parameters', async () => {
|
|
265
|
+
auth._setRefreshToken(validRefreshToken);
|
|
266
|
+
|
|
267
|
+
global.fetch.mockResolvedValueOnce({
|
|
268
|
+
status: 200,
|
|
269
|
+
json: () =>
|
|
270
|
+
Promise.resolve({
|
|
271
|
+
access_token: validAccessToken,
|
|
272
|
+
refresh_token: validRefreshToken
|
|
273
|
+
})
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
await auth._attemptRefresh();
|
|
277
|
+
|
|
278
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
279
|
+
'https://auth.gameglue.gg/realms/GameGlue/protocol/openid-connect/token',
|
|
280
|
+
expect.objectContaining({
|
|
281
|
+
method: 'POST',
|
|
282
|
+
headers: {
|
|
283
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should update tokens on successful refresh', async () => {
|
|
290
|
+
auth._setRefreshToken(validRefreshToken);
|
|
291
|
+
|
|
292
|
+
const newAccessToken = createMockJwt({ sub: 'user-456' }, 3600);
|
|
293
|
+
const newRefreshToken = createMockJwt({ sub: 'user-456', type: 'refresh' }, 86400);
|
|
294
|
+
|
|
295
|
+
global.fetch.mockResolvedValueOnce({
|
|
296
|
+
status: 200,
|
|
297
|
+
json: () =>
|
|
298
|
+
Promise.resolve({
|
|
299
|
+
access_token: newAccessToken,
|
|
300
|
+
refresh_token: newRefreshToken
|
|
301
|
+
})
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
await auth._attemptRefresh();
|
|
305
|
+
|
|
306
|
+
expect(auth.getAccessToken()).toBe(newAccessToken);
|
|
307
|
+
expect(auth._getRefreshToken()).toBe(newRefreshToken);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('should call refresh callback with access token on successful refresh', async () => {
|
|
311
|
+
const callback = jest.fn();
|
|
312
|
+
auth.onTokenRefreshed(callback);
|
|
313
|
+
auth._setRefreshToken(validRefreshToken);
|
|
314
|
+
|
|
315
|
+
const responseData = {
|
|
316
|
+
access_token: validAccessToken,
|
|
317
|
+
refresh_token: validRefreshToken
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
global.fetch.mockResolvedValueOnce({
|
|
321
|
+
status: 200,
|
|
322
|
+
json: () => Promise.resolve(responseData)
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
await auth._attemptRefresh();
|
|
326
|
+
|
|
327
|
+
expect(callback).toHaveBeenCalledWith(validAccessToken);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should handle network errors gracefully', async () => {
|
|
331
|
+
auth._setRefreshToken(validRefreshToken);
|
|
332
|
+
global.fetch.mockRejectedValueOnce(new Error('Network error'));
|
|
333
|
+
|
|
334
|
+
// Should not throw
|
|
335
|
+
await expect(auth._attemptRefresh()).resolves.toBeUndefined();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should not update tokens on non-200 response', async () => {
|
|
339
|
+
auth._setRefreshToken(validRefreshToken);
|
|
340
|
+
const originalAccessToken = auth.getAccessToken();
|
|
341
|
+
|
|
342
|
+
global.fetch.mockResolvedValueOnce({
|
|
343
|
+
status: 400,
|
|
344
|
+
json: () => Promise.resolve({ error: 'invalid_grant' })
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
await auth._attemptRefresh();
|
|
348
|
+
|
|
349
|
+
// Access token should remain unchanged (null in this case)
|
|
350
|
+
expect(auth.getAccessToken()).toBe(originalAccessToken);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
describe('onTokenRefreshed', () => {
|
|
355
|
+
it('should register callback for token refresh', () => {
|
|
356
|
+
const callback = jest.fn();
|
|
357
|
+
auth.onTokenRefreshed(callback);
|
|
358
|
+
|
|
359
|
+
expect(auth._refreshCallback).toBe(callback);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe('_setTokenRefreshTimeout', () => {
|
|
364
|
+
it('should schedule refresh before token expiration', () => {
|
|
365
|
+
// Token expires in 1 hour (3600 seconds)
|
|
366
|
+
auth._setTokenRefreshTimeout(validAccessToken);
|
|
367
|
+
|
|
368
|
+
// Should have scheduled a timeout
|
|
369
|
+
expect(jest.getTimerCount()).toBe(1);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('should clear previous timeout when setting new one', () => {
|
|
373
|
+
auth._setTokenRefreshTimeout(validAccessToken);
|
|
374
|
+
auth._setTokenRefreshTimeout(validAccessToken);
|
|
375
|
+
|
|
376
|
+
// Should only have one active timeout
|
|
377
|
+
expect(jest.getTimerCount()).toBe(1);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should not schedule timeout for expired token', () => {
|
|
381
|
+
auth._setTokenRefreshTimeout(expiredAccessToken);
|
|
382
|
+
|
|
383
|
+
// Timer count may be 0 since the timeUntilExp is negative
|
|
384
|
+
// The important thing is it doesn't throw
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
describe('_hasCallbackParams', () => {
|
|
389
|
+
it('should return true when hash contains state and code', () => {
|
|
390
|
+
window.location.hash = '#state=abc123&code=xyz789';
|
|
391
|
+
|
|
392
|
+
expect(auth._hasCallbackParams()).toBe(true);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it('should return true when hash contains state and error', () => {
|
|
396
|
+
window.location.hash = '#state=abc123&error=access_denied';
|
|
397
|
+
|
|
398
|
+
expect(auth._hasCallbackParams()).toBe(true);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should return false when hash does not contain state', () => {
|
|
402
|
+
window.location.hash = '#code=xyz789';
|
|
403
|
+
|
|
404
|
+
expect(auth._hasCallbackParams()).toBe(false);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('should return false when hash is empty', () => {
|
|
408
|
+
window.location.hash = '';
|
|
409
|
+
|
|
410
|
+
expect(auth._hasCallbackParams()).toBe(false);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
describe('_processCallback', () => {
|
|
415
|
+
it('should process signin response and store tokens', async () => {
|
|
416
|
+
const { OidcClient } = require('oidc-client-ts');
|
|
417
|
+
OidcClient.mockImplementation(() => ({
|
|
418
|
+
processSigninResponse: jest.fn().mockResolvedValue({
|
|
419
|
+
access_token: validAccessToken,
|
|
420
|
+
refresh_token: validRefreshToken
|
|
421
|
+
})
|
|
422
|
+
}));
|
|
423
|
+
|
|
424
|
+
const authNew = new GameGlueAuth(mockConfig);
|
|
425
|
+
await authNew._processCallback();
|
|
426
|
+
|
|
427
|
+
expect(authNew.getAccessToken()).toBe(validAccessToken);
|
|
428
|
+
expect(authNew._getRefreshToken()).toBe(validRefreshToken);
|
|
429
|
+
expect(window.history.replaceState).toHaveBeenCalled();
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('should throw on error response', async () => {
|
|
433
|
+
const { OidcClient } = require('oidc-client-ts');
|
|
434
|
+
OidcClient.mockImplementation(() => ({
|
|
435
|
+
processSigninResponse: jest.fn().mockResolvedValue({
|
|
436
|
+
error: 'access_denied',
|
|
437
|
+
access_token: null
|
|
438
|
+
})
|
|
439
|
+
}));
|
|
440
|
+
|
|
441
|
+
const authNew = new GameGlueAuth(mockConfig);
|
|
442
|
+
|
|
443
|
+
await expect(authNew._processCallback()).rejects.toThrow('access_denied');
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it('should throw when no access token received', async () => {
|
|
447
|
+
const { OidcClient } = require('oidc-client-ts');
|
|
448
|
+
OidcClient.mockImplementation(() => ({
|
|
449
|
+
processSigninResponse: jest.fn().mockResolvedValue({
|
|
450
|
+
access_token: null
|
|
451
|
+
})
|
|
452
|
+
}));
|
|
453
|
+
|
|
454
|
+
const authNew = new GameGlueAuth(mockConfig);
|
|
455
|
+
|
|
456
|
+
await expect(authNew._processCallback()).rejects.toThrow('No access token received');
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('should handle concurrent calls (only process once)', async () => {
|
|
460
|
+
const { OidcClient } = require('oidc-client-ts');
|
|
461
|
+
const processSigninResponse = jest.fn().mockResolvedValue({
|
|
462
|
+
access_token: validAccessToken,
|
|
463
|
+
refresh_token: validRefreshToken
|
|
464
|
+
});
|
|
465
|
+
OidcClient.mockImplementation(() => ({
|
|
466
|
+
processSigninResponse
|
|
467
|
+
}));
|
|
468
|
+
|
|
469
|
+
const authNew = new GameGlueAuth(mockConfig);
|
|
470
|
+
|
|
471
|
+
// Call twice concurrently
|
|
472
|
+
await Promise.all([
|
|
473
|
+
authNew._processCallback(),
|
|
474
|
+
authNew._processCallback()
|
|
475
|
+
]);
|
|
476
|
+
|
|
477
|
+
// Should only have been called once due to locking
|
|
478
|
+
expect(processSigninResponse).toHaveBeenCalledTimes(1);
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
});
|