@strivacity/sdk-core 4.0.0-beta.0 → 4.0.0-beta.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.
Files changed (97) hide show
  1. package/README.md +228 -86
  2. package/dist/assets/oidc.cjs +1 -1
  3. package/dist/assets/oidc.cjs.map +1 -1
  4. package/dist/assets/oidc.mjs +1 -1
  5. package/dist/assets/oidc.mjs.map +1 -1
  6. package/dist/assets/server.cjs +1 -1
  7. package/dist/assets/server.cjs.map +1 -1
  8. package/dist/assets/server.mjs +1 -1
  9. package/dist/assets/server.mjs.map +1 -1
  10. package/dist/flows/base.cjs +1 -1
  11. package/dist/flows/base.cjs.map +1 -1
  12. package/dist/flows/base.d.ts +4 -2
  13. package/dist/flows/base.mjs +1 -1
  14. package/dist/flows/base.mjs.map +1 -1
  15. package/dist/flows/popup.cjs +1 -1
  16. package/dist/flows/popup.cjs.map +1 -1
  17. package/dist/flows/popup.mjs +1 -1
  18. package/dist/flows/popup.mjs.map +1 -1
  19. package/dist/index.cjs +1 -1
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.ts +6 -2
  22. package/dist/index.mjs +1 -1
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/server/base.cjs +1 -1
  25. package/dist/server/base.cjs.map +1 -1
  26. package/dist/server/base.d.ts +2 -4
  27. package/dist/server/base.mjs +1 -1
  28. package/dist/server/base.mjs.map +1 -1
  29. package/dist/server/index.cjs +1 -1
  30. package/dist/server/index.mjs +1 -1
  31. package/dist/storages/cache.cjs.map +1 -1
  32. package/dist/storages/cache.d.ts +1 -1
  33. package/dist/storages/cache.mjs.map +1 -1
  34. package/dist/storages/index.cjs +1 -1
  35. package/dist/storages/index.mjs +1 -1
  36. package/dist/storages/server.cjs +1 -1
  37. package/dist/storages/server.d.ts +29 -1
  38. package/dist/storages/server.mjs +1 -1
  39. package/dist/storages/worker.cjs.map +1 -1
  40. package/dist/storages/worker.d.ts +1 -1
  41. package/dist/storages/worker.mjs.map +1 -1
  42. package/dist/types/native.d.ts +67 -2
  43. package/dist/types/oidc.cjs.map +1 -1
  44. package/dist/types/oidc.d.ts +47 -6
  45. package/dist/types/oidc.mjs.map +1 -1
  46. package/dist/types/popup.d.ts +1 -1
  47. package/dist/types/server.d.ts +21 -9
  48. package/dist/utils/index.cjs +1 -1
  49. package/dist/utils/index.d.ts +1 -0
  50. package/dist/utils/index.mjs +1 -1
  51. package/dist/utils/native-login.cjs +2 -0
  52. package/dist/utils/native-login.cjs.map +1 -0
  53. package/dist/utils/native-login.d.ts +13 -0
  54. package/dist/utils/native-login.mjs +2 -0
  55. package/dist/utils/native-login.mjs.map +1 -0
  56. package/dist/utils/oidc.d.ts +3 -4
  57. package/dist/utils/path.cjs +2 -0
  58. package/dist/utils/path.cjs.map +1 -0
  59. package/dist/utils/path.d.ts +16 -0
  60. package/dist/utils/path.mjs +2 -0
  61. package/dist/utils/path.mjs.map +1 -0
  62. package/dist/utils/server.d.ts +5 -4
  63. package/dist/utils/session.cjs +1 -1
  64. package/dist/utils/session.cjs.map +1 -1
  65. package/dist/utils/session.d.ts +10 -1
  66. package/dist/utils/session.mjs +1 -1
  67. package/dist/utils/session.mjs.map +1 -1
  68. package/package.json +1 -1
  69. package/testing/tests/flows/base.spec.ts +0 -1030
  70. package/testing/tests/flows/embedded.spec.ts +0 -443
  71. package/testing/tests/flows/index.spec.ts +0 -8
  72. package/testing/tests/flows/native.spec.ts +0 -487
  73. package/testing/tests/flows/popup.spec.ts +0 -400
  74. package/testing/tests/flows/redirect.spec.ts +0 -369
  75. package/testing/tests/handlers/embedded.spec.ts +0 -199
  76. package/testing/tests/handlers/index.spec.ts +0 -8
  77. package/testing/tests/handlers/native.spec.ts +0 -283
  78. package/testing/tests/handlers/popup.spec.ts +0 -206
  79. package/testing/tests/handlers/redirect.spec.ts +0 -70
  80. package/testing/tests/index.spec.ts +0 -78
  81. package/testing/tests/server/base.spec.ts +0 -846
  82. package/testing/tests/server/index.spec.ts +0 -15
  83. package/testing/tests/storages/cache.spec.ts +0 -107
  84. package/testing/tests/storages/index.spec.ts +0 -22
  85. package/testing/tests/storages/indexeddb.spec.ts +0 -113
  86. package/testing/tests/storages/local.spec.ts +0 -46
  87. package/testing/tests/storages/memory.spec.ts +0 -51
  88. package/testing/tests/storages/server.spec.ts +0 -435
  89. package/testing/tests/storages/session.spec.ts +0 -46
  90. package/testing/tests/storages/worker.spec.ts +0 -106
  91. package/testing/tests/utils/common.spec.ts +0 -77
  92. package/testing/tests/utils/credentials.spec.ts +0 -195
  93. package/testing/tests/utils/dom.spec.ts +0 -37
  94. package/testing/tests/utils/index.spec.ts +0 -71
  95. package/testing/tests/utils/logging.spec.ts +0 -82
  96. package/testing/tests/utils/oidc.spec.ts +0 -744
  97. package/testing/tests/utils/server.spec.ts +0 -235
