@strivacity/sdk-nuxt 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/.nuxt/app-component.mjs +1 -1
- package/.nuxt/app.config.mjs +21 -0
- package/.nuxt/component-names.mjs +1 -1
- package/.nuxt/components.d.ts +48 -48
- package/.nuxt/components.islands.mjs +6 -2
- package/.nuxt/error-component.mjs +1 -1
- package/.nuxt/fetch.d.ts +1 -0
- package/.nuxt/fetch.mjs +4 -3
- package/.nuxt/imports.d.ts +15 -5
- package/.nuxt/imports.mjs +15 -5
- package/.nuxt/island-renderer.mjs +2 -0
- package/.nuxt/middleware.mjs +3 -1
- package/.nuxt/nuxt.config.mjs +103 -0
- package/.nuxt/nuxt.d.ts +3 -4
- package/.nuxt/nuxt.node.d.ts +4 -4
- package/.nuxt/nuxt.shared.d.ts +3 -1
- package/.nuxt/plugins.client.mjs +10 -14
- package/.nuxt/plugins.server.mjs +6 -6
- package/.nuxt/root-component.mjs +1 -1
- package/.nuxt/route-rules.mjs +22 -0
- package/.nuxt/test-component-wrapper.mjs +1 -1
- package/.nuxt/tsconfig.app.json +211 -0
- package/.nuxt/tsconfig.json +212 -0
- package/.nuxt/tsconfig.node.json +117 -0
- package/.nuxt/tsconfig.server.json +161 -0
- package/.nuxt/tsconfig.shared.json +165 -0
- package/.nuxt/types/app.config.d.ts +35 -0
- package/.nuxt/types/components.d.ts +48 -48
- package/.nuxt/types/imports.d.ts +1 -1
- package/.nuxt/types/layouts.d.ts +16 -0
- package/.nuxt/types/modules.d.ts +20 -20
- package/.nuxt/types/nitro-imports.d.ts +9 -4
- package/.nuxt/types/nitro-layouts.d.ts +17 -0
- package/.nuxt/types/nitro-nuxt.d.ts +19 -5
- package/.nuxt/types/nitro-routes.d.ts +25 -1
- package/.nuxt/types/plugins.d.ts +8 -9
- package/.nuxt/types/runtime-config.d.ts +6 -12
- package/.nuxt/types/shared-app.config.d.ts +28 -0
- package/.nuxt/types/shared-imports.d.ts +2 -0
- package/.nuxt/unhead-options.mjs +2 -1
- package/.nuxt/unhead.config.mjs +1 -0
- package/CHANGELOG.md +16 -14
- package/README.md +1577 -406
- package/build.config.ts +5 -0
- package/dist/module.d.mts +3 -193
- package/dist/module.json +2 -2
- package/dist/module.mjs +78 -57
- package/dist/module.mjs.map +1 -1
- package/dist/runtime/composables/use-native-login-context.d.ts +10 -0
- package/dist/runtime/composables/use-native-login-context.js +9 -0
- package/dist/runtime/composables/use-native-login.d.ts +10 -0
- package/dist/runtime/composables/use-native-login.js +143 -0
- package/dist/runtime/composables/use-session.d.ts +8 -0
- package/dist/runtime/composables/use-session.js +2 -0
- package/dist/runtime/composables/use-strivacity.d.ts +9 -0
- package/dist/runtime/composables/use-strivacity.js +77 -0
- package/dist/runtime/middleware/auth.server.d.ts +2 -0
- package/dist/runtime/middleware/auth.server.js +12 -0
- package/dist/runtime/server/api/auth/backchannel-logout.get.d.ts +2 -0
- package/dist/runtime/server/api/auth/backchannel-logout.get.js +2 -0
- package/dist/runtime/server/api/auth/callback.get.d.ts +2 -0
- package/dist/runtime/server/api/auth/callback.get.js +2 -0
- package/dist/runtime/server/api/auth/entry.get.d.ts +2 -0
- package/dist/runtime/server/api/auth/entry.get.js +2 -0
- package/dist/runtime/server/api/auth/login.get.d.ts +2 -0
- package/dist/runtime/server/api/auth/login.get.js +2 -0
- package/dist/runtime/server/api/auth/logout.get.d.ts +2 -0
- package/dist/runtime/server/api/auth/logout.get.js +2 -0
- package/dist/runtime/server/api/auth/refresh.get.d.ts +2 -0
- package/dist/runtime/server/api/auth/refresh.get.js +2 -0
- package/dist/runtime/server/api/auth/register.get.d.ts +2 -0
- package/dist/runtime/server/api/auth/register.get.js +2 -0
- package/dist/runtime/server/api/auth/revoke.get.d.ts +2 -0
- package/dist/runtime/server/api/auth/revoke.get.js +2 -0
- package/dist/runtime/server/composables/use-strivacity.d.ts +2 -0
- package/dist/runtime/server/composables/use-strivacity.js +9 -0
- package/dist/runtime/server/plugins/auth.server.d.ts +10 -0
- package/dist/runtime/server/plugins/auth.server.js +66 -0
- package/dist/runtime/server/utils/helpers.d.ts +18 -0
- package/dist/runtime/server/utils/helpers.js +39 -0
- package/dist/runtime/storages/core.d.ts +1 -0
- package/dist/runtime/storages/core.js +13 -0
- package/dist/runtime/storages/default.d.ts +21 -0
- package/dist/runtime/storages/default.js +71 -0
- package/dist/runtime/types.d.ts +219 -0
- package/dist/runtime/types.js +1 -0
- package/dist/runtime/utils/core.d.ts +1 -0
- package/dist/runtime/utils/core.js +64 -0
- package/dist/runtime/utils/noop.d.ts +1 -0
- package/dist/runtime/utils/noop.js +3 -0
- package/dist/types.d.mts +4 -16
- package/eslint.config.mjs +5 -0
- package/nuxt.config.ts +5 -0
- package/package.json +31 -5
- package/project.json +44 -0
- package/src/module.ts +107 -0
- package/src/runtime/composables/use-native-login-context.ts +20 -0
- package/src/runtime/composables/use-native-login.ts +182 -0
- package/src/runtime/composables/use-session.ts +10 -0
- package/src/runtime/composables/use-strivacity.ts +95 -0
- package/src/runtime/middleware/auth.server.ts +16 -0
- package/src/runtime/server/api/auth/backchannel-logout.get.ts +3 -0
- package/src/runtime/server/api/auth/callback.get.ts +3 -0
- package/src/runtime/server/api/auth/entry.get.ts +3 -0
- package/src/runtime/server/api/auth/login.get.ts +3 -0
- package/src/runtime/server/api/auth/logout.get.ts +3 -0
- package/src/runtime/server/api/auth/refresh.get.ts +3 -0
- package/src/runtime/server/api/auth/register.get.ts +3 -0
- package/src/runtime/server/api/auth/revoke.get.ts +3 -0
- package/src/runtime/server/composables/use-strivacity.ts +13 -0
- package/src/runtime/server/plugins/auth.server.ts +84 -0
- package/src/runtime/server/utils/helpers.ts +67 -0
- package/src/runtime/storages/core.ts +13 -0
- package/src/runtime/storages/default.ts +111 -0
- package/src/runtime/types.ts +289 -0
- package/src/runtime/utils/core.ts +64 -0
- package/src/runtime/utils/noop.ts +3 -0
- package/testing/stubs/app.ts +6 -0
- package/testing/stubs/imports.ts +19 -0
- package/testing/stubs/options-httpClient.ts +8 -0
- package/testing/stubs/options-logging.ts +8 -0
- package/testing/stubs/options-stateStorage.ts +8 -0
- package/testing/stubs/options-storage.ts +8 -0
- package/testing/tests/composables.spec.ts +512 -0
- package/testing/tests/server/helpers.spec.ts +86 -0
- package/testing/tests/server/plugin.spec.ts +138 -0
- package/testing/tests/server/routes.spec.ts +34 -0
- package/testing/tests/server/use-strivacity.spec.ts +22 -0
- package/testing/tests/storages-default.spec.ts +151 -0
- package/testing/tests/storages.spec.ts +16 -0
- package/testing/tests/utils.spec.ts +10 -0
- package/testing/utils/common.ts +33 -0
- package/testing/utils/http.ts +58 -0
- package/tsconfig.json +3 -0
- package/vite.config.mts +20 -0
- package/.nuxt/strivacity-sdk-logging.d.ts +0 -13
- package/.nuxt/strivacity-sdk-logging.mjs +0 -1
- package/.nuxt/strivacity-sdk-storage.d.ts +0 -10
- package/.nuxt/strivacity-sdk-storage.mjs +0 -1
- package/dist/runtime/composables.d.ts +0 -12
- package/dist/runtime/composables.js +0 -83
- package/dist/runtime/login-renderer.vue +0 -174
- package/dist/runtime/login-renderer.vue.d.ts +0 -39
- /package/.nuxt/manifest/meta/{b47927c0-b36b-4d2c-b650-8d55c5ec676a.json → cb11df97-b0c9-4a91-9d4b-60c25c918599.json} +0 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Stub for the '#strivacity-options-logging' virtual alias, aliased in vite.config.mts (see
|
|
2
|
+
// app.ts for why). Kept as its own file rather than sharing one file across all four
|
|
3
|
+
// '#strivacity-options-*' aliases - vitest's mock registry keys mocks by resolved module id, so
|
|
4
|
+
// two different specifiers pointing at the same physical file collide when a test vi.mock()s one
|
|
5
|
+
// of them. Mirrors ./runtime/utils/noop's default shape.
|
|
6
|
+
export default function () {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Stub for the '#strivacity-options-stateStorage' virtual alias, aliased in vite.config.mts (see
|
|
2
|
+
// app.ts for why). Kept as its own file rather than sharing one file across all four
|
|
3
|
+
// '#strivacity-options-*' aliases - vitest's mock registry keys mocks by resolved module id, so
|
|
4
|
+
// two different specifiers pointing at the same physical file collide when a test vi.mock()s one
|
|
5
|
+
// of them. Mirrors ./runtime/utils/noop's default shape.
|
|
6
|
+
export default function () {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Stub for the '#strivacity-options-storage' virtual alias, aliased in vite.config.mts (see
|
|
2
|
+
// app.ts for why). Kept as its own file rather than sharing one file across all four
|
|
3
|
+
// '#strivacity-options-*' aliases - vitest's mock registry keys mocks by resolved module id, so
|
|
4
|
+
// two different specifiers pointing at the same physical file collide when a test vi.mock()s one
|
|
5
|
+
// of them. Mirrors ./runtime/utils/noop's default shape.
|
|
6
|
+
export default function () {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import type { RedirectFlow, NativeFlow, SessionData, NativeFlowState } from '@strivacity/sdk-core/types';
|
|
2
|
+
import type { FallbackError as FallbackErrorType } from '@strivacity/sdk-core/utils/errors';
|
|
3
|
+
import type { LoginContext } from '../../src/runtime/types';
|
|
4
|
+
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
5
|
+
import { createApp, defineComponent, h } from 'vue';
|
|
6
|
+
import { flushPromises } from '@strivacity/testing/mocks/common';
|
|
7
|
+
import { createMockFlow } from '@strivacity/testing/mocks/sdk';
|
|
8
|
+
import { mockInitFlow, mountWithNativeLogin } 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
|
+
beforeEach(() => {
|
|
16
|
+
vi.resetModules();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
// vi.doMock() registrations survive vi.resetModules() (only the module cache is cleared), so
|
|
21
|
+
// tests that override the default stubs must revert them here to avoid leaking into the next test.
|
|
22
|
+
vi.doUnmock('#app');
|
|
23
|
+
vi.doUnmock('#strivacity-options-logging');
|
|
24
|
+
vi.doUnmock('#strivacity-options-httpClient');
|
|
25
|
+
vi.doUnmock('../../src/runtime/composables/use-session');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('useStrivacity', () => {
|
|
29
|
+
test('initializes the underlying flow through core sdk initFlow with the runtime config merged with the logging/httpClient factories', async () => {
|
|
30
|
+
const flow = createMockFlow<RedirectFlow>();
|
|
31
|
+
await mockInitFlow(flow);
|
|
32
|
+
|
|
33
|
+
const publicOptions = { mode: 'redirect', issuer: 'https://brandtegrity.io', clientId: 'client-id', redirectUri: 'https://brandtegrity.io/callback' };
|
|
34
|
+
const logging = { debug: vi.fn() };
|
|
35
|
+
const httpClient = { request: vi.fn() };
|
|
36
|
+
vi.doMock('#app', () => ({ useRuntimeConfig: () => ({ public: { strivacity: publicOptions } }) }));
|
|
37
|
+
vi.doMock('#strivacity-options-logging', () => ({ default: () => logging }));
|
|
38
|
+
vi.doMock('#strivacity-options-httpClient', () => ({ default: () => httpClient }));
|
|
39
|
+
|
|
40
|
+
const core = await import('@strivacity/sdk-core');
|
|
41
|
+
const { useStrivacity } = await import('../../src/runtime/composables/use-strivacity');
|
|
42
|
+
useStrivacity();
|
|
43
|
+
|
|
44
|
+
expect(core.initFlow).toHaveBeenCalledWith({ ...publicOptions, logging, httpClient });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('passes undefined logging/httpClient through to initFlow when the factory modules resolve to a falsy default', async () => {
|
|
48
|
+
const flow = createMockFlow<RedirectFlow>();
|
|
49
|
+
await mockInitFlow(flow);
|
|
50
|
+
|
|
51
|
+
const core = await import('@strivacity/sdk-core');
|
|
52
|
+
const { useStrivacity } = await import('../../src/runtime/composables/use-strivacity');
|
|
53
|
+
useStrivacity();
|
|
54
|
+
|
|
55
|
+
expect(core.initFlow).toHaveBeenCalledWith(expect.objectContaining({ logging: undefined, httpClient: undefined }));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('seeds sdk.session from the initial value returned by useSession', async () => {
|
|
59
|
+
const flow = createMockFlow<RedirectFlow>();
|
|
60
|
+
await mockInitFlow(flow);
|
|
61
|
+
|
|
62
|
+
const session = { access_token: 'access-token' } as unknown as SessionData;
|
|
63
|
+
vi.doMock('../../src/runtime/composables/use-session', () => ({ useSession: () => ({ value: session }) }));
|
|
64
|
+
|
|
65
|
+
const { useStrivacity } = await import('../../src/runtime/composables/use-strivacity');
|
|
66
|
+
const ctx = useStrivacity<RedirectFlow>();
|
|
67
|
+
|
|
68
|
+
expect(ctx.sdk.session).toBe(session);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('only initializes the flow once: a second call within the same module instance reuses the same sdk instance', async () => {
|
|
72
|
+
const flow = createMockFlow<RedirectFlow>();
|
|
73
|
+
await mockInitFlow(flow);
|
|
74
|
+
|
|
75
|
+
const core = await import('@strivacity/sdk-core');
|
|
76
|
+
const { useStrivacity } = await import('../../src/runtime/composables/use-strivacity');
|
|
77
|
+
const first = useStrivacity<RedirectFlow>();
|
|
78
|
+
const second = useStrivacity<RedirectFlow>();
|
|
79
|
+
|
|
80
|
+
expect(core.initFlow).toHaveBeenCalledTimes(1);
|
|
81
|
+
expect(first.sdk).toBe(second.sdk);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('subscribes to all sdk events once during initialization', async () => {
|
|
85
|
+
const flow = createMockFlow<RedirectFlow>();
|
|
86
|
+
await mockInitFlow(flow);
|
|
87
|
+
|
|
88
|
+
const { useStrivacity } = await import('../../src/runtime/composables/use-strivacity');
|
|
89
|
+
useStrivacity();
|
|
90
|
+
|
|
91
|
+
expect(flow.subscribeToAllEvents).toHaveBeenCalledTimes(1);
|
|
92
|
+
expect(flow.subscribeToAllEvents).toHaveBeenCalledWith(expect.any(Function));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('mirrors loading/language/isAuthenticated/idTokenClaims/accessToken/refreshToken/accessTokenExpired/accessTokenExpirationDate from the flow instance once the initial session update settles', async () => {
|
|
96
|
+
const flow = createMockFlow<RedirectFlow>({
|
|
97
|
+
isAuthenticated: Promise.resolve(true),
|
|
98
|
+
language: 'hu-HU',
|
|
99
|
+
idTokenClaims: { sub: 'user-1' },
|
|
100
|
+
accessToken: 'access-token',
|
|
101
|
+
refreshToken: 'refresh-token',
|
|
102
|
+
accessTokenExpired: false,
|
|
103
|
+
accessTokenExpirationDate: 1234,
|
|
104
|
+
});
|
|
105
|
+
await mockInitFlow(flow);
|
|
106
|
+
|
|
107
|
+
const { useStrivacity } = await import('../../src/runtime/composables/use-strivacity');
|
|
108
|
+
const ctx = useStrivacity<RedirectFlow>();
|
|
109
|
+
|
|
110
|
+
expect(ctx.loading.value).toBe(true);
|
|
111
|
+
|
|
112
|
+
await flushPromises();
|
|
113
|
+
|
|
114
|
+
expect(ctx.loading.value).toBe(false);
|
|
115
|
+
expect(ctx.isAuthenticated.value).toBe(true);
|
|
116
|
+
expect(ctx.language.value).toBe('hu-HU');
|
|
117
|
+
expect(ctx.idTokenClaims.value).toEqual({ sub: 'user-1' });
|
|
118
|
+
expect(ctx.accessToken.value).toBe('access-token');
|
|
119
|
+
expect(ctx.refreshToken.value).toBe('refresh-token');
|
|
120
|
+
expect(ctx.accessTokenExpired.value).toBe(false);
|
|
121
|
+
expect(ctx.accessTokenExpirationDate.value).toBe(1234);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('updates the reactive state again whenever the flow emits an event via subscribeToAllEvents', async () => {
|
|
125
|
+
const subscribeToAllEvents = vi.fn().mockReturnValue({ dispose: vi.fn() });
|
|
126
|
+
const flow = createMockFlow<RedirectFlow>({ subscribeToAllEvents });
|
|
127
|
+
await mockInitFlow(flow);
|
|
128
|
+
|
|
129
|
+
const { useStrivacity } = await import('../../src/runtime/composables/use-strivacity');
|
|
130
|
+
const ctx = useStrivacity<RedirectFlow>();
|
|
131
|
+
await flushPromises();
|
|
132
|
+
|
|
133
|
+
const onEvent = subscribeToAllEvents.mock.calls[0][0] as () => Promise<void>;
|
|
134
|
+
const mutableFlow = flow as unknown as { language: string; accessToken: string | null; isAuthenticated: Promise<boolean> };
|
|
135
|
+
mutableFlow.language = 'de-DE';
|
|
136
|
+
mutableFlow.accessToken = 'new-token';
|
|
137
|
+
mutableFlow.isAuthenticated = Promise.resolve(true);
|
|
138
|
+
|
|
139
|
+
await onEvent();
|
|
140
|
+
await flushPromises();
|
|
141
|
+
|
|
142
|
+
expect(ctx.language.value).toBe('de-DE');
|
|
143
|
+
expect(ctx.accessToken.value).toBe('new-token');
|
|
144
|
+
expect(ctx.isAuthenticated.value).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('subscribeToAllEvents on the returned context calls through to the flow instance', async () => {
|
|
148
|
+
const callback = vi.fn();
|
|
149
|
+
const disposeResult = { dispose: vi.fn() };
|
|
150
|
+
const flow = createMockFlow<RedirectFlow>();
|
|
151
|
+
vi.mocked(flow.subscribeToAllEvents).mockReturnValue(disposeResult);
|
|
152
|
+
await mockInitFlow(flow);
|
|
153
|
+
|
|
154
|
+
const { useStrivacity } = await import('../../src/runtime/composables/use-strivacity');
|
|
155
|
+
const ctx = useStrivacity<RedirectFlow>();
|
|
156
|
+
|
|
157
|
+
// the init-time subscription happens first; this is the caller-facing pass-through
|
|
158
|
+
const result = ctx.subscribeToAllEvents(callback);
|
|
159
|
+
|
|
160
|
+
expect(flow.subscribeToAllEvents).toHaveBeenLastCalledWith(callback);
|
|
161
|
+
expect(result).toBe(disposeResult);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test.each([
|
|
165
|
+
['init', [], undefined],
|
|
166
|
+
['subscribeToEvent', ['loggedIn', vi.fn()], { dispose: vi.fn() }],
|
|
167
|
+
['checkAuthentication', [{ autoRefresh: false }], true],
|
|
168
|
+
['tokenExchange', [{ code: 'abc' }], undefined],
|
|
169
|
+
['handleCallback', ['https://brandtegrity.io/callback?code=abc'], undefined],
|
|
170
|
+
['refresh', [], undefined],
|
|
171
|
+
['revoke', [], undefined],
|
|
172
|
+
['logout', [{ postLogoutRedirectUri: 'https://brandtegrity.io' }], undefined],
|
|
173
|
+
['login', [{ scopes: ['openid'] }], undefined],
|
|
174
|
+
['register', [{ scopes: ['openid'] }], undefined],
|
|
175
|
+
['entry', ['https://brandtegrity.io/entry'], undefined],
|
|
176
|
+
] as const)('%s on the returned context calls through to the flow instance returned by core sdk', async (method, args, resolvedValue) => {
|
|
177
|
+
const flow = createMockFlow<RedirectFlow>({ [method]: vi.fn().mockReturnValue(resolvedValue) });
|
|
178
|
+
await mockInitFlow(flow);
|
|
179
|
+
|
|
180
|
+
const { useStrivacity } = await import('../../src/runtime/composables/use-strivacity');
|
|
181
|
+
const ctx = useStrivacity<RedirectFlow>();
|
|
182
|
+
|
|
183
|
+
const result = await (ctx[method] as (...a: Array<unknown>) => unknown)(...args);
|
|
184
|
+
|
|
185
|
+
expect(flow[method as keyof RedirectFlow]).toHaveBeenCalledWith(...args);
|
|
186
|
+
expect(result).toEqual(resolvedValue);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe('useSession', () => {
|
|
191
|
+
test('reads/writes the "strivacity_session" nuxt state key', async () => {
|
|
192
|
+
const useState = vi.fn((_key: string, init: () => unknown) => ({ value: init() }));
|
|
193
|
+
vi.doMock('#imports', () => ({ useState }));
|
|
194
|
+
|
|
195
|
+
const { useSession } = await import('../../src/runtime/composables/use-session');
|
|
196
|
+
const session = useSession();
|
|
197
|
+
|
|
198
|
+
expect(useState).toHaveBeenCalledWith('strivacity_session', expect.any(Function));
|
|
199
|
+
expect(session.value).toBeUndefined();
|
|
200
|
+
|
|
201
|
+
vi.doUnmock('#imports');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('useNativeLoginContext', () => {
|
|
206
|
+
test('throws when used outside of a native login session', async () => {
|
|
207
|
+
const { useNativeLoginContext } = await import('../../src/runtime/composables/use-native-login-context');
|
|
208
|
+
|
|
209
|
+
expect(() => {
|
|
210
|
+
const app = createApp({
|
|
211
|
+
setup() {
|
|
212
|
+
useNativeLoginContext();
|
|
213
|
+
|
|
214
|
+
return () => null;
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
app.mount(document.createElement('div'));
|
|
218
|
+
}).toThrow('Missing SDK native login context');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test('resolves to the context provided by an ancestor useNativeLogin call', async () => {
|
|
222
|
+
await mockInitFlow(createMockFlow<NativeFlow>());
|
|
223
|
+
|
|
224
|
+
const { useNativeLoginContext } = await import('../../src/runtime/composables/use-native-login-context');
|
|
225
|
+
const { useNativeLogin } = await import('../../src/runtime/composables/use-native-login');
|
|
226
|
+
|
|
227
|
+
let injected: LoginContext | undefined;
|
|
228
|
+
let login!: LoginContext;
|
|
229
|
+
|
|
230
|
+
const Child = defineComponent({
|
|
231
|
+
setup() {
|
|
232
|
+
injected = useNativeLoginContext();
|
|
233
|
+
|
|
234
|
+
return () => null;
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
const Parent = defineComponent({
|
|
238
|
+
setup() {
|
|
239
|
+
login = useNativeLogin();
|
|
240
|
+
|
|
241
|
+
return () => h(Child);
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const app = createApp(Parent);
|
|
246
|
+
app.mount(document.createElement('div'));
|
|
247
|
+
await flushPromises();
|
|
248
|
+
|
|
249
|
+
expect(injected?.state).toBe(login.state);
|
|
250
|
+
expect(injected?.loading).toBe(login.loading);
|
|
251
|
+
expect(injected?.forms).toBe(login.forms);
|
|
252
|
+
expect(injected?.messages).toBe(login.messages);
|
|
253
|
+
expect(injected?.submitForm).toBe(login.submitForm);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('useNativeLogin', () => {
|
|
258
|
+
test('initializes the sdk and starts a session on mount, defaulting params to an empty object', async () => {
|
|
259
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
260
|
+
await mountWithNativeLogin(sdk);
|
|
261
|
+
|
|
262
|
+
await flushPromises();
|
|
263
|
+
|
|
264
|
+
expect(sdk.init).toHaveBeenCalledTimes(1);
|
|
265
|
+
expect(sdk.startSession).toHaveBeenCalledWith({});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test('forwards the given login params to sdk.startSession', async () => {
|
|
269
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
270
|
+
await mountWithNativeLogin(sdk, { params: { sessionId: 'session-1', language: 'hu-HU' } });
|
|
271
|
+
|
|
272
|
+
await flushPromises();
|
|
273
|
+
|
|
274
|
+
expect(sdk.startSession).toHaveBeenCalledWith({ sessionId: 'session-1', language: 'hu-HU' });
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test('is loading until sdk.startSession resolves with a state that has no pending finalizeUrl', async () => {
|
|
278
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
279
|
+
vi.mocked(sdk.startSession).mockResolvedValue({ screen: 'identifier' });
|
|
280
|
+
const { login } = await mountWithNativeLogin(sdk);
|
|
281
|
+
|
|
282
|
+
expect(login.loading.value).toBe(true);
|
|
283
|
+
|
|
284
|
+
await flushPromises();
|
|
285
|
+
|
|
286
|
+
expect(login.loading.value).toBe(false);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('stays loading forever if sdk.startSession resolves with no state at all', async () => {
|
|
290
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
291
|
+
vi.mocked(sdk.startSession).mockResolvedValue(undefined);
|
|
292
|
+
const { login } = await mountWithNativeLogin(sdk);
|
|
293
|
+
|
|
294
|
+
await flushPromises();
|
|
295
|
+
|
|
296
|
+
expect(login.loading.value).toBe(true);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('applies the returned flow state: resets per-form data, routes the global message, and keeps per-widget messages', async () => {
|
|
300
|
+
const state: NativeFlowState = {
|
|
301
|
+
hostedUrl: 'https://brandtegrity.io/hosted',
|
|
302
|
+
screen: 'identifier',
|
|
303
|
+
forms: [{ id: 'form1', type: 'form', widgets: [] }],
|
|
304
|
+
messages: {
|
|
305
|
+
global: { type: 'info', text: 'Welcome' },
|
|
306
|
+
form1: { field1: { type: 'error', text: 'Required' } },
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
310
|
+
vi.mocked(sdk.startSession).mockResolvedValue(state);
|
|
311
|
+
const onGlobalMessage = vi.fn();
|
|
312
|
+
|
|
313
|
+
const { login } = await mountWithNativeLogin(sdk, { onGlobalMessage });
|
|
314
|
+
await flushPromises();
|
|
315
|
+
|
|
316
|
+
expect(onGlobalMessage).toHaveBeenCalledWith({ type: 'info', text: 'Welcome' });
|
|
317
|
+
expect(login.forms.value).toEqual({ form1: {} });
|
|
318
|
+
expect(login.messages.value).toEqual({ form1: { field1: { type: 'error', text: 'Required' } } });
|
|
319
|
+
expect(login.state.value).toEqual(state);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test('keeps loading while a finalizeUrl is still pending', async () => {
|
|
323
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
324
|
+
vi.mocked(sdk.startSession).mockResolvedValue({ finalizeUrl: 'https://brandtegrity.io/finalize' });
|
|
325
|
+
|
|
326
|
+
const { login } = await mountWithNativeLogin(sdk);
|
|
327
|
+
await flushPromises();
|
|
328
|
+
|
|
329
|
+
expect(login.loading.value).toBe(true);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test('calls onLogin and leaves the tracked state untouched when the sdk already has a session', async () => {
|
|
333
|
+
const session = { access_token: 'access-token' } as unknown as SessionData;
|
|
334
|
+
vi.doMock('../../src/runtime/composables/use-session', () => ({ useSession: () => ({ value: session }) }));
|
|
335
|
+
|
|
336
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
337
|
+
vi.mocked(sdk.startSession).mockResolvedValue({ screen: 'identifier' });
|
|
338
|
+
const onLogin = vi.fn();
|
|
339
|
+
|
|
340
|
+
const { login } = await mountWithNativeLogin(sdk, { onLogin });
|
|
341
|
+
await flushPromises();
|
|
342
|
+
|
|
343
|
+
expect(onLogin).toHaveBeenCalledWith(session);
|
|
344
|
+
expect(login.state.value).toEqual({});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test('routes a FallbackError from sdk.startSession to onFallback and logs it', async () => {
|
|
348
|
+
// FallbackError must come from the same dynamic-import generation as the composable under
|
|
349
|
+
// test (see beforeEach) - a top-level static import would be a different class instance
|
|
350
|
+
// after vi.resetModules(), failing the instanceof check inside use-native-login.ts.
|
|
351
|
+
const { FallbackError } = await import('@strivacity/sdk-core/utils/errors');
|
|
352
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
353
|
+
const fallbackError = new FallbackError(new URL('https://brandtegrity.io/hosted'));
|
|
354
|
+
vi.mocked(sdk.startSession).mockRejectedValue(fallbackError);
|
|
355
|
+
const onFallback = vi.fn();
|
|
356
|
+
|
|
357
|
+
const { login } = await mountWithNativeLogin(sdk, { onFallback });
|
|
358
|
+
await flushPromises();
|
|
359
|
+
|
|
360
|
+
expect(onFallback).toHaveBeenCalledWith(fallbackError);
|
|
361
|
+
expect(sdk.logging!.error).toHaveBeenCalledWith('Fallback error occurred', fallbackError);
|
|
362
|
+
expect(login.loading.value).toBe(true);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test('routes a non-fallback error from sdk.startSession to onError and logs it', async () => {
|
|
366
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
367
|
+
const error = new Error('network down');
|
|
368
|
+
vi.mocked(sdk.startSession).mockRejectedValue(error);
|
|
369
|
+
const onError = vi.fn();
|
|
370
|
+
|
|
371
|
+
await mountWithNativeLogin(sdk, { onError });
|
|
372
|
+
await flushPromises();
|
|
373
|
+
|
|
374
|
+
expect(onError).toHaveBeenCalledWith(error);
|
|
375
|
+
expect(sdk.logging!.error).toHaveBeenCalledWith('Error starting session', error);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe('submitForm', () => {
|
|
379
|
+
test('unflattens the tracked form values and calls through to sdk.submitForm', async () => {
|
|
380
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
381
|
+
const initialForms = [{ id: 'form1', type: 'form' as const, widgets: [] }];
|
|
382
|
+
vi.mocked(sdk.startSession).mockResolvedValue({ screen: 'identifier', forms: initialForms });
|
|
383
|
+
vi.mocked(sdk.submitForm).mockResolvedValue({ screen: 'done' });
|
|
384
|
+
|
|
385
|
+
const { login } = await mountWithNativeLogin(sdk);
|
|
386
|
+
await flushPromises();
|
|
387
|
+
|
|
388
|
+
login.setFormValue('form1', 'address.city', 'Budapest');
|
|
389
|
+
login.setFormValue('form1', 'address.zip', '');
|
|
390
|
+
|
|
391
|
+
await login.submitForm('form1');
|
|
392
|
+
|
|
393
|
+
expect(sdk.submitForm).toHaveBeenCalledWith('form1', { address: { city: 'Budapest', zip: null } });
|
|
394
|
+
// unspecified fields on the new state fall back to what was already tracked (except messages, which always resets)
|
|
395
|
+
expect(login.state.value).toEqual({
|
|
396
|
+
hostedUrl: undefined,
|
|
397
|
+
finalizeUrl: undefined,
|
|
398
|
+
screen: 'done',
|
|
399
|
+
forms: initialForms,
|
|
400
|
+
layout: undefined,
|
|
401
|
+
messages: {},
|
|
402
|
+
branding: undefined,
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test('uses a given custom body instead of the tracked form values', async () => {
|
|
407
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
408
|
+
vi.mocked(sdk.submitForm).mockResolvedValue({ screen: 'done' });
|
|
409
|
+
|
|
410
|
+
const { login } = await mountWithNativeLogin(sdk);
|
|
411
|
+
await flushPromises();
|
|
412
|
+
|
|
413
|
+
await login.submitForm('form1', { raw: true });
|
|
414
|
+
|
|
415
|
+
expect(sdk.submitForm).toHaveBeenCalledWith('form1', { raw: true });
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test('routes a FallbackError to onFallback', async () => {
|
|
419
|
+
// see the same-generation note on the sibling FallbackError test above
|
|
420
|
+
const { FallbackError } = await import('@strivacity/sdk-core/utils/errors');
|
|
421
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
422
|
+
const fallbackError = new FallbackError(new URL('https://brandtegrity.io/hosted'));
|
|
423
|
+
vi.mocked(sdk.submitForm).mockRejectedValue(fallbackError);
|
|
424
|
+
const onFallback = vi.fn();
|
|
425
|
+
|
|
426
|
+
const { login } = await mountWithNativeLogin(sdk, { onFallback });
|
|
427
|
+
await flushPromises();
|
|
428
|
+
|
|
429
|
+
await login.submitForm('form1');
|
|
430
|
+
|
|
431
|
+
expect(onFallback).toHaveBeenCalledWith(fallbackError);
|
|
432
|
+
expect(sdk.logging!.error).toHaveBeenCalledWith('Fallback error occurred', fallbackError);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test('routes a non-fallback error to onError', async () => {
|
|
436
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
437
|
+
const error = new Error('submit failed');
|
|
438
|
+
vi.mocked(sdk.submitForm).mockRejectedValue(error);
|
|
439
|
+
const onError = vi.fn();
|
|
440
|
+
|
|
441
|
+
const { login } = await mountWithNativeLogin(sdk, { onError });
|
|
442
|
+
await flushPromises();
|
|
443
|
+
|
|
444
|
+
await login.submitForm('form1');
|
|
445
|
+
|
|
446
|
+
expect(onError).toHaveBeenCalledWith(error);
|
|
447
|
+
expect(sdk.logging!.error).toHaveBeenCalledWith('Error submitting form', error);
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test('setFormValue coerces empty strings to null and lazily creates the form bucket', async () => {
|
|
452
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
453
|
+
const { login } = await mountWithNativeLogin(sdk);
|
|
454
|
+
await flushPromises();
|
|
455
|
+
|
|
456
|
+
login.setFormValue('newForm', 'field', 'value');
|
|
457
|
+
login.setFormValue('newForm', 'other', '');
|
|
458
|
+
|
|
459
|
+
expect(login.forms.value.newForm).toEqual({ field: 'value', other: null });
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test('setMessage lazily creates the message bucket for a form', async () => {
|
|
463
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
464
|
+
const { login } = await mountWithNativeLogin(sdk);
|
|
465
|
+
await flushPromises();
|
|
466
|
+
|
|
467
|
+
login.setMessage('newForm', 'field', { type: 'error', text: 'Required' });
|
|
468
|
+
|
|
469
|
+
expect(login.messages.value.newForm).toEqual({ field: { type: 'error', text: 'Required' } });
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
describe('triggerFallback', () => {
|
|
473
|
+
test('throws when no hosted URL is known yet', async () => {
|
|
474
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
475
|
+
vi.mocked(sdk.startSession).mockResolvedValue({ screen: 'identifier' });
|
|
476
|
+
const { login } = await mountWithNativeLogin(sdk);
|
|
477
|
+
await flushPromises();
|
|
478
|
+
|
|
479
|
+
expect(() => login.triggerFallback()).toThrow('No hosted URL provided');
|
|
480
|
+
expect(sdk.logging!.error).toHaveBeenCalledWith('Fallback error', expect.any(Error));
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test('calls onFallback with a FallbackError built from the known hosted URL', async () => {
|
|
484
|
+
// see the same-generation note on the sibling FallbackError test above
|
|
485
|
+
const { FallbackError } = await import('@strivacity/sdk-core/utils/errors');
|
|
486
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
487
|
+
vi.mocked(sdk.startSession).mockResolvedValue({ hostedUrl: 'https://brandtegrity.io/hosted', screen: 'identifier' });
|
|
488
|
+
const onFallback = vi.fn();
|
|
489
|
+
|
|
490
|
+
const { login } = await mountWithNativeLogin(sdk, { onFallback });
|
|
491
|
+
await flushPromises();
|
|
492
|
+
|
|
493
|
+
login.triggerFallback('manual trigger');
|
|
494
|
+
|
|
495
|
+
expect(sdk.logging!.warn).toHaveBeenCalledWith('Triggering fallback due to: manual trigger');
|
|
496
|
+
expect(onFallback).toHaveBeenCalledWith(expect.any(FallbackError));
|
|
497
|
+
expect((onFallback.mock.calls[0][0] as FallbackErrorType).url.toString()).toBe('https://brandtegrity.io/hosted');
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test('triggerClose calls onClose and logs', async () => {
|
|
502
|
+
const sdk = createMockFlow<NativeFlow>();
|
|
503
|
+
const onClose = vi.fn();
|
|
504
|
+
const { login } = await mountWithNativeLogin(sdk, { onClose });
|
|
505
|
+
await flushPromises();
|
|
506
|
+
|
|
507
|
+
login.triggerClose();
|
|
508
|
+
|
|
509
|
+
expect(onClose).toHaveBeenCalledTimes(1);
|
|
510
|
+
expect(sdk.logging!.debug).toHaveBeenCalledWith('Triggering close');
|
|
511
|
+
});
|
|
512
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { H3Event } from 'h3';
|
|
2
|
+
import { describe, test, expect, vi } from 'vitest';
|
|
3
|
+
import { proxyResponse, getLocaleFromEvent, toSafeRedirect } from '../../../src/runtime/server/utils/helpers';
|
|
4
|
+
import { createHttpResponse } from '@strivacity/testing/mocks/sdk';
|
|
5
|
+
|
|
6
|
+
function createFakeEvent(headers: Record<string, string> = {}): H3Event {
|
|
7
|
+
return { headers: new Headers(headers) } as unknown as H3Event;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('proxyResponse', () => {
|
|
11
|
+
test('copies status and headers from the given response onto a new Response', () => {
|
|
12
|
+
const response = createHttpResponse({ status: 201, statusText: 'Created', headers: new Headers({ 'content-type': 'application/json' }) });
|
|
13
|
+
|
|
14
|
+
const proxied = proxyResponse(response);
|
|
15
|
+
|
|
16
|
+
expect(proxied.status).toBe(201);
|
|
17
|
+
expect(proxied.headers.get('content-type')).toBe('application/json');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('strips disallowed proxy headers', () => {
|
|
21
|
+
const response = createHttpResponse({ headers: new Headers({ 'content-encoding': 'gzip', 'content-type': 'application/json' }) });
|
|
22
|
+
|
|
23
|
+
const proxied = proxyResponse(response);
|
|
24
|
+
|
|
25
|
+
expect(proxied.headers.has('content-encoding')).toBe(false);
|
|
26
|
+
expect(proxied.headers.get('content-type')).toBe('application/json');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('preserves a set-cookie header even though it is otherwise skipped by the generic copy loop', () => {
|
|
30
|
+
// happy-dom's Response constructor strips set-cookie from the resulting Headers entirely
|
|
31
|
+
// (a real spec-compliant restriction on response headers), so the outcome can't be observed
|
|
32
|
+
// by reading proxied.headers back out here - spying on the write is the reachable assertion.
|
|
33
|
+
const setSpy = vi.spyOn(Headers.prototype, 'set');
|
|
34
|
+
const response = createHttpResponse({ headers: new Headers({ 'set-cookie': 'sty.session=abc; Path=/' }) });
|
|
35
|
+
|
|
36
|
+
proxyResponse(response);
|
|
37
|
+
|
|
38
|
+
expect(setSpy).toHaveBeenCalledWith('set-cookie', 'sty.session=abc; Path=/');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('carries the response body through unchanged', async () => {
|
|
42
|
+
const response = createHttpResponse({ body: 'raw-body' as unknown as ReadableStream });
|
|
43
|
+
|
|
44
|
+
const proxied = proxyResponse(response);
|
|
45
|
+
|
|
46
|
+
await expect(proxied.text()).resolves.toBe('raw-body');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('getLocaleFromEvent', () => {
|
|
51
|
+
test('returns undefined when there is no accept-language header', () => {
|
|
52
|
+
expect(getLocaleFromEvent(createFakeEvent())).toBeUndefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('extracts the first locale, ignoring quality values', () => {
|
|
56
|
+
expect(getLocaleFromEvent(createFakeEvent({ 'accept-language': 'hu-HU;q=0.9, en-US;q=0.8' }))).toBe('hu-HU');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('trims surrounding whitespace from the extracted locale', () => {
|
|
60
|
+
expect(getLocaleFromEvent(createFakeEvent({ 'accept-language': ' hu-HU , en-US' }))).toBe('hu-HU');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('toSafeRedirect', () => {
|
|
65
|
+
test('returns null when no redirect is given', () => {
|
|
66
|
+
expect(toSafeRedirect(undefined, 'https://brandtegrity.io')).toBeNull();
|
|
67
|
+
expect(toSafeRedirect(null, 'https://brandtegrity.io')).toBeNull();
|
|
68
|
+
expect(toSafeRedirect('', 'https://brandtegrity.io')).toBeNull();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('resolves a relative path against the safe base URL', () => {
|
|
72
|
+
expect(toSafeRedirect('/dashboard', 'https://brandtegrity.io')).toBe('https://brandtegrity.io/dashboard');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('accepts an absolute URL that shares the same origin', () => {
|
|
76
|
+
expect(toSafeRedirect('https://brandtegrity.io/dashboard?x=1', 'https://brandtegrity.io')).toBe('https://brandtegrity.io/dashboard?x=1');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('rejects an absolute URL on a different origin (open redirect protection)', () => {
|
|
80
|
+
expect(toSafeRedirect('https://evil.example.com/phish', 'https://brandtegrity.io')).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('returns null when the dangerous redirect cannot be parsed into a URL at all', () => {
|
|
84
|
+
expect(toSafeRedirect('http://[::1', 'https://brandtegrity.io')).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
});
|