@qelos/sdk 3.8.1 → 3.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/administrator/tests/roles.test.d.ts +1 -0
- package/dist/administrator/tests/roles.test.js +32 -0
- package/dist/administrator/tests/roles.test.js.map +1 -0
- package/dist/base-sdk.d.ts +3 -0
- package/dist/base-sdk.js +67 -24
- package/dist/base-sdk.js.map +1 -1
- package/package.json +3 -3
- package/src/base-sdk.ts +79 -23
- package/test/base-sdk-exceptions.test.ts +197 -0
- package/test/base-sdk.test.ts +80 -0
- package/test/index.test.ts +85 -0
- package/test/qelos-sdk-edge-cases.test.ts +301 -0
- package/test/qelos-sdk-refresh.test.ts +412 -0
- package/tsconfig.json +3 -2
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import * as assert from 'node:assert/strict';
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
import QelosSDK from '../src/index';
|
|
4
|
+
import { QelosSDKOptions } from '../src/types';
|
|
5
|
+
|
|
6
|
+
test('QelosSDK', async (t) => {
|
|
7
|
+
let options: QelosSDKOptions;
|
|
8
|
+
let sdk: QelosSDK;
|
|
9
|
+
|
|
10
|
+
// Setup before each test
|
|
11
|
+
const setup = () => {
|
|
12
|
+
// Initialize SDK with mocked options
|
|
13
|
+
options = {
|
|
14
|
+
appUrl: 'http://localhost:3000',
|
|
15
|
+
fetch: async () => {
|
|
16
|
+
const res = new Response(JSON.stringify({ success: true }));
|
|
17
|
+
res.headers.set('Content-Type', 'application/json');
|
|
18
|
+
return res;
|
|
19
|
+
},
|
|
20
|
+
refreshToken: 'mock-refresh-token'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
sdk = new QelosSDK(options);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
await t.test('should initialize QelosSDK with all required properties', () => {
|
|
27
|
+
setup();
|
|
28
|
+
assert.ok(sdk);
|
|
29
|
+
assert.ok(sdk.blocks);
|
|
30
|
+
assert.ok(sdk.layouts);
|
|
31
|
+
assert.ok(sdk.appConfigurations);
|
|
32
|
+
assert.ok(sdk.authentication);
|
|
33
|
+
assert.ok(sdk.workspaces);
|
|
34
|
+
assert.ok(sdk.invites);
|
|
35
|
+
assert.ok(sdk.blueprints);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await t.test('should set and remove custom headers', async () => {
|
|
39
|
+
setup();
|
|
40
|
+
// Mock the authentication.accessToken to prevent token-related errors
|
|
41
|
+
Object.defineProperty(sdk.authentication, 'accessToken', {
|
|
42
|
+
get: () => 'mock-access-token'
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Set a custom header
|
|
46
|
+
sdk.setCustomHeader('x-test-header', 'test-value');
|
|
47
|
+
|
|
48
|
+
// Test that the header is included in extraHeaders
|
|
49
|
+
const headersWithCustom = await options.extraHeaders('/api/test');
|
|
50
|
+
assert.equal(headersWithCustom['x-test-header'], 'test-value');
|
|
51
|
+
|
|
52
|
+
// Remove the custom header
|
|
53
|
+
sdk.removeCustomHeader('x-test-header');
|
|
54
|
+
|
|
55
|
+
// Test that the header is no longer included
|
|
56
|
+
const headersWithoutCustom = await options.extraHeaders('/api/test');
|
|
57
|
+
assert.equal(headersWithoutCustom['x-test-header'], undefined);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
await t.test('should not include authorization header for specific URLs', async () => {
|
|
61
|
+
setup();
|
|
62
|
+
// Test URLs that should not have authorization headers
|
|
63
|
+
const noAuthUrls = [
|
|
64
|
+
'/api/token/refresh',
|
|
65
|
+
'/api/signin',
|
|
66
|
+
'/api/signup'
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
for (const url of noAuthUrls) {
|
|
70
|
+
const headers = await options.extraHeaders(url);
|
|
71
|
+
assert.equal(headers.authorization, undefined);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await t.test('should include authorization header for regular URLs', async () => {
|
|
76
|
+
setup();
|
|
77
|
+
// Mock the authentication.accessToken
|
|
78
|
+
Object.defineProperty(sdk.authentication, 'accessToken', {
|
|
79
|
+
get: () => 'mock-access-token'
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const headers = await options.extraHeaders('/api/regular-endpoint');
|
|
83
|
+
assert.equal(headers.authorization, 'Bearer mock-access-token');
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import * as assert from 'node:assert';
|
|
3
|
+
import QelosSDK from '../src';
|
|
4
|
+
import { QelosSDKOptions } from '../src/types';
|
|
5
|
+
import { FetchLike } from '../src/types';
|
|
6
|
+
|
|
7
|
+
type ApiCall = {
|
|
8
|
+
url: string;
|
|
9
|
+
init?: RequestInit;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const apiCalls: ApiCall[] = [];
|
|
13
|
+
|
|
14
|
+
// Create a simplified mock Response that works with our SDK
|
|
15
|
+
function createMockResponse(status: number, body: any, headers: Record<string, string> = {}): Response {
|
|
16
|
+
const defaultHeaders = {
|
|
17
|
+
'Content-Type': 'application/json',
|
|
18
|
+
...headers
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Create a partial Response object with just the properties we need
|
|
22
|
+
const mockResponse = {
|
|
23
|
+
status,
|
|
24
|
+
ok: status >= 200 && status < 300,
|
|
25
|
+
headers: {
|
|
26
|
+
get: (name: string) => defaultHeaders[name.toLowerCase()] || null
|
|
27
|
+
},
|
|
28
|
+
json: async () => body,
|
|
29
|
+
text: async () => JSON.stringify(body),
|
|
30
|
+
redirected: false,
|
|
31
|
+
statusText: status === 200 ? 'OK' : 'Error',
|
|
32
|
+
type: 'basic' as ResponseType,
|
|
33
|
+
url: '',
|
|
34
|
+
bodyUsed: false
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Cast to Response type - this is safe for our tests since we only use these properties
|
|
38
|
+
return mockResponse as unknown as Response;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function setup(options: {
|
|
42
|
+
networkFailure?: boolean;
|
|
43
|
+
malformedResponse?: boolean;
|
|
44
|
+
concurrentRequests?: boolean;
|
|
45
|
+
nonJsonResponse?: boolean;
|
|
46
|
+
raceCondition?: boolean;
|
|
47
|
+
retryTransient?: boolean;
|
|
48
|
+
statusCode?: number;
|
|
49
|
+
} = {}) {
|
|
50
|
+
apiCalls.length = 0;
|
|
51
|
+
let tokenRefreshInProgress = false;
|
|
52
|
+
let tokenRefreshCompleted = false;
|
|
53
|
+
let requestCount = 0;
|
|
54
|
+
|
|
55
|
+
const mockFetch: FetchLike = async (url: string, init?: RequestInit) => {
|
|
56
|
+
apiCalls.push({ url: url.toString(), init });
|
|
57
|
+
|
|
58
|
+
// Simulate network failure
|
|
59
|
+
if (options.networkFailure) {
|
|
60
|
+
throw new Error('Network error');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Simulate race condition during token refresh
|
|
64
|
+
if (options.raceCondition && url.toString().includes('/api/test-endpoint')) {
|
|
65
|
+
if (tokenRefreshInProgress && !tokenRefreshCompleted) {
|
|
66
|
+
// This simulates a request that happens during token refresh
|
|
67
|
+
return createMockResponse(401, { error: 'Unauthorized during refresh' });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (url.toString().includes('/api/token/refresh')) {
|
|
72
|
+
tokenRefreshInProgress = true;
|
|
73
|
+
|
|
74
|
+
// Simulate a delay in token refresh
|
|
75
|
+
if (options.raceCondition) {
|
|
76
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
tokenRefreshCompleted = true;
|
|
80
|
+
|
|
81
|
+
return createMockResponse(200, {
|
|
82
|
+
payload: {
|
|
83
|
+
token: 'new-access-token',
|
|
84
|
+
refreshToken: 'new-refresh-token',
|
|
85
|
+
user: { _id: 'user123', username: 'testuser' }
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (url.toString().includes('/api/test-endpoint')) {
|
|
91
|
+
requestCount++;
|
|
92
|
+
|
|
93
|
+
// For testing retrying transient errors
|
|
94
|
+
if (options.retryTransient && requestCount === 1) {
|
|
95
|
+
return createMockResponse(503, { error: 'Service Unavailable' });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// For testing specific status codes
|
|
99
|
+
if (options.statusCode) {
|
|
100
|
+
return createMockResponse(options.statusCode, { error: `Status code ${options.statusCode}` });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// For the first request, return 401 to trigger token refresh
|
|
104
|
+
if (requestCount === 1) {
|
|
105
|
+
return createMockResponse(401, { error: 'Unauthorized' });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// For malformed response test
|
|
109
|
+
if (options.malformedResponse) {
|
|
110
|
+
const headers = options.nonJsonResponse
|
|
111
|
+
? { 'Content-Type': 'text/plain' }
|
|
112
|
+
: { 'Content-Type': 'application/json' };
|
|
113
|
+
|
|
114
|
+
// Create a malformed response that will cause JSON parsing to fail
|
|
115
|
+
const malformedResponse = createMockResponse(200, {});
|
|
116
|
+
// Override the json method to throw an error
|
|
117
|
+
Object.defineProperty(malformedResponse, 'json', {
|
|
118
|
+
value: async () => { throw new Error('Invalid JSON'); }
|
|
119
|
+
});
|
|
120
|
+
// Override the text method to return invalid JSON
|
|
121
|
+
Object.defineProperty(malformedResponse, 'text', {
|
|
122
|
+
value: async () => options.nonJsonResponse ? 'Not a JSON response' : '{malformed json'
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return malformedResponse;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Normal successful response
|
|
129
|
+
return createMockResponse(200, { success: true, data: 'test data' });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return createMockResponse(404, { error: 'Not found' });
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const sdkOptions: QelosSDKOptions = {
|
|
136
|
+
appUrl: 'http://localhost:3000',
|
|
137
|
+
fetch: mockFetch,
|
|
138
|
+
refreshToken: 'initial-refresh-token',
|
|
139
|
+
accessToken: 'initial-access-token',
|
|
140
|
+
forceRefresh: true
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const sdk = new QelosSDK(sdkOptions);
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
sdk,
|
|
147
|
+
sdkOptions,
|
|
148
|
+
requestCount
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
test('QelosSDK edge cases', async (t) => {
|
|
153
|
+
await t.test('should handle network failures gracefully', async () => {
|
|
154
|
+
const { sdk } = setup({ networkFailure: true });
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
await sdk.appConfigurations.callJsonApi('/api/test-endpoint');
|
|
158
|
+
assert.fail('Should have thrown a network error');
|
|
159
|
+
} catch (error) {
|
|
160
|
+
assert.ok(error instanceof Error);
|
|
161
|
+
assert.ok(
|
|
162
|
+
error.message.includes('Network error'),
|
|
163
|
+
`Error should be network related, got: ${error.message}`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
await t.test('should handle malformed JSON responses', async () => {
|
|
169
|
+
const { sdk } = setup({ malformedResponse: true });
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
await sdk.appConfigurations.callJsonApi('/api/test-endpoint');
|
|
173
|
+
assert.fail('Should have thrown an error for malformed JSON');
|
|
174
|
+
} catch (error) {
|
|
175
|
+
assert.ok(error instanceof Error);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
await t.test('should handle non-JSON responses correctly', async () => {
|
|
180
|
+
const { sdk } = setup({ nonJsonResponse: true, malformedResponse: true });
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
await sdk.appConfigurations.callJsonApi('/api/test-endpoint');
|
|
184
|
+
assert.fail('Should have thrown an error for non-JSON response');
|
|
185
|
+
} catch (error) {
|
|
186
|
+
assert.ok(error instanceof Error);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
await t.test('should handle various HTTP status codes appropriately', async () => {
|
|
191
|
+
// Test a range of status codes - we'll test these individually
|
|
192
|
+
const statusCodes = [400, 403, 404, 500, 502, 503];
|
|
193
|
+
|
|
194
|
+
for (const statusCode of statusCodes) {
|
|
195
|
+
// For each status code, we need a fresh setup
|
|
196
|
+
apiCalls.length = 0;
|
|
197
|
+
|
|
198
|
+
// Create a custom mock fetch that directly returns the status code we want to test
|
|
199
|
+
// This bypasses the token refresh logic which complicates testing
|
|
200
|
+
const mockFetch: FetchLike = async (url: string, init?: RequestInit) => {
|
|
201
|
+
apiCalls.push({ url: url.toString(), init });
|
|
202
|
+
return createMockResponse(statusCode, { error: `Error with status code ${statusCode}` });
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const options: QelosSDKOptions = {
|
|
206
|
+
appUrl: 'http://localhost:3000',
|
|
207
|
+
fetch: mockFetch,
|
|
208
|
+
refreshToken: 'test-refresh-token',
|
|
209
|
+
accessToken: 'test-access-token',
|
|
210
|
+
// Disable token refresh for this test to focus on status code handling
|
|
211
|
+
forceRefresh: false
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const sdk = new QelosSDK(options);
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
await sdk.appConfigurations.callJsonApi('/api/test-endpoint');
|
|
218
|
+
assert.fail(`Should have thrown an error for status code ${statusCode}`);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
assert.ok(error instanceof Error, `Error should be an instance of Error for status ${statusCode}`);
|
|
221
|
+
// The SDK might wrap the error, so we just verify we got an error
|
|
222
|
+
assert.ok(error.message, `Error should have a message for status ${statusCode}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await t.test('should handle URL encoding in query parameters', async () => {
|
|
228
|
+
const { sdk } = setup();
|
|
229
|
+
|
|
230
|
+
// Create complex query parameters with special characters
|
|
231
|
+
const complexQuery = {
|
|
232
|
+
'complex key': 'value with spaces',
|
|
233
|
+
'special&chars': 'value+with&special=chars',
|
|
234
|
+
'array': ['value1', 'value2'],
|
|
235
|
+
'unicode': 'üñîçødé',
|
|
236
|
+
'empty': '',
|
|
237
|
+
'null': null
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// Reset API calls
|
|
241
|
+
apiCalls.length = 0;
|
|
242
|
+
|
|
243
|
+
// Make a request with complex query parameters
|
|
244
|
+
try {
|
|
245
|
+
// Since RequestInit doesn't have a query property, we need to manually construct the URL
|
|
246
|
+
// Convert the complex query object to a format URLSearchParams can handle
|
|
247
|
+
const queryParams: Record<string, string> = {};
|
|
248
|
+
Object.entries(complexQuery).forEach(([key, value]) => {
|
|
249
|
+
if (Array.isArray(value)) {
|
|
250
|
+
// Convert arrays to comma-separated strings
|
|
251
|
+
queryParams[key] = value.join(',');
|
|
252
|
+
} else if (value === null) {
|
|
253
|
+
queryParams[key] = '';
|
|
254
|
+
} else {
|
|
255
|
+
queryParams[key] = String(value);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
const queryString = new URLSearchParams(queryParams).toString();
|
|
259
|
+
await sdk.appConfigurations.callJsonApi(`/api/test-endpoint?${queryString}`, {
|
|
260
|
+
method: 'GET'
|
|
261
|
+
});
|
|
262
|
+
} catch (error) {
|
|
263
|
+
// We expect this to fail with 401 and then retry, but we're just checking the URL encoding
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Check that the URL was properly encoded
|
|
267
|
+
const firstCall = apiCalls[0];
|
|
268
|
+
assert.ok(firstCall.url.includes('/api/test-endpoint'), 'URL should include the endpoint');
|
|
269
|
+
|
|
270
|
+
// Verify that special characters are properly encoded
|
|
271
|
+
assert.ok(firstCall.url.includes('complex+key='), 'Should encode spaces in keys');
|
|
272
|
+
assert.ok(firstCall.url.includes('value+with+spaces'), 'Should encode spaces in values');
|
|
273
|
+
assert.ok(firstCall.url.includes('special%26chars='), 'Should encode & in keys');
|
|
274
|
+
assert.ok(firstCall.url.includes('value%2Bwith%26special%3Dchars'), 'Should encode special chars in values');
|
|
275
|
+
assert.ok(firstCall.url.includes('%C3%BC%C3%B1%C3%AE%C3%A7%C3%B8d%C3%A9'), 'Should encode unicode characters');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
await t.test('should handle concurrent requests during token refresh', async () => {
|
|
279
|
+
const { sdk } = setup({ raceCondition: true });
|
|
280
|
+
|
|
281
|
+
// Reset API calls
|
|
282
|
+
apiCalls.length = 0;
|
|
283
|
+
|
|
284
|
+
// Make two concurrent requests
|
|
285
|
+
const request1 = sdk.appConfigurations.callJsonApi('/api/test-endpoint');
|
|
286
|
+
const request2 = sdk.appConfigurations.callJsonApi('/api/test-endpoint');
|
|
287
|
+
|
|
288
|
+
// Wait for both requests to complete
|
|
289
|
+
const results = await Promise.allSettled([request1, request2]);
|
|
290
|
+
|
|
291
|
+
// At least one request should succeed
|
|
292
|
+
assert.ok(
|
|
293
|
+
results.some(result => result.status === 'fulfilled'),
|
|
294
|
+
'At least one request should succeed'
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
// Check that token refresh was only called once
|
|
298
|
+
const refreshCalls = apiCalls.filter(call => call.url.includes('/api/token/refresh'));
|
|
299
|
+
assert.ok(refreshCalls.length >= 1, 'Token refresh should be called at least once');
|
|
300
|
+
});
|
|
301
|
+
});
|