@strivacity/sdk-angular 3.0.2 → 4.0.0-beta.0

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 (57) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +1991 -609
  3. package/dist/README.md +1991 -609
  4. package/dist/fesm2022/strivacity-sdk-angular-src-server.mjs +221 -0
  5. package/dist/fesm2022/strivacity-sdk-angular-src-server.mjs.map +1 -0
  6. package/dist/fesm2022/strivacity-sdk-angular-src-types.mjs +6 -0
  7. package/dist/fesm2022/strivacity-sdk-angular-src-types.mjs.map +1 -0
  8. package/dist/fesm2022/strivacity-sdk-angular.mjs +284 -498
  9. package/dist/fesm2022/strivacity-sdk-angular.mjs.map +1 -1
  10. package/dist/types/strivacity-sdk-angular-src-server.d.ts +82 -0
  11. package/dist/types/strivacity-sdk-angular-src-types.d.ts +41 -0
  12. package/dist/types/strivacity-sdk-angular.d.ts +147 -0
  13. package/eslint.config.mjs +31 -0
  14. package/ng-package.json +3 -3
  15. package/package.json +29 -11
  16. package/project.json +33 -0
  17. package/src/index.ts +8 -0
  18. package/src/lib/services/auth.service.ts +131 -0
  19. package/src/lib/services/index.ts +2 -0
  20. package/src/lib/services/native-login.service.ts +172 -0
  21. package/src/lib/storages.ts +12 -0
  22. package/src/lib/utils.ts +39 -0
  23. package/src/server/errors.ts +1 -0
  24. package/src/server/index.ts +6 -0
  25. package/src/server/ng-package.json +6 -0
  26. package/src/server/sdk.ts +113 -0
  27. package/src/server/session.ts +30 -0
  28. package/src/server/storages.ts +25 -0
  29. package/src/server/types.ts +32 -0
  30. package/src/server/utils.ts +74 -0
  31. package/src/types/index.ts +47 -0
  32. package/src/types/ng-package.json +6 -0
  33. package/testing/setup.ts +10 -0
  34. package/testing/tests/auth.service.spec.ts +236 -0
  35. package/testing/tests/index.spec.ts +193 -0
  36. package/testing/tests/native-login.service.spec.ts +311 -0
  37. package/testing/tests/server/errors.spec.ts +14 -0
  38. package/testing/tests/server/sdk.spec.ts +197 -0
  39. package/testing/tests/server/session.spec.ts +52 -0
  40. package/testing/tests/server/storages.spec.ts +58 -0
  41. package/testing/tests/server/utils.spec.ts +112 -0
  42. package/testing/tests/storages.spec.ts +31 -0
  43. package/testing/tests/utils.spec.ts +24 -0
  44. package/testing/utils/testbed.ts +26 -0
  45. package/tsconfig.lib.json +13 -0
  46. package/tsconfig.lib.prod.json +9 -0
  47. package/tsconfig.spec.json +8 -0
  48. package/vite.config.mts +11 -0
  49. package/dist/index.d.ts +0 -5
  50. package/dist/lib/components/login-renderer.component.d.ts +0 -38
  51. package/dist/lib/components/widget-renderer.component.d.ts +0 -16
  52. package/dist/lib/services/auth.service.d.ts +0 -93
  53. package/dist/lib/services/widget.service.d.ts +0 -25
  54. package/dist/lib/strivacity-auth.module.d.ts +0 -10
  55. package/dist/lib/utils/helpers.d.ts +0 -16
  56. package/dist/lib/utils/types.d.ts +0 -41
  57. package/dist/public-api.d.ts +0 -16