@@ -1,487 +0,0 @@
1
- import { beforeEach, describe, test, expect, vi } from 'vitest';
2
- import { http, HttpResponse } from 'msw';
3
- import { createOptions, getMockStorage } from '@strivacity/testing/mocks/sdk';
4
- import { worker } from '@strivacity/testing/mocks/msw';
5
- import { createNativeFlow } from '../../../src/flows/native';
6
- import { FallbackError, NetworkError, OidcError, ProtocolError, ServerError, SessionExpiredError } from '../../../src/utils';
7
-
8
- describe('createNativeFlow', () => {
9
- let options: ReturnType<typeof createOptions>;
10
- let flow: ReturnType<typeof createNativeFlow>;
11
-
12
- beforeEach(() => {
13
- globalThis.location.href = 'https://brandtegrity.io';
14
-
15
- options = createOptions();
16
- flow = createNativeFlow(options);
17
- });
18
-
19
- describe('init', () => {
20
- test('lazyLoad: true', async () => {
21
- flow = createNativeFlow(createOptions({ lazyLoad: true }));
22
-
23
- // We need to wait for the next tick to ensure that any asynchronous initialization logic has a chance to run before we check the initialized state.
24
- await Promise.resolve();
25
-
26
- expect(flow.initialized).toBe(false);
27
-
28
- await flow.init();
29
-
30
- expect(flow.initialized).toBe(true);
31
- });
32
-
33
- test('lazyLoad: false', async () => {
34
- flow = createNativeFlow(createOptions({ lazyLoad: false }));
35
-
36
- await vi.waitFor(() => expect(flow.initialized).toBe(true));
37
- });
38
- });
39
-
40
- describe('startSession', () => {
41
- test('should submit the init form and collect the session identifiers when the response contains a session_id', async () => {
42
- worker.use(
43
- http.get('https://brandtegrity.io/oauth2/auth', () =>
44
- HttpResponse.text('https://brandtegrity.io/login?session_id=abc123&short_app_id=app&language=hu-HU'),
45
- ),
46
- http.post('https://brandtegrity.io/flow/api/v1/init', () => HttpResponse.json({ screen: 'identification' })),
47
- );
48
-
49
- const data = await flow.startSession();
50
-
51
- expect(flow.sessionId).toBe('abc123');
52
- expect(flow.shortAppId).toBe('app');
53
- expect(flow.language).toBe('hu-HU');
54
- expect(data).toEqual({ screen: 'identification' });
55
- });
56
-
57
- test('should continue an existing session without contacting the authorization endpoint when a sessionId is provided', async () => {
58
- worker.use(http.post('https://brandtegrity.io/flow/api/v1/init', () => HttpResponse.json({ screen: 'identification' })));
59
-
60
- const data = await flow.startSession({ sessionId: 'existing-session' });
61
-
62
- expect(flow.sessionId).toBe('existing-session');
63
- expect(data).toEqual({ screen: 'identification' });
64
- });
65
-
66
- describe('should exchange the code for tokens', () => {
67
- test('w/o serverSessionUri', async () => {
68
- worker.use(
69
- http.get('https://brandtegrity.io/oauth2/auth', ({ request }) => {
70
- const state = new URL(request.url).searchParams.get('state');
71
-
72
- return HttpResponse.text(`https://brandtegrity.io/callback?code=abc123&state=${state}`);
73
- }),
74
- http.post('https://brandtegrity.io/oauth2/token', () => HttpResponse.json({ access_token: 'access-token', token_type: 'Bearer', expires_in: 3600 })),
75
- );
76
-
77
- await flow.startSession();
78
-
79
- expect(globalThis.location.href).toBe('https://brandtegrity.io/');
80
- expect(flow.accessToken).toBe('access-token');
81
- });
82
-
83
- test('w/ serverSessionUri', async () => {
84
- worker.use(http.get('https://app.example.com/login', () => HttpResponse.text('https://brandtegrity.io/callback?code=abc123')));
85
-
86
- options = createOptions({ serverSessionUri: 'https://app.example.com/login' });
87
- flow = createNativeFlow(options);
88
-
89
- await flow.startSession();
90
-
91
- expect(globalThis.location.href).toBe('https://brandtegrity.io/callback?code=abc123');
92
- expect(flow.accessToken).toBeNull();
93
- });
94
- });
95
- });
96
-
97
- describe('submitForm', () => {
98
- test('should throw an error when the session ID is missing', async () => {
99
- await expect(flow.submitForm()).rejects.toThrow('Session ID is missing. Cannot submit form.');
100
- });
101
-
102
- test('should submit to the init endpoint when no formId is provided', async () => {
103
- worker.use(http.post('https://brandtegrity.io/flow/api/v1/init', () => HttpResponse.json({ screen: 'identification' })));
104
-
105
- flow.sessionId = 'abc123';
106
-
107
- await expect(flow.submitForm()).resolves.toEqual({ screen: 'identification' });
108
- });
109
-
110
- test('should submit to the form-specific endpoint when a formId is provided', async () => {
111
- worker.use(http.post('https://brandtegrity.io/flow/api/v1/form/identifier', () => HttpResponse.json({ screen: 'password' })));
112
-
113
- flow.sessionId = 'abc123';
114
-
115
- await expect(flow.submitForm('identifier', { username: 'user' })).resolves.toEqual({ screen: 'password' });
116
- });
117
-
118
- test('should finalize the session and exchange the code for tokens when the response contains a finalizeUrl', async () => {
119
- const stateData = await getMockStorage(options.stateStorage).generateState();
120
-
121
- worker.use(
122
- http.post('https://brandtegrity.io/flow/api/v1/init', () => HttpResponse.json({ finalizeUrl: 'https://brandtegrity.io/finalize' })),
123
- http.get('https://brandtegrity.io/finalize', () => HttpResponse.text(`https://brandtegrity.io/callback?code=abc123&state=${stateData.id}`)),
124
- http.post('https://brandtegrity.io/oauth2/token', () => HttpResponse.json({ access_token: 'access-token', token_type: 'Bearer', expires_in: 3600 })),
125
- );
126
-
127
- flow.sessionId = 'abc123';
128
-
129
- await flow.submitForm();
130
-
131
- expect(flow.accessToken).toBe('access-token');
132
- });
133
-
134
- test('should reject with a SessionExpiredError on HTTP 403', async () => {
135
- worker.use(http.post('https://brandtegrity.io/flow/api/v1/init', () => new HttpResponse(null, { status: 403 })));
136
-
137
- flow.sessionId = 'abc123';
138
-
139
- await expect(flow.submitForm()).rejects.toThrow(SessionExpiredError);
140
- });
141
-
142
- test('should reject with a FallbackError when the response only contains a hostedUrl', async () => {
143
- worker.use(http.post('https://brandtegrity.io/flow/api/v1/init', () => HttpResponse.json({ hostedUrl: 'https://brandtegrity.io/hosted' })));
144
-
145
- flow.sessionId = 'abc123';
146
-
147
- await expect(flow.submitForm()).rejects.toThrow(FallbackError);
148
- });
149
-
150
- test('the SDK never opens a browser/popup itself on a Fallback', async () => {
151
- const urlHandler = vi.fn();
152
- options = createOptions({ urlHandler });
153
- flow = createNativeFlow(options);
154
- flow.sessionId = 'abc123';
155
- worker.use(http.post('https://brandtegrity.io/flow/api/v1/init', () => HttpResponse.json({ hostedUrl: 'https://brandtegrity.io/hosted' })));
156
-
157
- await expect(flow.submitForm()).rejects.toThrow(FallbackError);
158
-
159
- expect(urlHandler).not.toHaveBeenCalled();
160
- });
161
-
162
- test('a chained journey of rejections returns each screen independently, never bleeding a stale message into a later one', async () => {
163
- flow.sessionId = 'abc123';
164
-
165
- worker.use(
166
- http.post('https://brandtegrity.io/flow/api/v1/form/password', () =>
167
- HttpResponse.json({ screen: 'password', messages: { global: { type: 'error', text: 'Wrong password' } } }),
168
- ),
169
- );
170
- const first = await flow.submitForm('password', { password: 'wrong-1' });
171
- expect(first.messages?.global?.text).toBe('Wrong password');
172
-
173
- worker.use(
174
- http.post('https://brandtegrity.io/flow/api/v1/form/password', () =>
175
- HttpResponse.json({ screen: 'password', messages: { global: { type: 'error', text: 'Account locked' } } }),
176
- ),
177
- );
178
- const second = await flow.submitForm('password', { password: 'wrong-2' });
179
- expect(second.messages?.global?.text).toBe('Account locked');
180
- expect(second.messages?.global?.text).not.toBe(first.messages?.global?.text);
181
-
182
- worker.use(http.post('https://brandtegrity.io/flow/api/v1/form/password', () => HttpResponse.json({ screen: 'passcode' })));
183
- const third = await flow.submitForm('password', { password: 'correct' });
184
- expect(third.messages).toBeUndefined();
185
- expect(third.screen).toBe('passcode');
186
- });
187
- });
188
-
189
- describe('finalizeSession', () => {
190
- test('should throw when the session ID is missing', async () => {
191
- await expect(flow.finalizeSession('https://brandtegrity.io/finalize')).rejects.toThrow('Session ID is missing. Cannot finalize login session.');
192
- });
193
-
194
- describe('should exchange the code for tokens', () => {
195
- test('w/o serverSessionUri', async () => {
196
- const stateData = await getMockStorage(options.stateStorage).generateState();
197
-
198
- worker.use(
199
- http.get('https://brandtegrity.io/finalize', () => HttpResponse.text(`https://brandtegrity.io/callback?code=abc123&state=${stateData.id}`)),
200
- http.post('https://brandtegrity.io/oauth2/token', () => HttpResponse.json({ access_token: 'access-token', token_type: 'Bearer', expires_in: 3600 })),
201
- );
202
-
203
- flow.sessionId = 'abc123';
204
-
205
- await flow.finalizeSession('https://brandtegrity.io/finalize');
206
-
207
- expect(flow.accessToken).toBe('access-token');
208
- });
209
-
210
- test('w/ serverSessionUri', async () => {
211
- worker.use(http.get('https://brandtegrity.io/finalize', () => HttpResponse.text('https://brandtegrity.io/callback?code=abc123')));
212
-
213
- options = createOptions({ serverSessionUri: 'https://app.example.com/login' });
214
- flow = createNativeFlow(options);
215
-
216
- flow.sessionId = 'abc123';
217
-
218
- await flow.finalizeSession('https://brandtegrity.io/finalize');
219
-
220
- expect(globalThis.location.href).toBe('https://brandtegrity.io/callback?code=abc123');
221
- expect(flow.accessToken).toBeNull();
222
- });
223
- });
224
-
225
- test('should throw a ProtocolError when the redirect URI in the response is invalid', async () => {
226
- worker.use(http.get('https://brandtegrity.io/finalize', () => HttpResponse.text('not-a-valid-url')));
227
-
228
- flow.sessionId = 'abc123';
229
-
230
- await expect(flow.finalizeSession('https://brandtegrity.io/finalize')).rejects.toThrow(new ProtocolError('Invalid redirect URI'));
231
- expect(flow.options.logging?.error).toHaveBeenCalledWith('Finalize login session error', expect.any(ProtocolError));
232
- });
233
- });
234
-
235
- describe('login', () => {
236
- test('backward compatibility', async () => {
237
- worker.use(
238
- http.get('https://brandtegrity.io/oauth2/auth', () =>
239
- HttpResponse.text('https://brandtegrity.io/login?session_id=xyz789&short_app_id=app2&language=en-US'),
240
- ),
241
- http.post('https://brandtegrity.io/flow/api/v1/init', () => HttpResponse.json({ screen: 'identification' })),
242
- );
243
-
244
- const loginFlow = flow.login();
245
-
246
- expect(loginFlow.sessionId).toBeNull();
247
- expect(loginFlow.shortAppId).toBeNull();
248
- expect(loginFlow.language).toBe('en-US');
249
-
250
- loginFlow.sessionId = 'abc123';
251
- loginFlow.shortAppId = 'app';
252
- loginFlow.language = 'hu-HU';
253
-
254
- expect(flow.sessionId).toBe('abc123');
255
- expect(flow.shortAppId).toBe('app');
256
- expect(flow.language).toBe('hu-HU');
257
-
258
- await loginFlow.startSession();
259
-
260
- expect(flow.sessionId).toBe('xyz789');
261
- expect(flow.shortAppId).toBe('app2');
262
- expect(flow.language).toBe('en-US');
263
-
264
- const stateData = await getMockStorage(options.stateStorage).generateState();
265
-
266
- worker.use(
267
- http.get('https://brandtegrity.io/finalize', () => HttpResponse.text(`https://brandtegrity.io/callback?code=abc123&state=${stateData.id}`)),
268
- http.post('https://brandtegrity.io/oauth2/token', () => HttpResponse.json({ access_token: 'access-token', token_type: 'Bearer', expires_in: 3600 })),
269
- );
270
-
271
- await loginFlow.finalizeSession('https://brandtegrity.io/finalize');
272
-
273
- expect(flow.accessToken).toBe('access-token');
274
- });
275
-
276
- test('should forward login params to startSession', async () => {
277
- let capturedUrl: URL | undefined;
278
-
279
- worker.use(
280
- http.get('https://brandtegrity.io/oauth2/auth', ({ request }) => {
281
- capturedUrl = new URL(request.url);
282
-
283
- return HttpResponse.text('https://brandtegrity.io/login?session_id=abc123&short_app_id=app&language=en-US');
284
- }),
285
- http.post('https://brandtegrity.io/flow/api/v1/init', () => HttpResponse.json({ screen: 'identification' })),
286
- );
287
-
288
- await flow.login({ prompt: 'login', loginHint: 'test@example.com' }).startSession();
289
-
290
- expect(capturedUrl?.searchParams.get('prompt')).toBe('login');
291
- expect(capturedUrl?.searchParams.get('login_hint')).toBe('test@example.com');
292
- expect(flow.sessionId).toBe('abc123');
293
- expect(flow.shortAppId).toBe('app');
294
- expect(flow.language).toBe('en-US');
295
- });
296
-
297
- test('should submit forms through the login flow instance', async () => {
298
- worker.use(http.post('https://brandtegrity.io/flow/api/v1/form/identifier', () => HttpResponse.json({ screen: 'password' })));
299
-
300
- const loginFlow = flow.login();
301
- loginFlow.sessionId = 'abc123';
302
-
303
- await expect(loginFlow.submitForm('identifier', { username: 'user' })).resolves.toEqual({ screen: 'password' });
304
- });
305
- });
306
-
307
- describe('register', () => {
308
- test('should force the "create" prompt', async () => {
309
- let capturedUrl: URL | undefined;
310
-
311
- worker.use(
312
- http.get('https://brandtegrity.io/oauth2/auth', ({ request }) => {
313
- capturedUrl = new URL(request.url);
314
-
315
- return HttpResponse.text('https://brandtegrity.io/login?session_id=abc123&short_app_id=app&language=en-US');
316
- }),
317
- http.post('https://brandtegrity.io/flow/api/v1/init', () => HttpResponse.json({ screen: 'identification' })),
318
- );
319
-
320
- await flow.register().startSession();
321
-
322
- expect(capturedUrl?.searchParams.get('prompt')).toBe('create');
323
- expect(flow.sessionId).toBe('abc123');
324
- expect(flow.shortAppId).toBe('app');
325
- expect(flow.language).toBe('en-US');
326
- });
327
- });
328
-
329
- describe('entry', () => {
330
- test('should fetch the entry data preserving query parameters', async () => {
331
- let capturedUrl: URL | undefined;
332
-
333
- worker.use(
334
- http.get('https://brandtegrity.io/provider/flow/entry', ({ request }) => {
335
- capturedUrl = new URL(request.url);
336
-
337
- return HttpResponse.text('https://brandtegrity.io/entry?session_id=abc123&short_app_id=app&language=hu-HU');
338
- }),
339
- );
340
-
341
- const data = await flow.entry('https://brandtegrity.io/entry?challenge=abc123&foo=bar');
342
-
343
- expect(capturedUrl?.searchParams.get('challenge')).toBe('abc123');
344
- expect(capturedUrl?.searchParams.get('foo')).toBe('bar');
345
- expect(capturedUrl?.searchParams.get('sdk')).toBe('web');
346
- expect(capturedUrl?.searchParams.get('client_id')).toBe('client-id');
347
- expect(capturedUrl?.searchParams.get('redirect_uri')).toBe('https://brandtegrity.io/callback');
348
- expect(data).toEqual({ session_id: 'abc123', short_app_id: 'app', language: 'hu-HU' });
349
- expect(flow.session).toBeNull();
350
- await expect(flow.isAuthenticated).resolves.toBe(false);
351
- });
352
-
353
- test('should default to the current window location when no url is provided', async () => {
354
- window.location.search = '?challenge=abc123&foo=bar';
355
-
356
- let capturedUrl: URL | undefined;
357
-
358
- worker.use(
359
- http.get('https://brandtegrity.io/provider/flow/entry', ({ request }) => {
360
- capturedUrl = new URL(request.url);
361
-
362
- return HttpResponse.text('https://brandtegrity.io/entry?session_id=abc123&short_app_id=app&language=hu-HU');
363
- }),
364
- );
365
-
366
- const data = await flow.entry();
367
-
368
- expect(capturedUrl?.searchParams.get('challenge')).toBe('abc123');
369
- expect(capturedUrl?.searchParams.get('foo')).toBe('bar');
370
- expect(data.session_id).toBe('abc123');
371
- });
372
-
373
- describe('should reject with an error when the HTTP request fails', () => {
374
- test('HTTP 400', async () => {
375
- worker.use(http.get('https://brandtegrity.io/provider/flow/entry', () => new HttpResponse('{}', { status: 400 })));
376
-
377
- await expect(flow.entry('https://brandtegrity.io/entry?challenge=abc123')).rejects.toThrow(new ProtocolError('Entry request failed'));
378
- expect(flow.options.logging?.error).toHaveBeenCalledWith('Entry request failed', expect.any(ProtocolError));
379
- });
380
-
381
- test('HTTP 400 with an OIDC error body', async () => {
382
- worker.use(
383
- http.get('https://brandtegrity.io/provider/flow/entry', () =>
384
- HttpResponse.json({ error: 'access_denied', error_description: 'User denied access' }, { status: 400 }),
385
- ),
386
- );
387
-
388
- await expect(flow.entry('https://brandtegrity.io/entry?challenge=abc123')).rejects.toThrow(new OidcError('access_denied', 'User denied access'));
389
- expect(flow.options.logging?.error).toHaveBeenCalledWith('Entry request failed', expect.any(OidcError));
390
- });
391
-
392
- test('HTTP 400 with an errorKey body', async () => {
393
- worker.use(http.get('https://brandtegrity.io/provider/flow/entry', () => HttpResponse.json({ errorKey: 'invalid_request' }, { status: 400 })));
394
-
395
- await expect(flow.entry('https://brandtegrity.io/entry?challenge=abc123')).rejects.toThrow(new ProtocolError('invalid_request'));
396
- expect(flow.options.logging?.error).toHaveBeenCalledWith('Entry request failed', expect.any(ProtocolError));
397
- });
398
-
399
- test('HTTP 429', async () => {
400
- worker.use(http.get('https://brandtegrity.io/provider/flow/entry', () => new HttpResponse(null, { status: 429 })));
401
-
402
- await expect(flow.entry('https://brandtegrity.io/entry?challenge=abc123')).rejects.toThrow(new ServerError('Entry request failed', 429));
403
- expect(flow.options.logging?.error).toHaveBeenCalledWith('Entry request failed', expect.any(ServerError));
404
- });
405
-
406
- test('HTTP 500', async () => {
407
- worker.use(http.get('https://brandtegrity.io/provider/flow/entry', () => new HttpResponse(null, { status: 500 })));
408
-
409
- await expect(flow.entry('https://brandtegrity.io/entry?challenge=abc123')).rejects.toThrow(new ServerError('Entry request failed', 500));
410
- expect(flow.options.logging?.error).toHaveBeenCalledWith('Entry request failed', expect.any(ServerError));
411
- });
412
-
413
- test('HTTP 503', async () => {
414
- worker.use(http.get('https://brandtegrity.io/provider/flow/entry', () => HttpResponse.error()));
415
-
416
- await expect(flow.entry('https://brandtegrity.io/entry?challenge=abc123')).rejects.toThrow(
417
- new NetworkError('Network request failed: https://brandtegrity.io/provider/flow/entry'),
418
- );
419
- expect(flow.options.logging?.error).toHaveBeenCalledWith('Entry request failed', expect.any(NetworkError));
420
- });
421
-
422
- test('should throw a ProtocolError when "session_id" is missing from the response', async () => {
423
- worker.use(http.get('https://brandtegrity.io/provider/flow/entry', () => HttpResponse.text('https://brandtegrity.io/entry?short_app_id=app')));
424
-
425
- await expect(flow.entry('https://brandtegrity.io/entry?challenge=abc123')).rejects.toThrow(
426
- new ProtocolError('"session_id" is missing from the response'),
427
- );
428
- expect(flow.options.logging?.error).toHaveBeenCalledWith('Entry request failed', expect.any(ProtocolError));
429
- });
430
- });
431
-
432
- test('should fall back to the response URL when the response body is not a valid URL', async () => {
433
- worker.use(
434
- http.get('https://brandtegrity.io/provider/flow/entry', () =>
435
- HttpResponse.redirect('https://brandtegrity.io/entry-redirected?session_id=abc123&short_app_id=app&language=hu-HU'),
436
- ),
437
- http.get('https://brandtegrity.io/entry-redirected', () => HttpResponse.text('<html>not a url</html>')),
438
- );
439
-
440
- const data = await flow.entry('https://brandtegrity.io/entry?challenge=abc123');
441
-
442
- expect(data).toEqual({ session_id: 'abc123', short_app_id: 'app', language: 'hu-HU' });
443
- });
444
- });
445
-
446
- test('flow state accessors', async () => {
447
- options = createOptions({ lazyLoad: true });
448
- flow = createNativeFlow(options);
449
-
450
- expect(flow.options).toBe(options);
451
- expect(flow.initialized).toBeFalsy();
452
- expect(flow.storage).toBe(options.storage);
453
- expect(flow.httpClient).toBe(options.httpClient);
454
- expect(flow.logging).toBe(options.logging);
455
- expect(flow.sessionId).toBeNull();
456
- expect(flow.shortAppId).toBeNull();
457
- expect(flow.language).toBe('en-US');
458
- await expect(flow.isAuthenticated).resolves.toBeFalsy();
459
- expect(flow.isAuthenticatedSync).toBeFalsy();
460
- expect(flow.accessToken).toBeNull();
461
- expect(flow.accessTokenExpired).toBeTruthy();
462
- expect(flow.accessTokenExpirationDate).toBeNull();
463
- expect(flow.refreshToken).toBeNull();
464
- expect(flow.idToken).toBeNull();
465
- expect(flow.idTokenClaims).toBeNull();
466
-
467
- await flow.init();
468
-
469
- flow.sessionId = 'abc123';
470
- flow.shortAppId = 'app';
471
- flow.language = 'hu-HU';
472
- flow.session = getMockStorage(options.storage).generateSession();
473
-
474
- expect(flow.initialized).toBeTruthy();
475
- expect(flow.sessionId).toBe('abc123');
476
- expect(flow.shortAppId).toBe('app');
477
- expect(flow.language).toBe('hu-HU');
478
- await expect(flow.isAuthenticated).resolves.toBeTruthy();
479
- expect(flow.isAuthenticatedSync).toBeTruthy();
480
- expect(flow.accessToken).toBe(flow.session.access_token);
481
- expect(flow.accessTokenExpired).toBeFalsy();
482
- expect(flow.accessTokenExpirationDate).toBe(flow.session.expires_at);
483
- expect(flow.refreshToken).toBe(flow.session.refresh_token);
484
- expect(flow.idToken).toBe(flow.session.id_token);
485
- expect(flow.idTokenClaims).toBe(flow.session?.claims);
486
- });
487
- });