@polyguard/sdk 1.4.2 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +200 -0
- package/README.md +328 -0
- package/dist/sdk.esm.js +16 -4
- package/dist/sdk.js +16 -4
- package/dist/server.esm.js +83 -0
- package/package.json +14 -3
- package/dist/sdk.global.js +0 -11256
- package/scripts/regenerate-client.sh +0 -59
- package/src/PolyguardClient.js +0 -25
- package/src/PolyguardWebsocketClientImpl.js +0 -183
- package/src/__tests__/PolyguardClient.test.js +0 -65
- package/src/__tests__/PolyguardWebsocketClientImpl.test.js +0 -574
- package/src/__tests__/helpers/fixtures.js +0 -27
- package/src/__tests__/helpers/mockWebSocket.js +0 -66
- package/src/__tests__/sidebar.test.js +0 -289
- package/src/browser.js +0 -9
- package/src/generated/.babelrc +0 -33
- package/src/generated/.openapi-generator/FILES +0 -63
- package/src/generated/.openapi-generator/VERSION +0 -1
- package/src/generated/.openapi-generator-ignore +0 -23
- package/src/generated/.travis.yml +0 -5
- package/src/generated/README.md +0 -211
- package/src/generated/docs/ApiControllersPslStirCallRequest.md +0 -18
- package/src/generated/docs/ApiModelsApiCallModelsCallRequest.md +0 -12
- package/src/generated/docs/AppId.md +0 -8
- package/src/generated/docs/AppleApi.md +0 -88
- package/src/generated/docs/AuthApi.md +0 -1464
- package/src/generated/docs/BlockingApi.md +0 -208
- package/src/generated/docs/CallsApi.md +0 -140
- package/src/generated/docs/Certainty.md +0 -8
- package/src/generated/docs/CreateLinkRequest.md +0 -12
- package/src/generated/docs/DefaultApi.md +0 -128
- package/src/generated/docs/HTTPValidationError.md +0 -9
- package/src/generated/docs/InviteRequestModel.md +0 -10
- package/src/generated/docs/JWTRequest.md +0 -11
- package/src/generated/docs/LinksApi.md +0 -162
- package/src/generated/docs/NumberVerification.md +0 -10
- package/src/generated/docs/SdkApi.md +0 -54
- package/src/generated/docs/SecureLinksApi.md +0 -614
- package/src/generated/docs/StartAttestationRequest.md +0 -9
- package/src/generated/docs/StartMeetingRequest.md +0 -9
- package/src/generated/docs/StirApi.md +0 -52
- package/src/generated/docs/TwilioApi.md +0 -138
- package/src/generated/docs/UsersApi.md +0 -362
- package/src/generated/docs/ValidationError.md +0 -11
- package/src/generated/docs/ValidationErrorLocInner.md +0 -8
- package/src/generated/docs/VeriffApi.md +0 -188
- package/src/generated/docs/VeriffSessionRequest.md +0 -10
- package/src/generated/docs/VerifyRequest.md +0 -10
- package/src/generated/docs/WellKnownApi.md +0 -128
- package/src/generated/docs/ZoomApi.md +0 -848
- package/src/generated/git_push.sh +0 -57
- package/src/generated/mocha.opts +0 -1
- package/src/generated/package.json +0 -46
- package/src/generated/src/ApiClient.js +0 -677
- package/src/generated/src/api/AppleApi.js +0 -109
- package/src/generated/src/api/AuthApi.js +0 -1512
- package/src/generated/src/api/BlockingApi.js +0 -217
- package/src/generated/src/api/CallsApi.js +0 -164
- package/src/generated/src/api/DefaultApi.js +0 -145
- package/src/generated/src/api/LinksApi.js +0 -195
- package/src/generated/src/api/SdkApi.js +0 -81
- package/src/generated/src/api/SecureLinksApi.js +0 -670
- package/src/generated/src/api/StirApi.js +0 -80
- package/src/generated/src/api/TwilioApi.js +0 -158
- package/src/generated/src/api/UsersApi.js +0 -375
- package/src/generated/src/api/VeriffApi.js +0 -209
- package/src/generated/src/api/WellKnownApi.js +0 -145
- package/src/generated/src/api/ZoomApi.js +0 -823
- package/src/generated/src/index.js +0 -244
- package/src/generated/src/model/ApiControllersPslStirCallRequest.js +0 -211
- package/src/generated/src/model/ApiModelsApiCallModelsCallRequest.js +0 -119
- package/src/generated/src/model/CreateLinkRequest.js +0 -131
- package/src/generated/src/model/HTTPValidationError.js +0 -94
- package/src/generated/src/model/InviteRequestModel.js +0 -109
- package/src/generated/src/model/JWTRequest.js +0 -113
- package/src/generated/src/model/NumberVerification.js +0 -107
- package/src/generated/src/model/StartAttestationRequest.js +0 -95
- package/src/generated/src/model/StartMeetingRequest.js +0 -95
- package/src/generated/src/model/ValidationError.js +0 -130
- package/src/generated/src/model/VeriffSessionRequest.js +0 -107
- package/src/generated/src/model/VerifyRequest.js +0 -109
- package/src/generated/test/api/AppleApi.spec.js +0 -73
- package/src/generated/test/api/AuthApi.spec.js +0 -353
- package/src/generated/test/api/BlockingApi.spec.js +0 -103
- package/src/generated/test/api/CallsApi.spec.js +0 -83
- package/src/generated/test/api/DefaultApi.spec.js +0 -73
- package/src/generated/test/api/LinksApi.spec.js +0 -83
- package/src/generated/test/api/SdkApi.spec.js +0 -63
- package/src/generated/test/api/SecureLinksApi.spec.js +0 -143
- package/src/generated/test/api/StirApi.spec.js +0 -63
- package/src/generated/test/api/TwilioApi.spec.js +0 -83
- package/src/generated/test/api/UsersApi.spec.js +0 -133
- package/src/generated/test/api/VeriffApi.spec.js +0 -93
- package/src/generated/test/api/WellKnownApi.spec.js +0 -83
- package/src/generated/test/api/ZoomApi.spec.js +0 -213
- package/src/generated/test/model/ApiControllersPslStirCallRequest.spec.js +0 -119
- package/src/generated/test/model/ApiModelsApiCallModelsCallRequest.spec.js +0 -83
- package/src/generated/test/model/AppId.spec.js +0 -59
- package/src/generated/test/model/Certainty.spec.js +0 -59
- package/src/generated/test/model/CreateLinkRequest.spec.js +0 -83
- package/src/generated/test/model/HTTPValidationError.spec.js +0 -65
- package/src/generated/test/model/InviteRequestModel.spec.js +0 -71
- package/src/generated/test/model/JWTRequest.spec.js +0 -77
- package/src/generated/test/model/NumberVerification.spec.js +0 -71
- package/src/generated/test/model/StartAttestationRequest.spec.js +0 -65
- package/src/generated/test/model/StartMeetingRequest.spec.js +0 -65
- package/src/generated/test/model/ValidationError.spec.js +0 -77
- package/src/generated/test/model/ValidationErrorLocInner.spec.js +0 -59
- package/src/generated/test/model/VeriffSessionRequest.spec.js +0 -71
- package/src/generated/test/model/VerifyRequest.spec.js +0 -71
- package/src/index.js +0 -16
- package/src/messageHandler.js +0 -121
- package/src/ticketService.js +0 -24
- package/src/ui.js +0 -88
- package/vitest.config.js +0 -10
|
@@ -1,574 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { buildFakeJwt, DEFAULT_PARAMS, mockTicketResponse } from './helpers/fixtures.js';
|
|
3
|
-
import { MockReconnectingWebSocket } from './helpers/mockWebSocket.js';
|
|
4
|
-
|
|
5
|
-
// ---- Module mocks (using vi.hoisted to avoid hoisting issues) ----
|
|
6
|
-
|
|
7
|
-
const API_CLASS_NAMES = [
|
|
8
|
-
'BlockingApi', 'CallsApi', 'DefaultApi', 'LinksApi', 'SdkApi',
|
|
9
|
-
'SecureLinksApi', 'StirApi', 'TwilioApi', 'UsersApi', 'VeriffApi',
|
|
10
|
-
'WellKnownApi', 'ZoomApi',
|
|
11
|
-
];
|
|
12
|
-
|
|
13
|
-
// Use vi.hoisted so these are available when vi.mock factories run (which are hoisted)
|
|
14
|
-
const { MockApiClient, generatedMock, mockQRCode } = vi.hoisted(() => {
|
|
15
|
-
class MockApiClient {
|
|
16
|
-
constructor() {
|
|
17
|
-
this.basePath = '';
|
|
18
|
-
this.authentications = { bearerAuth: { apiKey: null } };
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const generatedMock = { ApiClient: MockApiClient };
|
|
23
|
-
const apiClassNames = [
|
|
24
|
-
'BlockingApi', 'CallsApi', 'DefaultApi', 'LinksApi', 'SdkApi',
|
|
25
|
-
'SecureLinksApi', 'StirApi', 'TwilioApi', 'UsersApi', 'VeriffApi',
|
|
26
|
-
'WellKnownApi', 'ZoomApi',
|
|
27
|
-
];
|
|
28
|
-
for (const name of apiClassNames) {
|
|
29
|
-
generatedMock[name] = class {
|
|
30
|
-
constructor(apiClient) { this._apiClient = apiClient; }
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const mockQRCode = {
|
|
35
|
-
toString: null, // will be set per-test via vi.fn()
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
return { MockApiClient, generatedMock, mockQRCode };
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
vi.mock('../generated/src', () => generatedMock);
|
|
42
|
-
|
|
43
|
-
vi.mock('reconnecting-websocket', async () => {
|
|
44
|
-
// Import the actual mock class at runtime (not hoisted)
|
|
45
|
-
const { MockReconnectingWebSocket } = await import('./helpers/mockWebSocket.js');
|
|
46
|
-
return { default: MockReconnectingWebSocket };
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
vi.mock('qrcode', () => ({
|
|
50
|
-
default: mockQRCode,
|
|
51
|
-
}));
|
|
52
|
-
|
|
53
|
-
// Import after mocks
|
|
54
|
-
const { PolyguardWebsocketClientImpl } = await import('../PolyguardWebsocketClientImpl.js');
|
|
55
|
-
|
|
56
|
-
// ---- Helpers ----
|
|
57
|
-
|
|
58
|
-
/** Flush microtasks/timers to let async code settle */
|
|
59
|
-
async function flushMicrotasks() {
|
|
60
|
-
// Use a real setTimeout(0) to flush the microtask queue
|
|
61
|
-
await new Promise(resolve => setTimeout(resolve, 0));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Set up fetch mock that returns a valid ticket response */
|
|
65
|
-
function stubFetch(ticket = 'test-ticket-123', ok = true) {
|
|
66
|
-
const mock = vi.fn().mockResolvedValue(mockTicketResponse(ticket, ok));
|
|
67
|
-
vi.stubGlobal('fetch', mock);
|
|
68
|
-
return mock;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Set window.location.protocol */
|
|
72
|
-
function setProtocol(protocol) {
|
|
73
|
-
Object.defineProperty(window, 'location', {
|
|
74
|
-
value: { protocol, assign: vi.fn(), href: '' },
|
|
75
|
-
writable: true,
|
|
76
|
-
configurable: true,
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Set navigator.userAgent */
|
|
81
|
-
function setUserAgent(ua) {
|
|
82
|
-
Object.defineProperty(navigator, 'userAgent', {
|
|
83
|
-
value: ua,
|
|
84
|
-
writable: true,
|
|
85
|
-
configurable: true,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Start verify and wait for the WebSocket to be created.
|
|
91
|
-
* Returns { promise, ws }.
|
|
92
|
-
*/
|
|
93
|
-
async function startVerifyAndGetWs(client, target = null, rawJwt = false) {
|
|
94
|
-
const promise = client.verify(target, rawJwt);
|
|
95
|
-
// Flush to let the async executor run through fetch + WS creation
|
|
96
|
-
await flushMicrotasks();
|
|
97
|
-
const ws = MockReconnectingWebSocket.latest();
|
|
98
|
-
return { promise, ws };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ---- Tests ----
|
|
102
|
-
|
|
103
|
-
describe('PolyguardWebsocketClientImpl', () => {
|
|
104
|
-
beforeEach(() => {
|
|
105
|
-
vi.clearAllMocks();
|
|
106
|
-
MockReconnectingWebSocket.reset();
|
|
107
|
-
setProtocol('https:');
|
|
108
|
-
setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)');
|
|
109
|
-
stubFetch();
|
|
110
|
-
// Reset QRCode mock
|
|
111
|
-
mockQRCode.toString = vi.fn((data, opts, cb) => cb(null, '<svg>mock-qr</svg>'));
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
afterEach(() => {
|
|
115
|
-
vi.restoreAllMocks();
|
|
116
|
-
document.body.innerHTML = '';
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// ---------- constructor ----------
|
|
120
|
-
describe('constructor', () => {
|
|
121
|
-
it('defaults requiredProofs to ["Full Name"]', () => {
|
|
122
|
-
const client = new PolyguardWebsocketClientImpl({ appId: 'x', apiServer: 'y' });
|
|
123
|
-
expect(client.requiredProofs).toEqual(['Full Name']);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('defaults scanType to "single"', () => {
|
|
127
|
-
const client = new PolyguardWebsocketClientImpl({ appId: 'x', apiServer: 'y' });
|
|
128
|
-
expect(client.scanType).toBe('single');
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('stores custom requiredProofs', () => {
|
|
132
|
-
const client = new PolyguardWebsocketClientImpl({ ...DEFAULT_PARAMS });
|
|
133
|
-
expect(client.requiredProofs).toEqual(['Full Name', 'Email']);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('stores all params correctly', () => {
|
|
137
|
-
const params = {
|
|
138
|
-
...DEFAULT_PARAMS,
|
|
139
|
-
redirectUrl: 'https://example.com/redirect',
|
|
140
|
-
callbackUrl: 'https://example.com/callback',
|
|
141
|
-
cookieName: 'pg_token',
|
|
142
|
-
link_uuid: 'link-abc',
|
|
143
|
-
};
|
|
144
|
-
const client = new PolyguardWebsocketClientImpl(params);
|
|
145
|
-
expect(client.appId).toBe(params.appId);
|
|
146
|
-
expect(client.apiServer).toBe(params.apiServer);
|
|
147
|
-
expect(client.scanType).toBe('single');
|
|
148
|
-
expect(client.redirectUrl).toBe(params.redirectUrl);
|
|
149
|
-
expect(client.callbackUrl).toBe(params.callbackUrl);
|
|
150
|
-
expect(client.cookieName).toBe(params.cookieName);
|
|
151
|
-
expect(client.link_uuid).toBe(params.link_uuid);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('creates an ApiClient instance', () => {
|
|
155
|
-
const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
|
|
156
|
-
expect(client.apiClient).toBeDefined();
|
|
157
|
-
expect(client.apiClient.authentications).toBeDefined();
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('sets basePath when baseUrl is provided', () => {
|
|
161
|
-
const client = new PolyguardWebsocketClientImpl({ ...DEFAULT_PARAMS, baseUrl: 'https://custom.api' });
|
|
162
|
-
expect(client.apiClient.basePath).toBe('https://custom.api');
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('sets apiKey when provided', () => {
|
|
166
|
-
const client = new PolyguardWebsocketClientImpl({ ...DEFAULT_PARAMS, apiKey: 'my-key' });
|
|
167
|
-
expect(client.apiClient.authentications.bearerAuth.apiKey).toBe('my-key');
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('sets integrationType to websocket', () => {
|
|
171
|
-
const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
|
|
172
|
-
expect(client.integrationType).toBe('websocket');
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('instantiates all 12 API service classes as lowercase-first properties', () => {
|
|
176
|
-
const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
|
|
177
|
-
for (const name of API_CLASS_NAMES) {
|
|
178
|
-
const propName = name.charAt(0).toLowerCase() + name.slice(1);
|
|
179
|
-
expect(client[propName]).toBeDefined();
|
|
180
|
-
expect(client[propName]._apiClient).toBe(client.apiClient);
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('constructor with no params does not throw', () => {
|
|
185
|
-
expect(() => new PolyguardWebsocketClientImpl()).not.toThrow();
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
// ---------- buildModal ----------
|
|
190
|
-
describe('buildModal', () => {
|
|
191
|
-
let client;
|
|
192
|
-
let modal;
|
|
193
|
-
|
|
194
|
-
beforeEach(() => {
|
|
195
|
-
client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
|
|
196
|
-
modal = client.buildModal();
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('returns a div element', () => {
|
|
200
|
-
expect(modal.tagName).toBe('DIV');
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('is a fixed-position fullscreen overlay', () => {
|
|
204
|
-
expect(modal.style.position).toBe('fixed');
|
|
205
|
-
expect(modal.style.top).toBe('0px');
|
|
206
|
-
expect(modal.style.left).toBe('0px');
|
|
207
|
-
expect(modal.style.width).toBe('100vw');
|
|
208
|
-
expect(modal.style.height).toBe('100vh');
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('contains close button', () => {
|
|
212
|
-
expect(modal.querySelector('#polyguard-modal-close')).not.toBeNull();
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it('contains QR div', () => {
|
|
216
|
-
expect(modal.querySelector('#polyguard-qr')).not.toBeNull();
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('contains cancel button', () => {
|
|
220
|
-
expect(modal.querySelector('#polyguard-modal-cancel')).not.toBeNull();
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it('contains error div that is initially hidden', () => {
|
|
224
|
-
const errDiv = modal.querySelector('#polyguard-error');
|
|
225
|
-
expect(errDiv).not.toBeNull();
|
|
226
|
-
expect(errDiv.style.display).toBe('none');
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('contains modal content wrapper', () => {
|
|
230
|
-
expect(modal.querySelector('#polyguard-modal-content')).not.toBeNull();
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
// ---------- verify - ticket fetch ----------
|
|
235
|
-
describe('verify - ticket fetch', () => {
|
|
236
|
-
it('builds ticket URL without link_uuid', async () => {
|
|
237
|
-
const fetchMock = stubFetch();
|
|
238
|
-
const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
|
|
239
|
-
const { promise, ws } = await startVerifyAndGetWs(client);
|
|
240
|
-
expect(fetchMock).toHaveBeenCalled();
|
|
241
|
-
const url = fetchMock.mock.calls[0][0];
|
|
242
|
-
expect(url).toBe(`https://${DEFAULT_PARAMS.apiServer}/v2/ticket/${DEFAULT_PARAMS.appId}`);
|
|
243
|
-
if (ws) ws.simulateMessage({ jwt: 'cleanup' });
|
|
244
|
-
await promise.catch(() => {});
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it('builds ticket URL with link_uuid', async () => {
|
|
248
|
-
const fetchMock = stubFetch();
|
|
249
|
-
const client = new PolyguardWebsocketClientImpl({ ...DEFAULT_PARAMS, link_uuid: 'link-123' });
|
|
250
|
-
const { promise, ws } = await startVerifyAndGetWs(client);
|
|
251
|
-
expect(fetchMock).toHaveBeenCalled();
|
|
252
|
-
const url = fetchMock.mock.calls[0][0];
|
|
253
|
-
expect(url).toBe(`https://${DEFAULT_PARAMS.apiServer}/v2/ticket/${DEFAULT_PARAMS.appId}/link-123`);
|
|
254
|
-
if (ws) ws.simulateMessage({ jwt: 'cleanup' });
|
|
255
|
-
await promise.catch(() => {});
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('sends POST with requiredProofs and scanType in body', async () => {
|
|
259
|
-
const fetchMock = stubFetch();
|
|
260
|
-
const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
|
|
261
|
-
const { promise, ws } = await startVerifyAndGetWs(client);
|
|
262
|
-
expect(fetchMock).toHaveBeenCalled();
|
|
263
|
-
const opts = fetchMock.mock.calls[0][1];
|
|
264
|
-
expect(opts.method).toBe('POST');
|
|
265
|
-
expect(opts.headers['Content-Type']).toBe('application/json');
|
|
266
|
-
const body = JSON.parse(opts.body);
|
|
267
|
-
expect(body.requiredProofs).toEqual(DEFAULT_PARAMS.requiredProofs);
|
|
268
|
-
expect(body.scanType).toBe(DEFAULT_PARAMS.scanType);
|
|
269
|
-
if (ws) ws.simulateMessage({ jwt: 'cleanup' });
|
|
270
|
-
await promise.catch(() => {});
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it('resolves with error presence on non-ok response', async () => {
|
|
274
|
-
stubFetch('test-ticket', false);
|
|
275
|
-
const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
|
|
276
|
-
document.body.innerHTML = '<div id="test-target"></div>';
|
|
277
|
-
const result = await client.verify('test-target');
|
|
278
|
-
expect(result).toEqual({ presence: { score: 'OFFLINE', msg: 'Failed to get ticket' } });
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it('resolves with error presence when no ticket in response', async () => {
|
|
282
|
-
stubFetch(null, true);
|
|
283
|
-
const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
|
|
284
|
-
document.body.innerHTML = '<div id="test-target"></div>';
|
|
285
|
-
const result = await client.verify('test-target');
|
|
286
|
-
expect(result).toEqual({ presence: { score: 'OFFLINE', msg: 'No ticket returned from server' } });
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it('resolves with error presence on fetch exception', async () => {
|
|
290
|
-
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('Network error')));
|
|
291
|
-
const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
|
|
292
|
-
document.body.innerHTML = '<div id="test-target"></div>';
|
|
293
|
-
const result = await client.verify('test-target');
|
|
294
|
-
expect(result).toEqual({ presence: { score: 'OFFLINE', msg: 'Failed to connect to WebSocket' } });
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
// ---------- verify - WebSocket ----------
|
|
299
|
-
describe('verify - WebSocket', () => {
|
|
300
|
-
let client;
|
|
301
|
-
|
|
302
|
-
beforeEach(() => {
|
|
303
|
-
stubFetch();
|
|
304
|
-
client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
it('uses wss protocol on https', async () => {
|
|
308
|
-
setProtocol('https:');
|
|
309
|
-
const { promise, ws } = await startVerifyAndGetWs(client);
|
|
310
|
-
expect(ws).toBeDefined();
|
|
311
|
-
expect(ws.url).toMatch(/^wss:\/\//);
|
|
312
|
-
ws.simulateMessage({ jwt: 'cleanup' });
|
|
313
|
-
await promise.catch(() => {});
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
it('uses ws protocol on http', async () => {
|
|
317
|
-
setProtocol('http:');
|
|
318
|
-
const { promise, ws } = await startVerifyAndGetWs(client);
|
|
319
|
-
expect(ws).toBeDefined();
|
|
320
|
-
expect(ws.url).toMatch(/^ws:\/\//);
|
|
321
|
-
ws.simulateMessage({ jwt: 'cleanup' });
|
|
322
|
-
await promise.catch(() => {});
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
it('builds correct WebSocket URL', async () => {
|
|
326
|
-
const { promise, ws } = await startVerifyAndGetWs(client);
|
|
327
|
-
expect(ws).toBeDefined();
|
|
328
|
-
expect(ws.url).toBe(`wss://${DEFAULT_PARAMS.apiServer}/v2/realtime/test-ticket-123`);
|
|
329
|
-
ws.simulateMessage({ jwt: 'cleanup' });
|
|
330
|
-
await promise.catch(() => {});
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
it('responds pong to ping with matching seq', async () => {
|
|
334
|
-
const { promise, ws } = await startVerifyAndGetWs(client);
|
|
335
|
-
ws.simulateMessage({ type: 'ping', seq: 42 });
|
|
336
|
-
expect(ws.sent).toHaveLength(1);
|
|
337
|
-
expect(JSON.parse(ws.sent[0])).toEqual({ type: 'pong', seq: 42 });
|
|
338
|
-
ws.simulateMessage({ jwt: 'cleanup' });
|
|
339
|
-
await promise.catch(() => {});
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
it('jwt message resolves promise with JWT string', async () => {
|
|
343
|
-
const fakeJwt = buildFakeJwt({ 'Full Name': 'Alice' });
|
|
344
|
-
const { promise, ws } = await startVerifyAndGetWs(client);
|
|
345
|
-
ws.simulateMessage({ jwt: fakeJwt });
|
|
346
|
-
const result = await promise;
|
|
347
|
-
expect(result).toBe(fakeJwt);
|
|
348
|
-
});
|
|
349
|
-
|
|
350
|
-
it('jwt message with rawJwt=true resolves with full data object', async () => {
|
|
351
|
-
const fakeJwt = buildFakeJwt({ 'Full Name': 'Alice' });
|
|
352
|
-
const { promise, ws } = await startVerifyAndGetWs(client, null, true);
|
|
353
|
-
ws.simulateMessage({ jwt: fakeJwt });
|
|
354
|
-
const result = await promise;
|
|
355
|
-
expect(result).toEqual({ jwt: fakeJwt });
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
it('jwt message closes WebSocket', async () => {
|
|
359
|
-
const fakeJwt = buildFakeJwt({});
|
|
360
|
-
const { promise, ws } = await startVerifyAndGetWs(client);
|
|
361
|
-
ws.simulateMessage({ jwt: fakeJwt });
|
|
362
|
-
await promise;
|
|
363
|
-
expect(ws.readyState).toBe(3); // CLOSED
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
it('error message resolves with error presence', async () => {
|
|
367
|
-
document.body.innerHTML = '<div id="test-target"></div>';
|
|
368
|
-
const c = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
|
|
369
|
-
const { promise, ws } = await startVerifyAndGetWs(c, 'test-target');
|
|
370
|
-
ws.simulateMessage({ error: 'Something went wrong' });
|
|
371
|
-
const result = await promise;
|
|
372
|
-
expect(result).toEqual({ presence: { score: 'OFFLINE', msg: 'Something went wrong' } });
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
it('url message calls window.location.assign', async () => {
|
|
376
|
-
const { promise, ws } = await startVerifyAndGetWs(client);
|
|
377
|
-
ws.simulateMessage({ url: 'https://example.com/redirect' });
|
|
378
|
-
expect(window.location.assign).toHaveBeenCalledWith('https://example.com/redirect');
|
|
379
|
-
ws.simulateMessage({ jwt: 'cleanup' });
|
|
380
|
-
await promise.catch(() => {});
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
it('status message resets QR div to spinner', async () => {
|
|
384
|
-
const { promise, ws } = await startVerifyAndGetWs(client);
|
|
385
|
-
// First send a qr_url to populate the div
|
|
386
|
-
ws.simulateMessage({ qr_url: 'https://app.polyguard.ai/qr?pcre=abc123' });
|
|
387
|
-
const qrDiv = document.querySelector('#polyguard-qr');
|
|
388
|
-
expect(qrDiv.innerHTML).toContain('mock-qr');
|
|
389
|
-
// Then status message should reset to spinner
|
|
390
|
-
ws.simulateMessage({ status: 'pending' });
|
|
391
|
-
expect(qrDiv.innerHTML).toContain('spinner-circle');
|
|
392
|
-
ws.simulateMessage({ jwt: 'cleanup' });
|
|
393
|
-
await promise.catch(() => {});
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
it('unknown message type resolves with error', async () => {
|
|
397
|
-
document.body.innerHTML = '<div id="test-target"></div>';
|
|
398
|
-
const c = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
|
|
399
|
-
const { promise, ws } = await startVerifyAndGetWs(c, 'test-target');
|
|
400
|
-
ws.simulateMessage({ unknown_field: true });
|
|
401
|
-
const result = await promise;
|
|
402
|
-
expect(result).toEqual({ presence: { score: 'OFFLINE', msg: 'Unknown message type from server' } });
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
it('qr_url extracts pcre param and sends pong', async () => {
|
|
406
|
-
const { promise, ws } = await startVerifyAndGetWs(client);
|
|
407
|
-
ws.simulateMessage({ qr_url: 'https://app.polyguard.ai/qr?pcre=xyz789&other=1' });
|
|
408
|
-
const pongMsg = ws.sent.find(s => {
|
|
409
|
-
const parsed = JSON.parse(s);
|
|
410
|
-
return parsed.type === 'pong' && parsed.seq === 'xyz789';
|
|
411
|
-
});
|
|
412
|
-
expect(pongMsg).toBeDefined();
|
|
413
|
-
ws.simulateMessage({ jwt: 'cleanup' });
|
|
414
|
-
await promise.catch(() => {});
|
|
415
|
-
});
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
// ---------- verify - modal vs target mode ----------
|
|
419
|
-
describe('verify - modal vs target mode', () => {
|
|
420
|
-
it('target mode renders into specified element', async () => {
|
|
421
|
-
document.body.innerHTML = '<div id="my-target"></div>';
|
|
422
|
-
const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
|
|
423
|
-
const { promise, ws } = await startVerifyAndGetWs(client, 'my-target');
|
|
424
|
-
const target = document.getElementById('my-target');
|
|
425
|
-
expect(target.innerHTML).toContain('spinner-circle');
|
|
426
|
-
ws.simulateMessage({ jwt: 'cleanup' });
|
|
427
|
-
await promise.catch(() => {});
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
it('target mode throws if element not found', async () => {
|
|
431
|
-
const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
|
|
432
|
-
await expect(client.verify('nonexistent-id')).rejects.toThrow("Target element with ID 'nonexistent-id' not found");
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
it('modal mode appends to document.body', async () => {
|
|
436
|
-
const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
|
|
437
|
-
const { promise, ws } = await startVerifyAndGetWs(client);
|
|
438
|
-
expect(document.querySelector('#polyguard-modal-content')).not.toBeNull();
|
|
439
|
-
ws.simulateMessage({ jwt: 'cleanup' });
|
|
440
|
-
await promise.catch(() => {});
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
it('close button rejects with User cancelled', async () => {
|
|
444
|
-
const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
|
|
445
|
-
const { promise } = await startVerifyAndGetWs(client);
|
|
446
|
-
document.querySelector('#polyguard-modal-close').click();
|
|
447
|
-
await expect(promise).rejects.toThrow('User cancelled');
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
it('cancel button rejects with User cancelled', async () => {
|
|
451
|
-
const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
|
|
452
|
-
const { promise } = await startVerifyAndGetWs(client);
|
|
453
|
-
document.querySelector('#polyguard-modal-cancel').click();
|
|
454
|
-
await expect(promise).rejects.toThrow('User cancelled');
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
it('jwt message cleans up modal from DOM', async () => {
|
|
458
|
-
const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
|
|
459
|
-
const { promise, ws } = await startVerifyAndGetWs(client);
|
|
460
|
-
expect(document.querySelector('#polyguard-modal-content')).not.toBeNull();
|
|
461
|
-
ws.simulateMessage({ jwt: buildFakeJwt({}) });
|
|
462
|
-
await promise;
|
|
463
|
-
expect(document.querySelector('#polyguard-modal-content')).toBeNull();
|
|
464
|
-
});
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
// ---------- verify - mobile detection ----------
|
|
468
|
-
describe('verify - mobile detection', () => {
|
|
469
|
-
it('mobile UA renders button instead of QR code', async () => {
|
|
470
|
-
setUserAgent('Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148');
|
|
471
|
-
const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
|
|
472
|
-
const { promise, ws } = await startVerifyAndGetWs(client);
|
|
473
|
-
ws.simulateMessage({ qr_url: 'https://app.polyguard.ai/qr?pcre=abc' });
|
|
474
|
-
const btn = document.querySelector('#polyguard-open-app-button');
|
|
475
|
-
expect(btn).not.toBeNull();
|
|
476
|
-
expect(btn.textContent).toBe('Open Polyguard App');
|
|
477
|
-
ws.simulateMessage({ jwt: 'cleanup' });
|
|
478
|
-
await promise.catch(() => {});
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
it('desktop UA calls QRCode.toString', async () => {
|
|
482
|
-
setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)');
|
|
483
|
-
const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
|
|
484
|
-
const { promise, ws } = await startVerifyAndGetWs(client);
|
|
485
|
-
ws.simulateMessage({ qr_url: 'https://app.polyguard.ai/qr?pcre=abc' });
|
|
486
|
-
expect(mockQRCode.toString).toHaveBeenCalledWith(
|
|
487
|
-
'https://app.polyguard.ai/qr?pcre=abc',
|
|
488
|
-
{ type: 'svg' },
|
|
489
|
-
expect.any(Function),
|
|
490
|
-
);
|
|
491
|
-
ws.simulateMessage({ jwt: 'cleanup' });
|
|
492
|
-
await promise.catch(() => {});
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
it('mobile button click calls window.location.assign with qr_url', async () => {
|
|
496
|
-
setUserAgent('Mozilla/5.0 (Linux; Android 12) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100 Mobile');
|
|
497
|
-
const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
|
|
498
|
-
const { promise, ws } = await startVerifyAndGetWs(client);
|
|
499
|
-
ws.simulateMessage({ qr_url: 'https://app.polyguard.ai/qr?pcre=abc' });
|
|
500
|
-
const btn = document.querySelector('#polyguard-open-app-button');
|
|
501
|
-
btn.click();
|
|
502
|
-
expect(window.location.assign).toHaveBeenCalledWith('https://app.polyguard.ai/qr?pcre=abc');
|
|
503
|
-
ws.simulateMessage({ jwt: 'cleanup' });
|
|
504
|
-
await promise.catch(() => {});
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
it('Android UA also detected as mobile', async () => {
|
|
508
|
-
setUserAgent('Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36');
|
|
509
|
-
const client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
|
|
510
|
-
const { promise, ws } = await startVerifyAndGetWs(client);
|
|
511
|
-
ws.simulateMessage({ qr_url: 'https://app.polyguard.ai/qr?pcre=abc' });
|
|
512
|
-
expect(document.querySelector('#polyguard-open-app-button')).not.toBeNull();
|
|
513
|
-
ws.simulateMessage({ jwt: 'cleanup' });
|
|
514
|
-
await promise.catch(() => {});
|
|
515
|
-
});
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
// ---------- require ----------
|
|
519
|
-
describe('require', () => {
|
|
520
|
-
let client;
|
|
521
|
-
|
|
522
|
-
beforeEach(() => {
|
|
523
|
-
client = new PolyguardWebsocketClientImpl(DEFAULT_PARAMS);
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
it('returns true when all proofs match JWT payload', async () => {
|
|
527
|
-
const jwt = buildFakeJwt({ 'Full Name': 'Alice Smith', 'Email': 'alice@example.com' });
|
|
528
|
-
vi.spyOn(client, 'verify').mockResolvedValue(jwt);
|
|
529
|
-
const result = await client.require({ 'Full Name': 'Alice Smith', 'Email': 'alice@example.com' });
|
|
530
|
-
expect(result).toBe(true);
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
it('returns false on proof value mismatch', async () => {
|
|
534
|
-
const jwt = buildFakeJwt({ 'Full Name': 'Alice Smith' });
|
|
535
|
-
vi.spyOn(client, 'verify').mockResolvedValue(jwt);
|
|
536
|
-
const result = await client.require({ 'Full Name': 'Bob Jones' });
|
|
537
|
-
expect(result).toBe(false);
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
it('returns false when expected key is missing from JWT', async () => {
|
|
541
|
-
const jwt = buildFakeJwt({ 'Full Name': 'Alice' });
|
|
542
|
-
vi.spyOn(client, 'verify').mockResolvedValue(jwt);
|
|
543
|
-
const result = await client.require({ 'Email': 'alice@example.com' });
|
|
544
|
-
expect(result).toBe(false);
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
it('returns false when verify returns falsy JWT', async () => {
|
|
548
|
-
vi.spyOn(client, 'verify').mockResolvedValue(null);
|
|
549
|
-
const result = await client.require({ 'Full Name': 'Alice' });
|
|
550
|
-
expect(result).toBe(false);
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
it('returns false when verify rejects (cancellation)', async () => {
|
|
554
|
-
vi.spyOn(client, 'verify').mockRejectedValue(new Error('User cancelled'));
|
|
555
|
-
const result = await client.require({ 'Full Name': 'Alice' });
|
|
556
|
-
expect(result).toBe(false);
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
it('correctly decodes base64 JWT payload', async () => {
|
|
560
|
-
const payload = { 'Full Name': 'Test User', score: 99 };
|
|
561
|
-
const jwt = buildFakeJwt(payload);
|
|
562
|
-
vi.spyOn(client, 'verify').mockResolvedValue(jwt);
|
|
563
|
-
const result = await client.require({ 'Full Name': 'Test User', score: 99 });
|
|
564
|
-
expect(result).toBe(true);
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
it('passes target through to verify', async () => {
|
|
568
|
-
const jwt = buildFakeJwt({ 'Full Name': 'Alice' });
|
|
569
|
-
const spy = vi.spyOn(client, 'verify').mockResolvedValue(jwt);
|
|
570
|
-
await client.require({ 'Full Name': 'Alice' }, 'my-element');
|
|
571
|
-
expect(spy).toHaveBeenCalledWith('my-element');
|
|
572
|
-
});
|
|
573
|
-
});
|
|
574
|
-
});
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Build a fake JWT with the given payload.
|
|
3
|
-
* Structure: header.payload.signature (base64url encoded)
|
|
4
|
-
*/
|
|
5
|
-
export function buildFakeJwt(payload = {}) {
|
|
6
|
-
const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
|
|
7
|
-
const body = btoa(JSON.stringify(payload));
|
|
8
|
-
const sig = btoa('fakesignature');
|
|
9
|
-
return `${header}.${body}.${sig}`;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export const DEFAULT_PARAMS = {
|
|
13
|
-
appId: 'test-app-id',
|
|
14
|
-
apiServer: 'api.test.polyguard.ai',
|
|
15
|
-
requiredProofs: ['Full Name', 'Email'],
|
|
16
|
-
scanType: 'single',
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Create a mock Response for the ticket fetch.
|
|
21
|
-
*/
|
|
22
|
-
export function mockTicketResponse(ticket = 'test-ticket-123', ok = true) {
|
|
23
|
-
return {
|
|
24
|
-
ok,
|
|
25
|
-
json: async () => (ticket ? { ticket } : {}),
|
|
26
|
-
};
|
|
27
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mock for reconnecting-websocket.
|
|
3
|
-
* Captures the URL/protocols/options passed to the constructor and
|
|
4
|
-
* exposes helpers to drive the message handler under test.
|
|
5
|
-
*/
|
|
6
|
-
export class MockReconnectingWebSocket {
|
|
7
|
-
static instances = [];
|
|
8
|
-
|
|
9
|
-
constructor(url, protocols, options) {
|
|
10
|
-
this.url = url;
|
|
11
|
-
this.protocols = protocols;
|
|
12
|
-
this.options = options;
|
|
13
|
-
this._listeners = {};
|
|
14
|
-
this.readyState = 1; // OPEN
|
|
15
|
-
this.sent = [];
|
|
16
|
-
MockReconnectingWebSocket.instances.push(this);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
addEventListener(type, handler) {
|
|
20
|
-
if (!this._listeners[type]) this._listeners[type] = [];
|
|
21
|
-
this._listeners[type].push(handler);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
removeEventListener(type, handler) {
|
|
25
|
-
if (!this._listeners[type]) return;
|
|
26
|
-
this._listeners[type] = this._listeners[type].filter(h => h !== handler);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
send(data) {
|
|
30
|
-
this.sent.push(data);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
close() {
|
|
34
|
-
this.readyState = 3; // CLOSED
|
|
35
|
-
this._emit('close', {});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// --- Test helpers ---
|
|
39
|
-
|
|
40
|
-
simulateMessage(data) {
|
|
41
|
-
this._emit('message', { data: JSON.stringify(data) });
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
simulateError() {
|
|
45
|
-
this._emit('error', {});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
simulateClose() {
|
|
49
|
-
this._emit('close', {});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
_emit(type, event) {
|
|
53
|
-
const handlers = this._listeners[type] || [];
|
|
54
|
-
for (const h of handlers) {
|
|
55
|
-
h(event);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
static reset() {
|
|
60
|
-
MockReconnectingWebSocket.instances = [];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
static latest() {
|
|
64
|
-
return MockReconnectingWebSocket.instances[MockReconnectingWebSocket.instances.length - 1];
|
|
65
|
-
}
|
|
66
|
-
}
|