@@ -0,0 +1,311 @@
1
+ import type { NativeFlow, NativeFlowState, SessionData } from '../../src/types';
2
+ import { inject } from '@angular/core';
3
+ import { TestBed } from '@angular/core/testing';
4
+ import { describe, test, expect, vi, beforeEach } from 'vitest';
5
+ import { initFlow } from '@strivacity/sdk-core';
6
+ import { FallbackError } from '@strivacity/sdk-core/utils/errors';
7
+ import { StrivacityAuthService } from '../../src/lib/services/auth.service';
8
+ import { StrivacityNativeLoginService } from '../../src/lib/services/native-login.service';
9
+ import { STRIVACITY_SDK } from '../../src/lib/utils';
10
+ import { createMockFlow, createOptions } from '@strivacity/testing/mocks/sdk';
11
+ import { mountWithProviders } from '../utils/testbed';
12
+
13
+ vi.mock('@strivacity/sdk-core', async (importOriginal) => ({
14
+ ...(await importOriginal<typeof import('@strivacity/sdk-core')>()),
15
+ initFlow: vi.fn(),
16
+ }));
17
+
18
+ function configure(flow: NativeFlow): StrivacityNativeLoginService {
19
+ vi.mocked(initFlow).mockReturnValue(flow);
20
+
21
+ TestBed.configureTestingModule({
22
+ providers: [{ provide: STRIVACITY_SDK, useValue: createOptions({ mode: 'native' }) }, StrivacityAuthService, StrivacityNativeLoginService],
23
+ });
24
+
25
+ return TestBed.inject(StrivacityNativeLoginService);
26
+ }
27
+
28
+ beforeEach(() => {
29
+ vi.mocked(initFlow).mockReset();
30
+ });
31
+
32
+ describe('StrivacityNativeLoginService', () => {
33
+ describe('start', () => {
34
+ test('initializes the sdk and starts a session, defaulting params to an empty object', async () => {
35
+ const flow = createMockFlow<NativeFlow>();
36
+ const login = configure(flow);
37
+
38
+ await login.start();
39
+
40
+ expect(flow.init).toHaveBeenCalledTimes(1);
41
+ expect(flow.startSession).toHaveBeenCalledWith({});
42
+ });
43
+
44
+ test('forwards the given login params to sdk.startSession', async () => {
45
+ const flow = createMockFlow<NativeFlow>();
46
+ const login = configure(flow);
47
+
48
+ await login.start({ params: { sessionId: 'session-1', language: 'hu-HU' } });
49
+
50
+ expect(flow.startSession).toHaveBeenCalledWith({ sessionId: 'session-1', language: 'hu-HU' });
51
+ });
52
+
53
+ test('is loading until sdk.startSession resolves with a state that has no pending finalizeUrl', async () => {
54
+ const flow = createMockFlow<NativeFlow>();
55
+ vi.mocked(flow.startSession).mockResolvedValue({ screen: 'identifier' });
56
+ const login = configure(flow);
57
+
58
+ const pending = login.start();
59
+
60
+ expect(login.loading()).toBe(true);
61
+
62
+ await pending;
63
+
64
+ expect(login.loading()).toBe(false);
65
+ });
66
+
67
+ test('stays loading forever if sdk.startSession resolves with no state at all', async () => {
68
+ const flow = createMockFlow<NativeFlow>();
69
+ vi.mocked(flow.startSession).mockResolvedValue(undefined);
70
+ const login = configure(flow);
71
+
72
+ await login.start();
73
+
74
+ expect(login.loading()).toBe(true);
75
+ });
76
+
77
+ test('applies the returned flow state: resets per-form data, routes the global message, and keeps per-widget messages', async () => {
78
+ const state: NativeFlowState = {
79
+ hostedUrl: 'https://brandtegrity.io/hosted',
80
+ screen: 'identifier',
81
+ forms: [{ id: 'form1', type: 'form', widgets: [] }],
82
+ messages: {
83
+ global: { type: 'info', text: 'Welcome' },
84
+ form1: { field1: { type: 'error', text: 'Required' } },
85
+ },
86
+ };
87
+ const flow = createMockFlow<NativeFlow>();
88
+ vi.mocked(flow.startSession).mockResolvedValue(state);
89
+ const onGlobalMessage = vi.fn();
90
+ const login = configure(flow);
91
+
92
+ await login.start({ onGlobalMessage });
93
+
94
+ expect(onGlobalMessage).toHaveBeenCalledWith({ type: 'info', text: 'Welcome' });
95
+ expect(login.forms()).toEqual({ form1: {} });
96
+ expect(login.messages()).toEqual({ form1: { field1: { type: 'error', text: 'Required' } } });
97
+ expect(login.state()).toEqual(state);
98
+ });
99
+
100
+ test('keeps loading while a finalizeUrl is still pending', async () => {
101
+ const flow = createMockFlow<NativeFlow>();
102
+ vi.mocked(flow.startSession).mockResolvedValue({ finalizeUrl: 'https://brandtegrity.io/finalize' });
103
+ const login = configure(flow);
104
+
105
+ await login.start();
106
+
107
+ expect(login.loading()).toBe(true);
108
+ });
109
+
110
+ test('calls onLogin and leaves the tracked state untouched when the sdk already has a session', async () => {
111
+ const session = { access_token: 'access-token' } as unknown as SessionData;
112
+ const flow = createMockFlow<NativeFlow>({ session });
113
+ vi.mocked(flow.startSession).mockResolvedValue({ screen: 'identifier' });
114
+ const onLogin = vi.fn();
115
+ const login = configure(flow);
116
+
117
+ await login.start({ onLogin });
118
+
119
+ expect(onLogin).toHaveBeenCalledWith(session);
120
+ expect(login.state()).toEqual({});
121
+ });
122
+
123
+ test('routes a FallbackError from sdk.startSession to onFallback and logs it', async () => {
124
+ const flow = createMockFlow<NativeFlow>();
125
+ const fallbackError = new FallbackError(new URL('https://brandtegrity.io/hosted'));
126
+ vi.mocked(flow.startSession).mockRejectedValue(fallbackError);
127
+ const onFallback = vi.fn();
128
+ const login = configure(flow);
129
+
130
+ await login.start({ onFallback });
131
+
132
+ expect(onFallback).toHaveBeenCalledWith(fallbackError);
133
+ expect(flow.logging!.error).toHaveBeenCalledWith('Fallback error occurred', fallbackError);
134
+ expect(login.loading()).toBe(true);
135
+ });
136
+
137
+ test('routes a non-fallback error from sdk.startSession to onError and logs it', async () => {
138
+ const flow = createMockFlow<NativeFlow>();
139
+ const error = new Error('network down');
140
+ vi.mocked(flow.startSession).mockRejectedValue(error);
141
+ const onError = vi.fn();
142
+ const login = configure(flow);
143
+
144
+ await login.start({ onError });
145
+
146
+ expect(onError).toHaveBeenCalledWith(error);
147
+ expect(flow.logging!.error).toHaveBeenCalledWith('Error starting session', error);
148
+ // startSession() has its own catch-all and never rethrows, so start()'s "set loading false on error" branch never runs for this path
149
+ expect(login.loading()).toBe(true);
150
+ });
151
+
152
+ test('propagates a rejection from sdk.init() as-is, since only the startSession() call is guarded by the try/catch', async () => {
153
+ // the AbortError short-circuit in start()'s own catch guards `this.startSession(...)`, not `await this.sdk.init()`
154
+ // above it - startSession() also swallows every error itself (including AbortError) and reports it through onError,
155
+ // so that branch is unreachable via a mocked flow; a rejecting init() is the one thing genuinely uncaught here.
156
+ const flow = createMockFlow<NativeFlow>();
157
+ const abortError = new DOMException('The operation was aborted', 'AbortError');
158
+ vi.mocked(flow.init).mockRejectedValue(abortError);
159
+ const onError = vi.fn();
160
+ const login = configure(flow);
161
+
162
+ await expect(login.start({ onError })).rejects.toBe(abortError);
163
+
164
+ expect(onError).not.toHaveBeenCalled();
165
+ expect(flow.startSession).not.toHaveBeenCalled();
166
+ });
167
+ });
168
+
169
+ describe('submitForm', () => {
170
+ test('unflattens the tracked form values and calls through to sdk.submitForm', async () => {
171
+ const flow = createMockFlow<NativeFlow>();
172
+ const initialForms = [{ id: 'form1', type: 'form' as const, widgets: [] }];
173
+ vi.mocked(flow.startSession).mockResolvedValue({ screen: 'identifier', forms: initialForms });
174
+ vi.mocked(flow.submitForm).mockResolvedValue({ screen: 'done' });
175
+ const login = configure(flow);
176
+
177
+ await login.start();
178
+
179
+ login.setFormValue('form1', 'address.city', 'Budapest');
180
+ login.setFormValue('form1', 'address.zip', '');
181
+
182
+ await login.submitForm('form1');
183
+
184
+ expect(flow.submitForm).toHaveBeenCalledWith('form1', { address: { city: 'Budapest', zip: null } });
185
+ // unspecified fields on the new state fall back to what was already tracked (except messages, which always resets)
186
+ expect(login.state()).toEqual({
187
+ hostedUrl: undefined,
188
+ finalizeUrl: undefined,
189
+ screen: 'done',
190
+ forms: initialForms,
191
+ layout: undefined,
192
+ messages: {},
193
+ branding: undefined,
194
+ });
195
+ });
196
+
197
+ test('uses a given custom body instead of the tracked form values', async () => {
198
+ const flow = createMockFlow<NativeFlow>();
199
+ vi.mocked(flow.submitForm).mockResolvedValue({ screen: 'done' });
200
+ const login = configure(flow);
201
+ await login.start();
202
+
203
+ await login.submitForm('form1', { raw: true });
204
+
205
+ expect(flow.submitForm).toHaveBeenCalledWith('form1', { raw: true });
206
+ });
207
+
208
+ test('routes a FallbackError to onFallback', async () => {
209
+ const flow = createMockFlow<NativeFlow>();
210
+ const fallbackError = new FallbackError(new URL('https://brandtegrity.io/hosted'));
211
+ vi.mocked(flow.submitForm).mockRejectedValue(fallbackError);
212
+ const onFallback = vi.fn();
213
+ const login = configure(flow);
214
+ await login.start({ onFallback });
215
+
216
+ await login.submitForm('form1');
217
+
218
+ expect(onFallback).toHaveBeenCalledWith(fallbackError);
219
+ expect(flow.logging!.error).toHaveBeenCalledWith('Fallback error occurred', fallbackError);
220
+ });
221
+
222
+ test('routes a non-fallback error to onError', async () => {
223
+ const flow = createMockFlow<NativeFlow>();
224
+ const error = new Error('submit failed');
225
+ vi.mocked(flow.submitForm).mockRejectedValue(error);
226
+ const onError = vi.fn();
227
+ const login = configure(flow);
228
+ await login.start({ onError });
229
+
230
+ await login.submitForm('form1');
231
+
232
+ expect(onError).toHaveBeenCalledWith(error);
233
+ expect(flow.logging!.error).toHaveBeenCalledWith('Error submitting form', error);
234
+ });
235
+ });
236
+
237
+ test('setFormValue coerces empty strings to null and lazily creates the form bucket', async () => {
238
+ const flow = createMockFlow<NativeFlow>();
239
+ const login = configure(flow);
240
+ await login.start();
241
+
242
+ login.setFormValue('newForm', 'field', 'value');
243
+ login.setFormValue('newForm', 'other', '');
244
+
245
+ expect(login.forms().newForm).toEqual({ field: 'value', other: null });
246
+ });
247
+
248
+ test('setMessage lazily creates the message bucket for a form', async () => {
249
+ const flow = createMockFlow<NativeFlow>();
250
+ const login = configure(flow);
251
+ await login.start();
252
+
253
+ login.setMessage('newForm', 'field', { type: 'error', text: 'Required' });
254
+
255
+ expect(login.messages().newForm).toEqual({ field: { type: 'error', text: 'Required' } });
256
+ });
257
+
258
+ describe('triggerFallback', () => {
259
+ test('throws when no hosted URL is known yet', async () => {
260
+ const flow = createMockFlow<NativeFlow>();
261
+ vi.mocked(flow.startSession).mockResolvedValue({ screen: 'identifier' });
262
+ const login = configure(flow);
263
+ await login.start();
264
+
265
+ expect(() => login.triggerFallback()).toThrow('No hosted URL provided');
266
+ expect(flow.logging!.error).toHaveBeenCalledWith('Fallback error', expect.any(Error));
267
+ });
268
+
269
+ test('calls onFallback with a FallbackError built from the known hosted URL', async () => {
270
+ const flow = createMockFlow<NativeFlow>();
271
+ vi.mocked(flow.startSession).mockResolvedValue({ hostedUrl: 'https://brandtegrity.io/hosted', screen: 'identifier' });
272
+ const onFallback = vi.fn();
273
+ const login = configure(flow);
274
+ await login.start({ onFallback });
275
+
276
+ login.triggerFallback('manual trigger');
277
+
278
+ expect(flow.logging!.warn).toHaveBeenCalledWith('Triggering fallback due to: manual trigger');
279
+ expect(onFallback).toHaveBeenCalledWith(expect.any(FallbackError));
280
+ expect((onFallback.mock.calls[0][0] as FallbackError).url.toString()).toBe('https://brandtegrity.io/hosted');
281
+ });
282
+ });
283
+
284
+ test('triggerClose calls onClose and logs', async () => {
285
+ const flow = createMockFlow<NativeFlow>();
286
+ const onClose = vi.fn();
287
+ const login = configure(flow);
288
+ await login.start({ onClose });
289
+
290
+ login.triggerClose();
291
+
292
+ expect(onClose).toHaveBeenCalledTimes(1);
293
+ expect(flow.logging!.debug).toHaveBeenCalledWith('Triggering close');
294
+ });
295
+
296
+ test('aborts the in-flight abort controller when the owning injector is destroyed', () => {
297
+ const flow = createMockFlow<NativeFlow>();
298
+ vi.mocked(initFlow).mockReturnValue(flow);
299
+ TestBed.configureTestingModule({ providers: [{ provide: STRIVACITY_SDK, useValue: createOptions({ mode: 'native' }) }, StrivacityAuthService] });
300
+
301
+ const abortSpy = vi.spyOn(AbortController.prototype, 'abort');
302
+
303
+ // both services must live on the component's own injector for fixture.destroy() to fire their DestroyRef callbacks
304
+ const { destroy } = mountWithProviders([StrivacityAuthService, StrivacityNativeLoginService], () => inject(StrivacityNativeLoginService));
305
+
306
+ destroy();
307
+
308
+ expect(abortSpy).toHaveBeenCalledTimes(1);
309
+ abortSpy.mockRestore();
310
+ });
311
+ });
@@ -0,0 +1,14 @@
1
+ import { test, expect } from 'vitest';
2
+ import * as angularErrors from '../../../src/server/errors';
3
+ import * as coreErrors from '@strivacity/sdk-core/utils/errors';
4
+
5
+ test('re-exports error classes from the core sdk', () => {
6
+ const coreKeys = Object.keys(coreErrors);
7
+
8
+ expect(coreKeys.length).toBeGreaterThan(0);
9
+ expect(Object.keys(angularErrors).sort()).toEqual(coreKeys.sort());
10
+
11
+ for (const key of coreKeys) {
12
+ expect(angularErrors[key as keyof typeof angularErrors]).toBe(coreErrors[key as keyof typeof coreErrors]);
13
+ }
14
+ });
@@ -0,0 +1,197 @@
1
+ import type { Router } from 'express';
2
+ import type { AngularServerRequest } from '../../../src/server/types';
3
+ import { describe, test, expect, vi, beforeEach } from 'vitest';
4
+ import { createBaseServerSDK } from '@strivacity/sdk-core/server';
5
+ import { applyResponse } from '../../../src/server/utils';
6
+ import { createServerSDK } from '../../../src/server/sdk';
7
+ import { createServerOptions } from '@strivacity/testing/mocks/sdk';
8
+
9
+ vi.mock('@strivacity/sdk-core/server', async (importOriginal) => ({
10
+ ...(await importOriginal<typeof import('@strivacity/sdk-core/server')>()),
11
+ createBaseServerSDK: vi.fn(),
12
+ }));
13
+
14
+ vi.mock('../../../src/server/utils', async (importOriginal) => ({
15
+ ...(await importOriginal<typeof import('../../../src/server/utils')>()),
16
+ applyResponse: vi.fn().mockResolvedValue(undefined),
17
+ }));
18
+
19
+ function createBase() {
20
+ return {
21
+ options: { clientId: 'client-id' },
22
+ getSession: vi.fn().mockResolvedValue(null),
23
+ updateSession: vi.fn().mockResolvedValue(undefined),
24
+ refreshSession: vi.fn().mockResolvedValue({ access_token: 'refreshed' }),
25
+ revokeSession: vi.fn().mockResolvedValue(undefined),
26
+ getEntrySession: vi.fn().mockResolvedValue({ sessionId: 'session-1' }),
27
+ completeLogin: vi.fn().mockResolvedValue({ access_token: 'access-token' }),
28
+ logout: vi.fn().mockResolvedValue(undefined),
29
+ handleLogin: vi.fn(),
30
+ handleRegister: vi.fn(),
31
+ handleCallback: vi.fn(),
32
+ handleRefresh: vi.fn(),
33
+ handleRevoke: vi.fn(),
34
+ handleEntry: vi.fn(),
35
+ handleLogout: vi.fn(),
36
+ handleBackChannelLogout: vi.fn(),
37
+ handler: vi.fn().mockResolvedValue(new Response(null)),
38
+ };
39
+ }
40
+
41
+ function getRouteHandler(router: Router, method: string, path: string) {
42
+ const layer = (
43
+ router.stack as Array<{ route?: { path: string; methods: Record<string, boolean>; stack: Array<{ handle: (...args: Array<unknown>) => unknown }> } }>
44
+ ).find((candidate) => candidate.route?.path === path && candidate.route.methods[method]);
45
+
46
+ if (!layer?.route) {
47
+ throw new Error(`No ${method.toUpperCase()} ${path} route registered`);
48
+ }
49
+
50
+ return layer.route.stack[0].handle as (req: unknown, res: unknown, next: (error?: unknown) => void) => Promise<void>;
51
+ }
52
+
53
+ beforeEach(() => {
54
+ vi.mocked(createBaseServerSDK).mockReset();
55
+ vi.mocked(applyResponse).mockClear();
56
+ });
57
+
58
+ describe('createServerSDK', () => {
59
+ test('creates the base server sdk with a toRequest adapter that converts Express requests to standard Requests, and the given config', () => {
60
+ const base = createBase();
61
+ vi.mocked(createBaseServerSDK).mockReturnValue(base as never);
62
+ const initConfig = createServerOptions();
63
+
64
+ createServerSDK(initConfig);
65
+
66
+ expect(createBaseServerSDK).toHaveBeenCalledTimes(1);
67
+ const [adapter, forwardedConfig] = vi.mocked(createBaseServerSDK).mock.calls[0];
68
+
69
+ expect(forwardedConfig).toBe(initConfig);
70
+
71
+ const req = {
72
+ originalUrl: '/callback?code=abc',
73
+ protocol: 'https',
74
+ method: 'GET',
75
+ headers: {},
76
+ get: () => 'brandtegrity.io',
77
+ } as unknown as AngularServerRequest;
78
+ const webRequest = adapter.toRequest(req);
79
+
80
+ expect(webRequest.url).toBe('https://brandtegrity.io/callback?code=abc');
81
+ });
82
+
83
+ test('passes an already-standard Request straight through the toRequest adapter', () => {
84
+ const base = createBase();
85
+ vi.mocked(createBaseServerSDK).mockReturnValue(base as never);
86
+ createServerSDK(createServerOptions());
87
+
88
+ const [adapter] = vi.mocked(createBaseServerSDK).mock.calls[0];
89
+ const request = new Request('https://brandtegrity.io');
90
+
91
+ expect(adapter.toRequest(request)).toBe(request);
92
+ });
93
+
94
+ test('exposes the underlying options', () => {
95
+ const base = createBase();
96
+ vi.mocked(createBaseServerSDK).mockReturnValue(base as never);
97
+
98
+ const sdk = createServerSDK(createServerOptions());
99
+
100
+ expect(sdk.options).toBe(base.options);
101
+ });
102
+
103
+ test.each([
104
+ ['getSession', [{} as never], null],
105
+ ['updateSession', [{ access_token: 'a' } as never, {} as never], undefined],
106
+ ['refreshSession', [{} as never], { access_token: 'refreshed' }],
107
+ ['revokeSession', [{} as never], undefined],
108
+ ['getEntrySession', ['https://brandtegrity.io/entry'], { sessionId: 'session-1' }],
109
+ ['completeLogin', [{ code: 'abc' }, {} as never], { access_token: 'access-token' }],
110
+ ['logout', ['https://brandtegrity.io', {} as never], undefined],
111
+ ['handleLogin', [{} as never], new Response(null)],
112
+ ['handleRegister', [{} as never], new Response(null)],
113
+ ['handleCallback', [{} as never], new Response(null)],
114
+ ['handleRefresh', [{} as never], new Response(null)],
115
+ ['handleRevoke', [{} as never], new Response(null)],
116
+ ['handleEntry', [{} as never], new Response(null)],
117
+ ['handleLogout', [{} as never], new Response(null)],
118
+ ['handleBackChannelLogout', [{} as never], new Response(null)],
119
+ ] as const)('%s calls through to the base server sdk with the same arguments and return value', async (method, args) => {
120
+ const base = createBase();
121
+ vi.mocked(createBaseServerSDK).mockReturnValue(base as never);
122
+ const sdk = createServerSDK(createServerOptions());
123
+
124
+ const result = await (sdk[method as keyof typeof sdk] as (...a: Array<unknown>) => unknown)(...args);
125
+ const baseMock = base[method as keyof typeof base] as ReturnType<typeof vi.fn>;
126
+
127
+ expect(baseMock).toHaveBeenCalledWith(...args);
128
+ expect(result).toEqual(await baseMock.mock.results[0].value);
129
+ });
130
+
131
+ test('handler calls through to the base server sdk', async () => {
132
+ const base = createBase();
133
+ vi.mocked(createBaseServerSDK).mockReturnValue(base as never);
134
+ const sdk = createServerSDK(createServerOptions());
135
+ const req = {} as AngularServerRequest;
136
+
137
+ const result = await sdk.handler(req);
138
+
139
+ expect(base.handler).toHaveBeenCalledWith(req);
140
+ expect(result).toBe(await base.handler.mock.results[0].value);
141
+ });
142
+
143
+ describe('handlers router', () => {
144
+ test.each([
145
+ ['GET', '/login', 'handleLogin'],
146
+ ['GET', '/register', 'handleRegister'],
147
+ ['GET', '/callback', 'handleCallback'],
148
+ ['GET', '/refresh', 'handleRefresh'],
149
+ ['GET', '/revoke', 'handleRevoke'],
150
+ ['GET', '/entry', 'handleEntry'],
151
+ ['GET', '/logout', 'handleLogout'],
152
+ ['POST', '/backchannel-logout', 'handleBackChannelLogout'],
153
+ ] as const)('%s %s calls base.%s with the request and applies the resulting response', async (method, path, baseMethod) => {
154
+ const base = createBase();
155
+ const response = new Response(null, { status: 200 });
156
+ vi.mocked(base[baseMethod]).mockResolvedValue(response);
157
+ vi.mocked(createBaseServerSDK).mockReturnValue(base as never);
158
+
159
+ const sdk = createServerSDK(createServerOptions());
160
+ const handler = getRouteHandler(sdk.handlers, method.toLowerCase(), path);
161
+ const req = { id: 'req' };
162
+ const res = { id: 'res' };
163
+ const next = vi.fn();
164
+
165
+ await handler(req, res, next);
166
+
167
+ expect(base[baseMethod]).toHaveBeenCalledWith(req);
168
+ expect(applyResponse).toHaveBeenCalledWith(response, res);
169
+ expect(next).not.toHaveBeenCalled();
170
+ });
171
+
172
+ test.each([
173
+ ['GET', '/login', 'handleLogin'],
174
+ ['GET', '/register', 'handleRegister'],
175
+ ['GET', '/callback', 'handleCallback'],
176
+ ['GET', '/refresh', 'handleRefresh'],
177
+ ['GET', '/revoke', 'handleRevoke'],
178
+ ['GET', '/entry', 'handleEntry'],
179
+ ['GET', '/logout', 'handleLogout'],
180
+ ['POST', '/backchannel-logout', 'handleBackChannelLogout'],
181
+ ] as const)('%s %s forwards a rejection from base.%s to next() instead of applying a response', async (method, path, baseMethod) => {
182
+ const base = createBase();
183
+ const error = new Error('boom');
184
+ vi.mocked(base[baseMethod]).mockRejectedValue(error);
185
+ vi.mocked(createBaseServerSDK).mockReturnValue(base as never);
186
+
187
+ const sdk = createServerSDK(createServerOptions());
188
+ const handler = getRouteHandler(sdk.handlers, method.toLowerCase(), path);
189
+ const next = vi.fn();
190
+
191
+ await handler({}, {}, next);
192
+
193
+ expect(next).toHaveBeenCalledWith(error);
194
+ expect(applyResponse).not.toHaveBeenCalled();
195
+ });
196
+ });
197
+ });
@@ -0,0 +1,52 @@
1
+ import type { AngularServerSDK } from '../../../src/server/types';
2
+ import { ApplicationInitStatus, REQUEST, TransferState } from '@angular/core';
3
+ import { TestBed } from '@angular/core/testing';
4
+ import { describe, test, expect, vi } from 'vitest';
5
+ import { SESSION_TRANSFER_KEY, provideStrivacityServerSession } from '../../../src/server/session';
6
+
7
+ function configure(request: Request | undefined, serverSdk: AngularServerSDK | undefined) {
8
+ TestBed.configureTestingModule({
9
+ providers: [{ provide: REQUEST, useValue: request }, provideStrivacityServerSession(serverSdk)],
10
+ });
11
+
12
+ return TestBed.inject(ApplicationInitStatus).donePromise;
13
+ }
14
+
15
+ describe('provideStrivacityServerSession', () => {
16
+ test('loads the session for the incoming request and hands it over via TransferState', async () => {
17
+ const request = new Request('https://brandtegrity.io');
18
+ const session = { access_token: 'access-token' };
19
+ const getSession = vi.fn().mockResolvedValue(session);
20
+
21
+ await configure(request, { getSession } as unknown as AngularServerSDK);
22
+
23
+ expect(getSession).toHaveBeenCalledWith(request);
24
+ expect(TestBed.inject(TransferState).get(SESSION_TRANSFER_KEY, undefined)).toEqual(session);
25
+ });
26
+
27
+ test('transfers null when the server sdk finds no session for the request', async () => {
28
+ const request = new Request('https://brandtegrity.io');
29
+ const getSession = vi.fn().mockResolvedValue(null);
30
+
31
+ await configure(request, { getSession } as unknown as AngularServerSDK);
32
+
33
+ expect(TestBed.inject(TransferState).get(SESSION_TRANSFER_KEY, undefined)).toBeNull();
34
+ });
35
+
36
+ test('is a no-op when there is no incoming request (e.g. not running under SSR)', async () => {
37
+ const getSession = vi.fn().mockResolvedValue({ access_token: 'access-token' });
38
+
39
+ await configure(undefined, { getSession } as unknown as AngularServerSDK);
40
+
41
+ expect(getSession).not.toHaveBeenCalled();
42
+ expect(TestBed.inject(TransferState).hasKey(SESSION_TRANSFER_KEY)).toBe(false);
43
+ });
44
+
45
+ test('is a no-op when no server sdk was configured (serverSessionUri unset)', async () => {
46
+ const request = new Request('https://brandtegrity.io');
47
+
48
+ await configure(request, undefined);
49
+
50
+ expect(TestBed.inject(TransferState).hasKey(SESSION_TRANSFER_KEY)).toBe(false);
51
+ });
52
+ });
@@ -0,0 +1,58 @@
1
+ import type { Request as ExpressRequest } from 'express';
2
+ import { describe, test, expect, vi, beforeEach } from 'vitest';
3
+ import { createMockStorage } from '@strivacity/testing/mocks/sdk';
4
+ import * as coreStorages from '@strivacity/sdk-core/storages';
5
+ import { createSessionIdCookieStorage as createSessionIdCookieStorageBase } from '@strivacity/sdk-core/storages/server';
6
+
7
+ vi.mock('@strivacity/sdk-core/storages/server', async (importOriginal) => ({
8
+ ...(await importOriginal<typeof import('@strivacity/sdk-core/storages/server')>()),
9
+ createSessionIdCookieStorage: vi.fn(),
10
+ }));
11
+
12
+ beforeEach(() => {
13
+ vi.mocked(createSessionIdCookieStorageBase).mockReset();
14
+ });
15
+
16
+ describe('server storages', () => {
17
+ test('re-exports everything from the core storages barrel, with createSessionIdCookieStorage overridden by the local wrapper', async () => {
18
+ const angularStorages = await import('../../../src/server/storages');
19
+ const coreKeys = Object.keys(coreStorages);
20
+
21
+ expect(coreKeys.length).toBeGreaterThan(0);
22
+ expect(Object.keys(angularStorages).sort()).toEqual(coreKeys.sort());
23
+
24
+ for (const key of coreKeys) {
25
+ if (key === 'createSessionIdCookieStorage') {
26
+ continue;
27
+ }
28
+
29
+ expect(angularStorages[key as keyof typeof angularStorages]).toBe(coreStorages[key as keyof typeof coreStorages]);
30
+ }
31
+
32
+ expect(angularStorages.createSessionIdCookieStorage).not.toBe(coreStorages.createSessionIdCookieStorage);
33
+ });
34
+
35
+ test('createSessionIdCookieStorage forwards the storage and options to the core implementation, with an adapter converting Express requests to standard Requests', async () => {
36
+ const angularStorages = await import('../../../src/server/storages');
37
+ const wrapped = { get: vi.fn() };
38
+ vi.mocked(createSessionIdCookieStorageBase).mockReturnValue(wrapped as never);
39
+ const storage = createMockStorage();
40
+ const options = { defaultCookieOptions: { path: '/' } };
41
+
42
+ const result = angularStorages.createSessionIdCookieStorage(storage, options);
43
+
44
+ expect(result).toBe(wrapped);
45
+ expect(createSessionIdCookieStorageBase).toHaveBeenCalledTimes(1);
46
+
47
+ const [adapter, forwardedStorage, forwardedOptions] = vi.mocked(createSessionIdCookieStorageBase).mock.calls[0];
48
+
49
+ expect(forwardedStorage).toBe(storage);
50
+ expect(forwardedOptions).toBe(options);
51
+
52
+ const req = { originalUrl: '/callback', protocol: 'https', method: 'GET', headers: {}, get: () => 'brandtegrity.io' } as unknown as ExpressRequest;
53
+ const webRequest = adapter.toRequest(req);
54
+
55
+ expect(webRequest.url).toBe('https://brandtegrity.io/callback');
56
+ expect(webRequest.method).toBe('GET');
57
+ });
58
+ });