@strivacity/sdk-vue 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.
- package/CHANGELOG.md +20 -18
- package/README.md +1402 -419
- package/dist/composables.cjs +2 -2
- package/dist/composables.cjs.map +1 -1
- package/dist/composables.d.ts +23 -8
- package/dist/composables.mjs +2 -2
- package/dist/composables.mjs.map +1 -1
- package/dist/errors.cjs +1 -0
- package/dist/errors.d.ts +1 -0
- package/dist/errors.mjs +1 -0
- package/dist/index.cjs +1 -2
- package/dist/index.d.ts +9 -31
- package/dist/index.mjs +1 -2
- package/dist/plugin.cjs +2 -0
- package/dist/plugin.cjs.map +1 -0
- package/dist/plugin.d.ts +8 -0
- package/dist/plugin.mjs +2 -0
- package/dist/plugin.mjs.map +1 -0
- package/dist/storages.cjs +1 -0
- package/dist/storages.d.ts +1 -0
- package/dist/storages.mjs +1 -0
- package/dist/types.cjs +1 -2
- package/dist/types.d.ts +106 -118
- package/dist/types.mjs +1 -2
- package/dist/utils.cjs +1 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.mjs +1 -0
- package/eslint.config.mjs +3 -0
- package/package.json +45 -8
- package/project.json +46 -0
- package/src/composables.ts +219 -0
- package/src/env.d.ts +8 -0
- package/src/errors.ts +1 -0
- package/src/index.ts +9 -0
- package/src/plugin.ts +89 -0
- package/src/storages.ts +1 -0
- package/src/types.ts +220 -0
- package/src/utils.ts +1 -0
- package/testing/tests/composables.spec.ts +328 -0
- package/testing/tests/errors.spec.ts +10 -0
- package/testing/tests/index.spec.ts +105 -0
- package/testing/tests/plugin.spec.ts +129 -0
- package/testing/tests/storages.spec.ts +10 -0
- package/testing/tests/utils.spec.ts +10 -0
- package/testing/utils/common.ts +53 -0
- package/tsconfig.app.json +4 -0
- package/tsconfig.json +3 -0
- package/vite.config.mts +53 -0
- package/dist/assets/login-renderer.vue_vue_type_script_setup_true_lang.cjs +0 -2
- package/dist/assets/login-renderer.vue_vue_type_script_setup_true_lang.cjs.map +0 -1
- package/dist/assets/login-renderer.vue_vue_type_script_setup_true_lang.mjs +0 -2
- package/dist/assets/login-renderer.vue_vue_type_script_setup_true_lang.mjs.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/login-renderer.cjs +0 -2
- package/dist/login-renderer.cjs.map +0 -1
- package/dist/login-renderer.mjs +0 -2
- package/dist/login-renderer.mjs.map +0 -1
- package/dist/login-renderer.vue.d.ts +0 -37
- package/dist/types.cjs.map +0 -1
- package/dist/types.mjs.map +0 -1
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import type { SDKContext, LoginContext, NativeFlow, NativeFlowState, SessionData } from '../../src/types';
|
|
2
|
+
import { describe, test, expect, vi } from 'vitest';
|
|
3
|
+
import { createApp, defineComponent, h } from 'vue';
|
|
4
|
+
import { FallbackError } from '@strivacity/sdk-core/utils/errors';
|
|
5
|
+
import { STRIVACITY_SDK, useStrivacity, useNativeLoginContext, useNativeLogin } from '../../src/composables';
|
|
6
|
+
import { flushPromises } from '@strivacity/testing/mocks/common';
|
|
7
|
+
import { createMockFlow } from '@strivacity/testing/mocks/sdk';
|
|
8
|
+
import { mount, mountWithNativeLogin } from '../utils/common';
|
|
9
|
+
|
|
10
|
+
describe('useStrivacity', () => {
|
|
11
|
+
test('throws when used outside of a Strivacity SDK provider', () => {
|
|
12
|
+
expect(() => {
|
|
13
|
+
const app = createApp({
|
|
14
|
+
setup() {
|
|
15
|
+
useStrivacity();
|
|
16
|
+
|
|
17
|
+
return () => null;
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
app.mount(document.createElement('div'));
|
|
21
|
+
}).toThrow('Missing SDK context');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('returns the context provided by createStrivacitySDK', () => {
|
|
25
|
+
const sdk = {} as NativeFlow;
|
|
26
|
+
let ctx: SDKContext<NativeFlow> | undefined;
|
|
27
|
+
|
|
28
|
+
mount(sdk, () => {
|
|
29
|
+
ctx = useStrivacity<NativeFlow>();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(ctx?.sdk).toBe(sdk);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('useNativeLoginContext', () => {
|
|
37
|
+
test('throws when used outside of a native login session', () => {
|
|
38
|
+
expect(() => {
|
|
39
|
+
const app = createApp({
|
|
40
|
+
setup() {
|
|
41
|
+
useNativeLoginContext();
|
|
42
|
+
|
|
43
|
+
return () => null;
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
app.mount(document.createElement('div'));
|
|
47
|
+
}).toThrow('Missing SDK native login context');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('resolves to the context provided by an ancestor useNativeLogin call', async () => {
|
|
51
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
52
|
+
let injected: LoginContext | undefined;
|
|
53
|
+
let login!: LoginContext;
|
|
54
|
+
|
|
55
|
+
const Child = defineComponent({
|
|
56
|
+
setup() {
|
|
57
|
+
injected = useNativeLoginContext();
|
|
58
|
+
|
|
59
|
+
return () => null;
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
const Parent = defineComponent({
|
|
63
|
+
setup() {
|
|
64
|
+
login = useNativeLogin();
|
|
65
|
+
|
|
66
|
+
return () => h(Child);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const app = createApp(Parent);
|
|
71
|
+
app.provide(STRIVACITY_SDK, { sdk });
|
|
72
|
+
app.mount(document.createElement('div'));
|
|
73
|
+
await flushPromises();
|
|
74
|
+
|
|
75
|
+
expect(injected?.state).toBe(login.state);
|
|
76
|
+
expect(injected?.loading).toBe(login.loading);
|
|
77
|
+
expect(injected?.forms).toBe(login.forms);
|
|
78
|
+
expect(injected?.messages).toBe(login.messages);
|
|
79
|
+
expect(injected?.submitForm).toBe(login.submitForm);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('useNativeLogin', () => {
|
|
84
|
+
test('initializes the sdk and starts a session on mount, defaulting params to an empty object', async () => {
|
|
85
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
86
|
+
mountWithNativeLogin(sdk);
|
|
87
|
+
|
|
88
|
+
await flushPromises();
|
|
89
|
+
|
|
90
|
+
expect(sdk.init).toHaveBeenCalledTimes(1);
|
|
91
|
+
expect(sdk.startSession).toHaveBeenCalledWith({});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('forwards the given login params to sdk.startSession', async () => {
|
|
95
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
96
|
+
mountWithNativeLogin(sdk, { params: { sessionId: 'session-1', language: 'hu-HU' } });
|
|
97
|
+
|
|
98
|
+
await flushPromises();
|
|
99
|
+
|
|
100
|
+
expect(sdk.startSession).toHaveBeenCalledWith({ sessionId: 'session-1', language: 'hu-HU' });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('is loading until sdk.startSession resolves with a state that has no pending finalizeUrl', async () => {
|
|
104
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
105
|
+
vi.mocked(sdk.startSession).mockResolvedValue({ screen: 'identifier' });
|
|
106
|
+
const { login } = mountWithNativeLogin(sdk);
|
|
107
|
+
|
|
108
|
+
expect(login.loading.value).toBe(true);
|
|
109
|
+
|
|
110
|
+
await flushPromises();
|
|
111
|
+
|
|
112
|
+
expect(login.loading.value).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('stays loading forever if sdk.startSession resolves with no state at all', async () => {
|
|
116
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
117
|
+
vi.mocked(sdk.startSession).mockResolvedValue(undefined);
|
|
118
|
+
const { login } = mountWithNativeLogin(sdk);
|
|
119
|
+
|
|
120
|
+
await flushPromises();
|
|
121
|
+
|
|
122
|
+
expect(login.loading.value).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('applies the returned flow state: resets per-form data, routes the global message, and keeps per-widget messages', async () => {
|
|
126
|
+
const state: NativeFlowState = {
|
|
127
|
+
hostedUrl: 'https://brandtegrity.io/hosted',
|
|
128
|
+
screen: 'identifier',
|
|
129
|
+
forms: [{ id: 'form1', type: 'form', widgets: [] }],
|
|
130
|
+
messages: {
|
|
131
|
+
global: { type: 'info', text: 'Welcome' },
|
|
132
|
+
form1: { field1: { type: 'error', text: 'Required' } },
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
136
|
+
vi.mocked(sdk.startSession).mockResolvedValue(state);
|
|
137
|
+
const onGlobalMessage = vi.fn();
|
|
138
|
+
|
|
139
|
+
const { login } = mountWithNativeLogin(sdk, { onGlobalMessage });
|
|
140
|
+
await flushPromises();
|
|
141
|
+
|
|
142
|
+
expect(onGlobalMessage).toHaveBeenCalledWith({ type: 'info', text: 'Welcome' });
|
|
143
|
+
expect(login.forms.value).toEqual({ form1: {} });
|
|
144
|
+
expect(login.messages.value).toEqual({ form1: { field1: { type: 'error', text: 'Required' } } });
|
|
145
|
+
expect(login.state.value).toEqual(state);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('keeps loading while a finalizeUrl is still pending', async () => {
|
|
149
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
150
|
+
vi.mocked(sdk.startSession).mockResolvedValue({ finalizeUrl: 'https://brandtegrity.io/finalize' });
|
|
151
|
+
|
|
152
|
+
const { login } = mountWithNativeLogin(sdk);
|
|
153
|
+
await flushPromises();
|
|
154
|
+
|
|
155
|
+
expect(login.loading.value).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('calls onLogin and leaves the tracked state untouched when the sdk already has a session', async () => {
|
|
159
|
+
const session = { access_token: 'access-token' } as unknown as SessionData;
|
|
160
|
+
const sdk = createMockFlow<NativeFlow>({ session });
|
|
161
|
+
vi.mocked(sdk.startSession).mockResolvedValue({ screen: 'identifier' });
|
|
162
|
+
const onLogin = vi.fn();
|
|
163
|
+
|
|
164
|
+
const { login } = mountWithNativeLogin(sdk, { onLogin });
|
|
165
|
+
await flushPromises();
|
|
166
|
+
|
|
167
|
+
expect(onLogin).toHaveBeenCalledWith(session);
|
|
168
|
+
expect(login.state.value).toEqual({});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('routes a FallbackError from sdk.startSession to onFallback and logs it', async () => {
|
|
172
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
173
|
+
const fallbackError = new FallbackError(new URL('https://brandtegrity.io/hosted'));
|
|
174
|
+
vi.mocked(sdk.startSession).mockRejectedValue(fallbackError);
|
|
175
|
+
const onFallback = vi.fn();
|
|
176
|
+
|
|
177
|
+
const { login } = mountWithNativeLogin(sdk, { onFallback });
|
|
178
|
+
await flushPromises();
|
|
179
|
+
|
|
180
|
+
expect(onFallback).toHaveBeenCalledWith(fallbackError);
|
|
181
|
+
expect(sdk.logging!.error).toHaveBeenCalledWith('Fallback error occurred', fallbackError);
|
|
182
|
+
expect(login.loading.value).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('routes a non-fallback error from sdk.startSession to onError and logs it', async () => {
|
|
186
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
187
|
+
const error = new Error('network down');
|
|
188
|
+
vi.mocked(sdk.startSession).mockRejectedValue(error);
|
|
189
|
+
const onError = vi.fn();
|
|
190
|
+
|
|
191
|
+
mountWithNativeLogin(sdk, { onError });
|
|
192
|
+
await flushPromises();
|
|
193
|
+
|
|
194
|
+
expect(onError).toHaveBeenCalledWith(error);
|
|
195
|
+
expect(sdk.logging!.error).toHaveBeenCalledWith('Error starting session', error);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('submitForm', () => {
|
|
199
|
+
test('unflattens the tracked form values and calls through to sdk.submitForm', async () => {
|
|
200
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
201
|
+
const initialForms = [{ id: 'form1', type: 'form' as const, widgets: [] }];
|
|
202
|
+
vi.mocked(sdk.startSession).mockResolvedValue({ screen: 'identifier', forms: initialForms });
|
|
203
|
+
vi.mocked(sdk.submitForm).mockResolvedValue({ screen: 'done' });
|
|
204
|
+
|
|
205
|
+
const { login } = mountWithNativeLogin(sdk);
|
|
206
|
+
await flushPromises();
|
|
207
|
+
|
|
208
|
+
login.setFormValue('form1', 'address.city', 'Budapest');
|
|
209
|
+
login.setFormValue('form1', 'address.zip', '');
|
|
210
|
+
|
|
211
|
+
await login.submitForm('form1');
|
|
212
|
+
|
|
213
|
+
expect(sdk.submitForm).toHaveBeenCalledWith('form1', { address: { city: 'Budapest', zip: null } });
|
|
214
|
+
// unspecified fields on the new state fall back to what was already tracked (except messages, which always resets)
|
|
215
|
+
expect(login.state.value).toEqual({
|
|
216
|
+
hostedUrl: undefined,
|
|
217
|
+
finalizeUrl: undefined,
|
|
218
|
+
screen: 'done',
|
|
219
|
+
forms: initialForms,
|
|
220
|
+
layout: undefined,
|
|
221
|
+
messages: {},
|
|
222
|
+
branding: undefined,
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('uses a given custom body instead of the tracked form values', async () => {
|
|
227
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
228
|
+
vi.mocked(sdk.submitForm).mockResolvedValue({ screen: 'done' });
|
|
229
|
+
|
|
230
|
+
const { login } = mountWithNativeLogin(sdk);
|
|
231
|
+
await flushPromises();
|
|
232
|
+
|
|
233
|
+
await login.submitForm('form1', { raw: true });
|
|
234
|
+
|
|
235
|
+
expect(sdk.submitForm).toHaveBeenCalledWith('form1', { raw: true });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('routes a FallbackError to onFallback', async () => {
|
|
239
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
240
|
+
const fallbackError = new FallbackError(new URL('https://brandtegrity.io/hosted'));
|
|
241
|
+
vi.mocked(sdk.submitForm).mockRejectedValue(fallbackError);
|
|
242
|
+
const onFallback = vi.fn();
|
|
243
|
+
|
|
244
|
+
const { login } = mountWithNativeLogin(sdk, { onFallback });
|
|
245
|
+
await flushPromises();
|
|
246
|
+
|
|
247
|
+
await login.submitForm('form1');
|
|
248
|
+
|
|
249
|
+
expect(onFallback).toHaveBeenCalledWith(fallbackError);
|
|
250
|
+
expect(sdk.logging!.error).toHaveBeenCalledWith('Fallback error occurred', fallbackError);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test('routes a non-fallback error to onError', async () => {
|
|
254
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
255
|
+
const error = new Error('submit failed');
|
|
256
|
+
vi.mocked(sdk.submitForm).mockRejectedValue(error);
|
|
257
|
+
const onError = vi.fn();
|
|
258
|
+
|
|
259
|
+
const { login } = mountWithNativeLogin(sdk, { onError });
|
|
260
|
+
await flushPromises();
|
|
261
|
+
|
|
262
|
+
await login.submitForm('form1');
|
|
263
|
+
|
|
264
|
+
expect(onError).toHaveBeenCalledWith(error);
|
|
265
|
+
expect(sdk.logging!.error).toHaveBeenCalledWith('Error submitting form', error);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('setFormValue coerces empty strings to null and lazily creates the form bucket', async () => {
|
|
270
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
271
|
+
const { login } = mountWithNativeLogin(sdk);
|
|
272
|
+
await flushPromises();
|
|
273
|
+
|
|
274
|
+
login.setFormValue('newForm', 'field', 'value');
|
|
275
|
+
login.setFormValue('newForm', 'other', '');
|
|
276
|
+
|
|
277
|
+
expect(login.forms.value.newForm).toEqual({ field: 'value', other: null });
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('setMessage lazily creates the message bucket for a form', async () => {
|
|
281
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
282
|
+
const { login } = mountWithNativeLogin(sdk);
|
|
283
|
+
await flushPromises();
|
|
284
|
+
|
|
285
|
+
login.setMessage('newForm', 'field', { type: 'error', text: 'Required' });
|
|
286
|
+
|
|
287
|
+
expect(login.messages.value.newForm).toEqual({ field: { type: 'error', text: 'Required' } });
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe('triggerFallback', () => {
|
|
291
|
+
test('throws when no hosted URL is known yet', async () => {
|
|
292
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
293
|
+
vi.mocked(sdk.startSession).mockResolvedValue({ screen: 'identifier' });
|
|
294
|
+
const { login } = mountWithNativeLogin(sdk);
|
|
295
|
+
await flushPromises();
|
|
296
|
+
|
|
297
|
+
expect(() => login.triggerFallback()).toThrow('No hosted URL provided');
|
|
298
|
+
expect(sdk.logging!.error).toHaveBeenCalledWith('Fallback error', expect.any(Error));
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test('calls onFallback with a FallbackError built from the known hosted URL', async () => {
|
|
302
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
303
|
+
vi.mocked(sdk.startSession).mockResolvedValue({ hostedUrl: 'https://brandtegrity.io/hosted', screen: 'identifier' });
|
|
304
|
+
const onFallback = vi.fn();
|
|
305
|
+
|
|
306
|
+
const { login } = mountWithNativeLogin(sdk, { onFallback });
|
|
307
|
+
await flushPromises();
|
|
308
|
+
|
|
309
|
+
login.triggerFallback('manual trigger');
|
|
310
|
+
|
|
311
|
+
expect(sdk.logging!.warn).toHaveBeenCalledWith('Triggering fallback due to: manual trigger');
|
|
312
|
+
expect(onFallback).toHaveBeenCalledWith(expect.any(FallbackError));
|
|
313
|
+
expect((onFallback.mock.calls[0][0] as FallbackError).url.toString()).toBe('https://brandtegrity.io/hosted');
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test('triggerClose calls onClose and logs', async () => {
|
|
318
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
319
|
+
const onClose = vi.fn();
|
|
320
|
+
const { login } = mountWithNativeLogin(sdk, { onClose });
|
|
321
|
+
await flushPromises();
|
|
322
|
+
|
|
323
|
+
login.triggerClose();
|
|
324
|
+
|
|
325
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
326
|
+
expect(sdk.logging!.debug).toHaveBeenCalledWith('Triggering close');
|
|
327
|
+
});
|
|
328
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { test, expect } from 'vitest';
|
|
2
|
+
import * as vueErrors from '../../src/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(vueErrors).sort()).toEqual(coreKeys.sort());
|
|
10
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { test, expect } from 'vitest';
|
|
2
|
+
import * as index from '../../src/index';
|
|
3
|
+
|
|
4
|
+
test('should export the correct things', () => {
|
|
5
|
+
const expectedExports = [
|
|
6
|
+
'createBaseFlow',
|
|
7
|
+
'createEmbeddedFlow',
|
|
8
|
+
'createNativeFlow',
|
|
9
|
+
'createPopupFlow',
|
|
10
|
+
'createRedirectFlow',
|
|
11
|
+
'popupCallbackHandler',
|
|
12
|
+
'popupUrlHandler',
|
|
13
|
+
'redirectCallbackHandler',
|
|
14
|
+
'redirectUrlHandler',
|
|
15
|
+
'BACKCHANNEL_LOGOUT_EVENT',
|
|
16
|
+
'ConfigurationError',
|
|
17
|
+
'DEFAULT_COOKIE_MAX_AGE',
|
|
18
|
+
'DISALLOWED_PROXY_HEADERS',
|
|
19
|
+
'FallbackError',
|
|
20
|
+
'InternalError',
|
|
21
|
+
'NetworkError',
|
|
22
|
+
'OidcError',
|
|
23
|
+
'PopupBlockedError',
|
|
24
|
+
'PopupClosedError',
|
|
25
|
+
'ProtocolError',
|
|
26
|
+
'RETURN_TO_COOKIE',
|
|
27
|
+
'ServerError',
|
|
28
|
+
'SessionExpiredError',
|
|
29
|
+
'UnsupportedFlowError',
|
|
30
|
+
'assertWebAuthnCredential',
|
|
31
|
+
'buildAuthorizationUrl',
|
|
32
|
+
'buildCookieString',
|
|
33
|
+
'buildEndSessionUrl',
|
|
34
|
+
'createDefaultLogging',
|
|
35
|
+
'createHttpClient',
|
|
36
|
+
'createSession',
|
|
37
|
+
'createState',
|
|
38
|
+
'createWebAuthnCredential',
|
|
39
|
+
'decodeBase64URL',
|
|
40
|
+
'decodeJwt',
|
|
41
|
+
'decryptString',
|
|
42
|
+
'defaultOptions',
|
|
43
|
+
'encodeBase64URL',
|
|
44
|
+
'encryptString',
|
|
45
|
+
'exchangeCode',
|
|
46
|
+
'fetchFlowEntry',
|
|
47
|
+
'finalizeLoginSession',
|
|
48
|
+
'flushSetCookies',
|
|
49
|
+
'generateCodeChallenge',
|
|
50
|
+
'generateCodeVerifier',
|
|
51
|
+
'generateJwt',
|
|
52
|
+
'generateRandomHex',
|
|
53
|
+
'getDefaultFlowState',
|
|
54
|
+
'getDiscoveryDocument',
|
|
55
|
+
'getSDKOptions',
|
|
56
|
+
'getServerSDKOptions',
|
|
57
|
+
'injectScript',
|
|
58
|
+
'isSessionExpired',
|
|
59
|
+
'loadSession',
|
|
60
|
+
'parseCookieHeader',
|
|
61
|
+
'parseState',
|
|
62
|
+
'proxyResponse',
|
|
63
|
+
'pushSetCookie',
|
|
64
|
+
'refreshToken',
|
|
65
|
+
'revokeToken',
|
|
66
|
+
'serializeCookie',
|
|
67
|
+
'serializeSession',
|
|
68
|
+
'serializeState',
|
|
69
|
+
'submitLoginForm',
|
|
70
|
+
'throwHttpError',
|
|
71
|
+
'throwTokenEndpointError',
|
|
72
|
+
'timestamp',
|
|
73
|
+
'toSafeRedirect',
|
|
74
|
+
'unflattenObject',
|
|
75
|
+
'verifyIdToken',
|
|
76
|
+
'verifyJwt',
|
|
77
|
+
'STRIVACITY_SDK',
|
|
78
|
+
'STRIVACITY_LOGIN_CONTEXT',
|
|
79
|
+
'useStrivacity',
|
|
80
|
+
'useNativeLoginContext',
|
|
81
|
+
'useNativeLogin',
|
|
82
|
+
'createStrivacitySDK',
|
|
83
|
+
'COOKIE_CHUNK_SIZE',
|
|
84
|
+
'COOKIE_CONTEXT',
|
|
85
|
+
'createCacheAPIStorage',
|
|
86
|
+
'createEncryptedCookieStorage',
|
|
87
|
+
'createIndexedDBStorage',
|
|
88
|
+
'createLocalStorage',
|
|
89
|
+
'createMemoryStorage',
|
|
90
|
+
'createServerMemoryStorage',
|
|
91
|
+
'createServerStateStorage',
|
|
92
|
+
'createSessionIdCookieStorage',
|
|
93
|
+
'createSessionStorage',
|
|
94
|
+
'createWorkerStorage',
|
|
95
|
+
'handleWorkerStorageRequests',
|
|
96
|
+
'AlgorithmTypeList',
|
|
97
|
+
'GrantTypeList',
|
|
98
|
+
'ResponseModeList',
|
|
99
|
+
'ResponseTypeList',
|
|
100
|
+
'SubjectTypeList',
|
|
101
|
+
'TokenEndpointAuthMethodList',
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
expect(Object.keys(index)).toEqual(expectedExports);
|
|
105
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { RedirectFlow } from '../../src/types';
|
|
2
|
+
import { describe, test, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import { createApp } from 'vue';
|
|
4
|
+
import { initFlow } from '@strivacity/sdk-core';
|
|
5
|
+
import { createStrivacitySDK } from '../../src/plugin';
|
|
6
|
+
import { flushPromises } from '@strivacity/testing/mocks/common';
|
|
7
|
+
import { createMockFlow } from '@strivacity/testing/mocks/sdk';
|
|
8
|
+
import { install } from '../utils/common';
|
|
9
|
+
|
|
10
|
+
vi.mock('@strivacity/sdk-core', async (importOriginal) => ({
|
|
11
|
+
...(await importOriginal<typeof import('@strivacity/sdk-core')>()),
|
|
12
|
+
initFlow: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
describe('createStrivacitySDK', () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.mocked(initFlow).mockReset();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('initializes the underlying flow through core sdk initFlow with the given options', () => {
|
|
21
|
+
const flow = createMockFlow<RedirectFlow>();
|
|
22
|
+
const options = { mode: 'redirect' as const, issuer: 'https://brandtegrity.io', clientId: 'client-id', redirectUri: 'https://brandtegrity.io/callback' };
|
|
23
|
+
|
|
24
|
+
vi.mocked(initFlow).mockReturnValue(flow as never);
|
|
25
|
+
|
|
26
|
+
const app = createApp({ setup: () => () => null });
|
|
27
|
+
app.use(createStrivacitySDK(options));
|
|
28
|
+
app.mount(document.createElement('div'));
|
|
29
|
+
|
|
30
|
+
expect(initFlow).toHaveBeenCalledTimes(1);
|
|
31
|
+
expect(initFlow).toHaveBeenCalledWith(options);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('subscribes to all sdk events on install and disposes on unmount', () => {
|
|
35
|
+
const dispose = vi.fn();
|
|
36
|
+
const flow = createMockFlow<RedirectFlow>({ subscribeToAllEvents: vi.fn().mockReturnValue({ dispose }) });
|
|
37
|
+
const { app } = install(flow);
|
|
38
|
+
|
|
39
|
+
expect(flow.subscribeToAllEvents).toHaveBeenCalledTimes(1);
|
|
40
|
+
expect(flow.subscribeToAllEvents).toHaveBeenCalledWith(expect.any(Function));
|
|
41
|
+
|
|
42
|
+
app.unmount();
|
|
43
|
+
|
|
44
|
+
expect(dispose).toHaveBeenCalledTimes(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test.each([
|
|
48
|
+
['init', [], undefined],
|
|
49
|
+
['subscribeToEvent', ['loggedIn', vi.fn()], { dispose: vi.fn() }],
|
|
50
|
+
['checkAuthentication', [{ autoRefresh: false }], true],
|
|
51
|
+
['tokenExchange', [{ code: 'abc' }], undefined],
|
|
52
|
+
['handleCallback', ['https://brandtegrity.io/callback?code=abc'], undefined],
|
|
53
|
+
['refresh', [], undefined],
|
|
54
|
+
['revoke', [], undefined],
|
|
55
|
+
['logout', [{ postLogoutRedirectUri: 'https://brandtegrity.io' }], undefined],
|
|
56
|
+
['login', [{ scopes: ['openid'] }], undefined],
|
|
57
|
+
['register', [{ scopes: ['openid'] }], undefined],
|
|
58
|
+
['entry', ['https://brandtegrity.io/entry'], undefined],
|
|
59
|
+
] as const)('%s calls through to the flow instance returned by core sdk', async (method, args, resolvedValue) => {
|
|
60
|
+
const flow = createMockFlow<RedirectFlow>({ [method]: vi.fn().mockReturnValue(resolvedValue) });
|
|
61
|
+
const { context } = install(flow);
|
|
62
|
+
|
|
63
|
+
const result = await (context[method] as (...a: Array<unknown>) => unknown)(...args);
|
|
64
|
+
|
|
65
|
+
expect(flow[method as keyof RedirectFlow]).toHaveBeenCalledWith(...args);
|
|
66
|
+
expect(result).toEqual(resolvedValue);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('subscribeToAllEvents on the context calls through to the flow instance', () => {
|
|
70
|
+
const callback = vi.fn();
|
|
71
|
+
const disposeResult = { dispose: vi.fn() };
|
|
72
|
+
const flow = createMockFlow<RedirectFlow>();
|
|
73
|
+
vi.mocked(flow.subscribeToAllEvents).mockReturnValue(disposeResult);
|
|
74
|
+
|
|
75
|
+
const { context } = install(flow);
|
|
76
|
+
// the install-time subscription happens first; this is the caller-facing pass-through
|
|
77
|
+
const result = context.subscribeToAllEvents(callback);
|
|
78
|
+
|
|
79
|
+
expect(flow.subscribeToAllEvents).toHaveBeenLastCalledWith(callback);
|
|
80
|
+
expect(result).toBe(disposeResult);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('exposes reactive state seeded from the flow instance after the initial session update settles', async () => {
|
|
84
|
+
const flow = createMockFlow<RedirectFlow>({
|
|
85
|
+
isAuthenticated: Promise.resolve(true),
|
|
86
|
+
language: 'hu-HU',
|
|
87
|
+
idTokenClaims: { sub: 'user-1' },
|
|
88
|
+
accessToken: 'access-token',
|
|
89
|
+
refreshToken: 'refresh-token',
|
|
90
|
+
accessTokenExpired: false,
|
|
91
|
+
accessTokenExpirationDate: 1234,
|
|
92
|
+
});
|
|
93
|
+
const { context } = install(flow);
|
|
94
|
+
|
|
95
|
+
expect(context.loading.value).toBe(true);
|
|
96
|
+
|
|
97
|
+
await flushPromises();
|
|
98
|
+
|
|
99
|
+
expect(context.loading.value).toBe(false);
|
|
100
|
+
expect(context.isAuthenticated.value).toBe(true);
|
|
101
|
+
expect(context.language.value).toBe('hu-HU');
|
|
102
|
+
expect(context.idTokenClaims.value).toEqual({ sub: 'user-1' });
|
|
103
|
+
expect(context.accessToken.value).toBe('access-token');
|
|
104
|
+
expect(context.refreshToken.value).toBe('refresh-token');
|
|
105
|
+
expect(context.accessTokenExpired.value).toBe(false);
|
|
106
|
+
expect(context.accessTokenExpirationDate.value).toBe(1234);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('reactive state updates when the flow emits an event', async () => {
|
|
110
|
+
const subscribeToAllEvents = vi.fn().mockReturnValue({ dispose: vi.fn() });
|
|
111
|
+
const flow = createMockFlow<RedirectFlow>({ subscribeToAllEvents });
|
|
112
|
+
const { context } = install(flow);
|
|
113
|
+
await flushPromises();
|
|
114
|
+
|
|
115
|
+
const onEvent = subscribeToAllEvents.mock.calls[0][0];
|
|
116
|
+
const mutableFlow = flow as { language: string; accessToken: string | null; isAuthenticated: Promise<boolean> };
|
|
117
|
+
|
|
118
|
+
mutableFlow.language = 'de-DE';
|
|
119
|
+
mutableFlow.accessToken = 'new-token';
|
|
120
|
+
mutableFlow.isAuthenticated = Promise.resolve(true);
|
|
121
|
+
|
|
122
|
+
await onEvent();
|
|
123
|
+
await flushPromises();
|
|
124
|
+
|
|
125
|
+
expect(context.language.value).toBe('de-DE');
|
|
126
|
+
expect(context.accessToken.value).toBe('new-token');
|
|
127
|
+
expect(context.isAuthenticated.value).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { test, expect } from 'vitest';
|
|
2
|
+
import * as vueStorages from '../../src/storages';
|
|
3
|
+
import * as coreStorages from '@strivacity/sdk-core/storages';
|
|
4
|
+
|
|
5
|
+
test('re-exports storages from the core sdk', () => {
|
|
6
|
+
const coreKeys = Object.keys(coreStorages);
|
|
7
|
+
|
|
8
|
+
expect(coreKeys.length).toBeGreaterThan(0);
|
|
9
|
+
expect(Object.keys(vueStorages).sort()).toEqual(coreKeys.sort());
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { test, expect } from 'vitest';
|
|
2
|
+
import * as vueUtils from '../../src/utils';
|
|
3
|
+
import * as coreUtils from '@strivacity/sdk-core/utils';
|
|
4
|
+
|
|
5
|
+
test('re-exports utility from the core sdk', () => {
|
|
6
|
+
const coreKeys = Object.keys(coreUtils);
|
|
7
|
+
|
|
8
|
+
expect(coreKeys.length).toBeGreaterThan(0);
|
|
9
|
+
expect(Object.keys(vueUtils).sort()).toEqual(coreKeys.sort());
|
|
10
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { SDKContext, LoginContext, UseNativeLoginOptions, RedirectFlow, NativeFlow } from '../../src/types';
|
|
2
|
+
import { vi } from 'vitest';
|
|
3
|
+
import { createApp, inject, type App } from 'vue';
|
|
4
|
+
import { initFlow } from '@strivacity/sdk-core';
|
|
5
|
+
import { STRIVACITY_SDK, useNativeLogin } from '../../src/composables';
|
|
6
|
+
import { createStrivacitySDK } from '../../src/plugin';
|
|
7
|
+
|
|
8
|
+
export function mount(sdk: unknown, setup: () => void) {
|
|
9
|
+
const app = createApp({
|
|
10
|
+
setup() {
|
|
11
|
+
setup();
|
|
12
|
+
return () => null;
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
app.provide(STRIVACITY_SDK, { sdk });
|
|
16
|
+
app.mount(document.createElement('div'));
|
|
17
|
+
|
|
18
|
+
return app;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function install(flow: RedirectFlow): { app: App; context: SDKContext<RedirectFlow> } {
|
|
22
|
+
vi.mocked(initFlow).mockReturnValue(flow as never);
|
|
23
|
+
|
|
24
|
+
let context: SDKContext<RedirectFlow> | undefined;
|
|
25
|
+
const app = createApp({
|
|
26
|
+
setup() {
|
|
27
|
+
context = inject<SDKContext<RedirectFlow>>(STRIVACITY_SDK);
|
|
28
|
+
|
|
29
|
+
return () => null;
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
app.use(
|
|
34
|
+
createStrivacitySDK({
|
|
35
|
+
mode: 'redirect',
|
|
36
|
+
issuer: 'https://brandtegrity.io',
|
|
37
|
+
clientId: 'client-id',
|
|
38
|
+
redirectUri: 'https://brandtegrity.io/callback',
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
app.mount(document.createElement('div'));
|
|
42
|
+
|
|
43
|
+
return { app, context: context! };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function mountWithNativeLogin(sdk: NativeFlow, options: UseNativeLoginOptions = {}) {
|
|
47
|
+
let login!: LoginContext;
|
|
48
|
+
const app = mount(sdk, () => {
|
|
49
|
+
login = useNativeLogin(options);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return { app, login };
|
|
53
|
+
}
|
package/tsconfig.json
ADDED