@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 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as assert from 'node:assert/strict';
|
|
2
|
+
import { QlRoles } from '../roles';
|
|
3
|
+
const { describe, beforeEach, test } = require('node:test');
|
|
4
|
+
describe('QlRoles', () => {
|
|
5
|
+
let options;
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
// Initialize SDK with the mocked fetch function and mock authentication
|
|
8
|
+
options = {
|
|
9
|
+
appUrl: 'http://localhost:3000',
|
|
10
|
+
fetch: () => null,
|
|
11
|
+
extraHeaders: async () => ({}),
|
|
12
|
+
refreshToken: 'mock-refresh-token' // Ensure a mock refresh token is set
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
test('should instantiate the roles property', () => {
|
|
16
|
+
const rolesSdk = new QlRoles(options);
|
|
17
|
+
assert.ok(rolesSdk);
|
|
18
|
+
});
|
|
19
|
+
test('should fetch roles successfully', async () => {
|
|
20
|
+
// Mock fetch to return a valid JSON response
|
|
21
|
+
options.fetch = async () => {
|
|
22
|
+
const res = new Response(JSON.stringify(['admin', 'user', 'plugin']));
|
|
23
|
+
res.headers.set('Content-Type', 'application/json');
|
|
24
|
+
return res;
|
|
25
|
+
};
|
|
26
|
+
const rolesSdk = new QlRoles(options);
|
|
27
|
+
// Call the function and verify results
|
|
28
|
+
const roles = await rolesSdk.getExistingRoles();
|
|
29
|
+
assert.deepEqual(roles, ['admin', 'user', 'plugin']);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
//# sourceMappingURL=roles.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roles.test.js","sourceRoot":"","sources":["../../../src/administrator/tests/roles.test.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,oBAAoB,CAAC;AAE7C,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAEnC,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;AAE3D,QAAQ,CAAC,SAAS,EAAE,GAAG,EAAE;IACvB,IAAI,OAAwB,CAAA;IAE5B,UAAU,CAAC,GAAG,EAAE;QACd,wEAAwE;QACxE,OAAO,GAAG;YACR,MAAM,EAAE,uBAAuB;YAC/B,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI;YACjB,YAAY,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;YAC9B,YAAY,EAAE,oBAAoB,CAAC,qCAAqC;SACzE,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,uCAAuC,EAAE,GAAG,EAAE;QACjD,MAAM,QAAQ,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;QACtC,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC;IACtB,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,iCAAiC,EAAE,KAAK,IAAI,EAAE;QACjD,6CAA6C;QAC7C,OAAO,CAAC,KAAK,GAAG,KAAK,IAAI,EAAE;YACzB,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAA;YACrE,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;YACpD,OAAO,GAAG,CAAC;QACb,CAAC,CAAA;QAED,MAAM,QAAQ,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;QAEtC,uCAAuC;QACvC,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,gBAAgB,EAAE,CAAC;QAChD,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/dist/base-sdk.d.ts
CHANGED
|
@@ -4,6 +4,9 @@ export default class BaseSDK {
|
|
|
4
4
|
private qlOptions;
|
|
5
5
|
constructor(qlOptions: QelosSDKOptions);
|
|
6
6
|
callApi(relativeUrl: string, data?: RequestInit): Promise<Response>;
|
|
7
|
+
private handleFailedRefreshToken;
|
|
8
|
+
private getContentType;
|
|
9
|
+
private parseResponse;
|
|
7
10
|
callJsonApi<T>(relativeUrl: string, data?: RequestInit): Promise<T>;
|
|
8
11
|
getQueryParams(moreQuery?: Record<string, any>): string;
|
|
9
12
|
}
|
package/dist/base-sdk.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
const ERRORS = {
|
|
2
|
+
EXTRA_HEADERS: 'could not get extra headers',
|
|
3
|
+
FAILED_REFRESH_TOKEN: 'could not handle failed refresh token',
|
|
4
|
+
UNABLE_TO_REFRESH_TOKEN: 'unable to refresh token'
|
|
5
|
+
};
|
|
1
6
|
export default class BaseSDK {
|
|
2
7
|
qlOptions;
|
|
3
8
|
#appUrl;
|
|
@@ -10,39 +15,77 @@ export default class BaseSDK {
|
|
|
10
15
|
async callApi(relativeUrl, data) {
|
|
11
16
|
data = data || {};
|
|
12
17
|
data.headers = data.headers || {};
|
|
13
|
-
|
|
18
|
+
if (this.qlOptions.extraHeaders) {
|
|
19
|
+
try {
|
|
20
|
+
const extraHeaders = await this.qlOptions.extraHeaders(relativeUrl);
|
|
21
|
+
Object.assign(data.headers, extraHeaders);
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
throw new Error(ERRORS.EXTRA_HEADERS);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
14
27
|
return this.#fetch(this.#appUrl + relativeUrl, data);
|
|
15
28
|
}
|
|
16
|
-
|
|
17
|
-
|
|
29
|
+
async handleFailedRefreshToken() {
|
|
30
|
+
if (this.qlOptions.onFailedRefreshToken) {
|
|
31
|
+
try {
|
|
32
|
+
await this.qlOptions.onFailedRefreshToken();
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
throw new Error(ERRORS.FAILED_REFRESH_TOKEN);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
throw new Error(ERRORS.UNABLE_TO_REFRESH_TOKEN);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
getContentType(res) {
|
|
43
|
+
return res.headers.get('Content-Type') ||
|
|
44
|
+
res.headers.get('content-type') ||
|
|
45
|
+
res.headers.get('ContentType') ||
|
|
46
|
+
res.headers.get('contenttype') ||
|
|
47
|
+
res.headers.get('contentType') || 'text';
|
|
48
|
+
}
|
|
49
|
+
async parseResponse(res) {
|
|
50
|
+
const contentType = this.getContentType(res);
|
|
51
|
+
const isJson = contentType.includes('json');
|
|
52
|
+
const body = await (isJson ? res.json() : res.text());
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
throw (typeof body === 'string' ? new Error(body) : body);
|
|
55
|
+
}
|
|
56
|
+
return body;
|
|
57
|
+
}
|
|
58
|
+
async callJsonApi(relativeUrl, data) {
|
|
59
|
+
try {
|
|
60
|
+
let res = await this.callApi(relativeUrl, data);
|
|
61
|
+
// Handle token refresh if needed
|
|
18
62
|
if (this.qlOptions.forceRefresh && res.status >= 400 && res.status < 500) {
|
|
19
|
-
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
await this.qlOptions.onFailedRefreshToken();
|
|
24
|
-
}
|
|
25
|
-
catch (e) {
|
|
26
|
-
throw new Error('could not handle failed refresh token');
|
|
27
|
-
}
|
|
63
|
+
let headers = {};
|
|
64
|
+
if (this.qlOptions.extraHeaders) {
|
|
65
|
+
try {
|
|
66
|
+
headers = await this.qlOptions.extraHeaders(relativeUrl, true);
|
|
28
67
|
}
|
|
29
|
-
|
|
30
|
-
|
|
68
|
+
catch {
|
|
69
|
+
// Ignore error during refresh attempt
|
|
31
70
|
}
|
|
32
71
|
}
|
|
72
|
+
if (!headers?.authorization) {
|
|
73
|
+
await this.handleFailedRefreshToken();
|
|
74
|
+
}
|
|
33
75
|
res = await this.callApi(relativeUrl, data);
|
|
34
76
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
77
|
+
return this.parseResponse(res);
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
// Type guard for Error objects
|
|
81
|
+
if (err instanceof Error && err.message === ERRORS.EXTRA_HEADERS) {
|
|
82
|
+
// Handle token refresh for EXTRA_HEADERS_ERROR
|
|
83
|
+
await this.handleFailedRefreshToken();
|
|
84
|
+
const res = await this.callApi(relativeUrl, data);
|
|
85
|
+
return this.parseResponse(res);
|
|
43
86
|
}
|
|
44
|
-
|
|
45
|
-
}
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
46
89
|
}
|
|
47
90
|
getQueryParams(moreQuery) {
|
|
48
91
|
const qs = this.qlOptions.extraQueryParams ? { ...this.qlOptions.extraQueryParams(), ...(moreQuery || {}) } : moreQuery;
|
package/dist/base-sdk.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base-sdk.js","sourceRoot":"","sources":["../src/base-sdk.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,OAAO,OAAO,OAAO;IAIN;IAHpB,OAAO,CAAS;IAChB,MAAM,CAAY;IAElB,YAAoB,SAA0B;QAA1B,cAAS,GAAT,SAAS,CAAiB;QAC5C,IAAI,CAAC,OAAO,GAAG,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC;QACjG,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC;IACpD,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,WAAmB,EAAE,IAAkB;QACnD,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC;QAClB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"base-sdk.js","sourceRoot":"","sources":["../src/base-sdk.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,GAAG;IACb,aAAa,EAAE,6BAA6B;IAC5C,oBAAoB,EAAE,uCAAuC;IAC7D,uBAAuB,EAAE,yBAAyB;CACnD,CAAA;AAED,MAAM,CAAC,OAAO,OAAO,OAAO;IAIN;IAHpB,OAAO,CAAS;IAChB,MAAM,CAAY;IAElB,YAAoB,SAA0B;QAA1B,cAAS,GAAT,SAAS,CAAiB;QAC5C,IAAI,CAAC,OAAO,GAAG,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC;QACjG,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC;IACpD,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,WAAmB,EAAE,IAAkB;QACnD,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC;QAClB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC;QAElC,IAAI,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE;YAC/B,IAAI;gBACF,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,WAAW,CAAC,CAAC;gBACpE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;aAC3C;YAAC,OAAO,CAAC,EAAE;gBACV,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;aACvC;SACF;QAED,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,GAAG,WAAW,EAAE,IAAI,CAAC,CAAC;IACvD,CAAC;IAEO,KAAK,CAAC,wBAAwB;QACpC,IAAI,IAAI,CAAC,SAAS,CAAC,oBAAoB,EAAE;YACvC,IAAI;gBACF,MAAM,IAAI,CAAC,SAAS,CAAC,oBAAoB,EAAE,CAAC;aAC7C;YAAC,OAAO,CAAC,EAAE;gBACV,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;aAC9C;SACF;aAAM;YACL,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,uBAAuB,CAAC,CAAC;SACjD;IACH,CAAC;IAEO,cAAc,CAAC,GAAa;QAClC,OAAO,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;YACpC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;YAC/B,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;YAC9B,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC;YAC9B,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,IAAI,MAAM,CAAC;IAC7C,CAAC;IAEO,KAAK,CAAC,aAAa,CAAI,GAAa;QAC1C,MAAM,WAAW,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC;QAC7C,MAAM,MAAM,GAAG,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAE5C,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;QAEtD,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE;YACX,MAAM,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;SAC3D;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,WAAW,CAAI,WAAmB,EAAE,IAAkB;QAC1D,IAAI;YACF,IAAI,GAAG,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;YAEhD,iCAAiC;YACjC,IAAI,IAAI,CAAC,SAAS,CAAC,YAAY,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,MAAM,GAAG,GAAG,EAAE;gBACxE,IAAI,OAAO,GAA2B,EAAE,CAAC;gBAEzC,IAAI,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE;oBAC/B,IAAI;wBACF,OAAO,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;qBAChE;oBAAC,MAAM;wBACN,sCAAsC;qBACvC;iBACF;gBAED,IAAI,CAAC,OAAO,EAAE,aAAa,EAAE;oBAC3B,MAAM,IAAI,CAAC,wBAAwB,EAAE,CAAC;iBACvC;gBAED,GAAG,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;aAC7C;YAED,OAAO,IAAI,CAAC,aAAa,CAAI,GAAG,CAAC,CAAC;SACnC;QAAC,OAAO,GAAY,EAAE;YACrB,+BAA+B;YAC/B,IAAI,GAAG,YAAY,KAAK,IAAI,GAAG,CAAC,OAAO,KAAK,MAAM,CAAC,aAAa,EAAE;gBAChE,+CAA+C;gBAC/C,MAAM,IAAI,CAAC,wBAAwB,EAAE,CAAC;gBAEtC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC;gBAClD,OAAO,IAAI,CAAC,aAAa,CAAI,GAAG,CAAC,CAAC;aACnC;YAED,MAAM,GAAG,CAAC;SACX;IACH,CAAC;IAED,cAAc,CAAC,SAA+B;QAC5C,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,gBAAgB,EAAE,EAAE,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;QACxH,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,eAAe,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACjD,CAAC;CACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qelos/sdk",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.2",
|
|
4
4
|
"description": "SDK for Qelos API",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
"access": "public"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@qelos/global-types": "^3.8.
|
|
16
|
+
"@qelos/global-types": "^3.8.2"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"tsx": "^3.12.7",
|
|
20
20
|
"typescript": "^4.9.5"
|
|
21
21
|
},
|
|
22
|
-
"gitHead": "
|
|
22
|
+
"gitHead": "dc75ba76bdb68c07b640d4469d9dffecf61759b1"
|
|
23
23
|
}
|
package/src/base-sdk.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { FetchLike, QelosSDKOptions } from './types';
|
|
2
2
|
|
|
3
|
+
const ERRORS = {
|
|
4
|
+
EXTRA_HEADERS: 'could not get extra headers',
|
|
5
|
+
FAILED_REFRESH_TOKEN: 'could not handle failed refresh token',
|
|
6
|
+
UNABLE_TO_REFRESH_TOKEN: 'could not able to refresh token'
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
export default class BaseSDK {
|
|
4
10
|
#appUrl: string;
|
|
5
11
|
#fetch: FetchLike;
|
|
@@ -12,38 +18,88 @@ export default class BaseSDK {
|
|
|
12
18
|
async callApi(relativeUrl: string, data?: RequestInit) {
|
|
13
19
|
data = data || {};
|
|
14
20
|
data.headers = data.headers || {};
|
|
15
|
-
|
|
21
|
+
|
|
22
|
+
if (this.qlOptions.extraHeaders) {
|
|
23
|
+
try {
|
|
24
|
+
const extraHeaders = await this.qlOptions.extraHeaders(relativeUrl);
|
|
25
|
+
Object.assign(data.headers, extraHeaders);
|
|
26
|
+
} catch (e) {
|
|
27
|
+
throw new Error(ERRORS.EXTRA_HEADERS);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
16
31
|
return this.#fetch(this.#appUrl + relativeUrl, data);
|
|
17
32
|
}
|
|
18
33
|
|
|
19
|
-
|
|
20
|
-
|
|
34
|
+
private async handleFailedRefreshToken(): Promise<void> {
|
|
35
|
+
if (this.qlOptions.onFailedRefreshToken) {
|
|
36
|
+
try {
|
|
37
|
+
await this.qlOptions.onFailedRefreshToken();
|
|
38
|
+
} catch (e) {
|
|
39
|
+
throw new Error(ERRORS.FAILED_REFRESH_TOKEN);
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
throw new Error(ERRORS.UNABLE_TO_REFRESH_TOKEN);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private getContentType(res: Response): string {
|
|
47
|
+
return res.headers.get('Content-Type') ||
|
|
48
|
+
res.headers.get('content-type') ||
|
|
49
|
+
res.headers.get('ContentType') ||
|
|
50
|
+
res.headers.get('contenttype') ||
|
|
51
|
+
res.headers.get('contentType') || 'text';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private async parseResponse<T>(res: Response): Promise<T> {
|
|
55
|
+
const contentType = this.getContentType(res);
|
|
56
|
+
const isJson = contentType.includes('json');
|
|
57
|
+
|
|
58
|
+
const body = await (isJson ? res.json() : res.text());
|
|
59
|
+
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
throw (typeof body === 'string' ? new Error(body) : body);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return body;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async callJsonApi<T>(relativeUrl: string, data?: RequestInit): Promise<T> {
|
|
68
|
+
try {
|
|
69
|
+
let res = await this.callApi(relativeUrl, data);
|
|
70
|
+
|
|
71
|
+
// Handle token refresh if needed
|
|
21
72
|
if (this.qlOptions.forceRefresh && res.status >= 400 && res.status < 500) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
} else {
|
|
31
|
-
throw new Error('could not able to refresh token');
|
|
73
|
+
let headers: Record<string, string> = {};
|
|
74
|
+
|
|
75
|
+
if (this.qlOptions.extraHeaders) {
|
|
76
|
+
try {
|
|
77
|
+
headers = await this.qlOptions.extraHeaders(relativeUrl, true);
|
|
78
|
+
} catch {
|
|
79
|
+
// Ignore error during refresh attempt
|
|
32
80
|
}
|
|
33
81
|
}
|
|
82
|
+
|
|
83
|
+
if (!headers?.authorization) {
|
|
84
|
+
await this.handleFailedRefreshToken();
|
|
85
|
+
}
|
|
86
|
+
|
|
34
87
|
res = await this.callApi(relativeUrl, data);
|
|
35
88
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
89
|
+
|
|
90
|
+
return this.parseResponse<T>(res);
|
|
91
|
+
} catch (err: unknown) {
|
|
92
|
+
// Type guard for Error objects
|
|
93
|
+
if (err instanceof Error && err.message === ERRORS.EXTRA_HEADERS) {
|
|
94
|
+
// Handle token refresh for EXTRA_HEADERS_ERROR
|
|
95
|
+
await this.handleFailedRefreshToken();
|
|
96
|
+
|
|
97
|
+
const res = await this.callApi(relativeUrl, data);
|
|
98
|
+
return this.parseResponse<T>(res);
|
|
44
99
|
}
|
|
45
|
-
|
|
46
|
-
|
|
100
|
+
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
47
103
|
}
|
|
48
104
|
|
|
49
105
|
getQueryParams(moreQuery?: Record<string, any>) {
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import * as assert from 'node:assert/strict';
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
import BaseSDK from '../src/base-sdk';
|
|
4
|
+
import { QelosSDKOptions } from '../src/types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Tests for BaseSDK exception handling in callJsonApi method
|
|
8
|
+
*
|
|
9
|
+
* These tests verify that the BaseSDK correctly handles various error scenarios:
|
|
10
|
+
* - Errors during initial API call
|
|
11
|
+
* - Errors during response parsing
|
|
12
|
+
* - Errors during token refresh
|
|
13
|
+
* - Errors when calling onFailedRefreshToken
|
|
14
|
+
* - Non-JSON responses with error status
|
|
15
|
+
*/
|
|
16
|
+
test('BaseSDK exception handling', async (t) => {
|
|
17
|
+
// Mock response factory to simulate different responses
|
|
18
|
+
const createMockResponse = (status: number, data: any, contentType = 'application/json') => {
|
|
19
|
+
const body = typeof data === 'string' ? data : JSON.stringify(data);
|
|
20
|
+
const res = new Response(body);
|
|
21
|
+
Object.defineProperty(res, 'status', { value: status });
|
|
22
|
+
Object.defineProperty(res, 'ok', { value: status >= 200 && status < 300 });
|
|
23
|
+
res.headers.set('Content-Type', contentType);
|
|
24
|
+
return res;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Setup function to create BaseSDK instances with different configurations
|
|
28
|
+
const setup = (config: {
|
|
29
|
+
failInitialCall?: boolean;
|
|
30
|
+
failExtraHeaders?: boolean;
|
|
31
|
+
failRefreshToken?: boolean;
|
|
32
|
+
failOnFailedRefreshToken?: boolean;
|
|
33
|
+
invalidJsonResponse?: boolean;
|
|
34
|
+
nonJsonErrorResponse?: boolean;
|
|
35
|
+
forceRefresh?: boolean;
|
|
36
|
+
} = {}) => {
|
|
37
|
+
// Mock fetch function that returns different responses based on configuration
|
|
38
|
+
const mockFetch = async (input: RequestInfo, init?: RequestInit) => {
|
|
39
|
+
const url = input.toString();
|
|
40
|
+
// Simulate network failure for initial API call
|
|
41
|
+
if (config.failInitialCall) {
|
|
42
|
+
throw new Error('Network error');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Return different responses based on URL and configuration
|
|
46
|
+
if (url.includes('/api/test-endpoint')) {
|
|
47
|
+
if (config.invalidJsonResponse) {
|
|
48
|
+
return createMockResponse(200, '{invalid-json}', 'application/json');
|
|
49
|
+
}
|
|
50
|
+
if (config.nonJsonErrorResponse) {
|
|
51
|
+
return createMockResponse(400, 'Bad request error message', 'text/plain');
|
|
52
|
+
}
|
|
53
|
+
return createMockResponse(401, { error: 'Unauthorized' });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (url.includes('/api/token/refresh')) {
|
|
57
|
+
if (config.failRefreshToken) {
|
|
58
|
+
return createMockResponse(401, { error: 'Invalid refresh token' });
|
|
59
|
+
}
|
|
60
|
+
return createMockResponse(200, {
|
|
61
|
+
payload: {
|
|
62
|
+
token: 'new-access-token',
|
|
63
|
+
refreshToken: 'new-refresh-token',
|
|
64
|
+
user: { _id: 'user123', username: 'testuser' }
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return createMockResponse(404, { error: 'Not found' });
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Create extraHeaders function based on configuration
|
|
73
|
+
const extraHeaders = async (relativeUrl: string, forceRefresh = false): Promise<{ [key: string]: string }> => {
|
|
74
|
+
if (config.failExtraHeaders) {
|
|
75
|
+
throw new Error('could not get extra headers');
|
|
76
|
+
}
|
|
77
|
+
// For token refresh tests, don't return authorization header if it's a refresh attempt
|
|
78
|
+
if (forceRefresh && config.failRefreshToken) {
|
|
79
|
+
return {}; // Empty object but still conforms to the expected return type
|
|
80
|
+
}
|
|
81
|
+
return { authorization: 'Bearer test-token' };
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Create onFailedRefreshToken function based on configuration
|
|
85
|
+
const onFailedRefreshToken = async () => {
|
|
86
|
+
if (config.failOnFailedRefreshToken) {
|
|
87
|
+
throw new Error('Failed to handle refresh token');
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Initialize SDK options
|
|
92
|
+
const options: QelosSDKOptions = {
|
|
93
|
+
appUrl: 'http://localhost:3000',
|
|
94
|
+
fetch: mockFetch,
|
|
95
|
+
extraHeaders,
|
|
96
|
+
forceRefresh: config.forceRefresh !== false,
|
|
97
|
+
onFailedRefreshToken: config.failOnFailedRefreshToken !== undefined ? onFailedRefreshToken : undefined
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return new BaseSDK(options);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
await t.test('should handle network errors during initial API call', async () => {
|
|
104
|
+
const sdk = setup({ failInitialCall: true });
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
await sdk.callJsonApi('/api/test-endpoint');
|
|
108
|
+
assert.fail('Should have thrown an error for network failure');
|
|
109
|
+
} catch (error) {
|
|
110
|
+
assert.ok(error instanceof Error);
|
|
111
|
+
assert.equal(error.message, 'Network error');
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await t.test('should handle errors during extraHeaders call', async () => {
|
|
116
|
+
const sdk = setup({ failExtraHeaders: true });
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
await sdk.callJsonApi('/api/test-endpoint');
|
|
120
|
+
assert.fail('Should have thrown an error for extraHeaders failure');
|
|
121
|
+
} catch (error) {
|
|
122
|
+
assert.ok(error instanceof Error);
|
|
123
|
+
// The BaseSDK implementation will try to refresh the token when extraHeaders fails
|
|
124
|
+
// and then throw 'could not able to refresh token' if no onFailedRefreshToken is provided
|
|
125
|
+
assert.equal(error.message, 'could not able to refresh token');
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
await t.test('should handle failed token refresh', async () => {
|
|
130
|
+
const sdk = setup({ failRefreshToken: true, forceRefresh: true });
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
await sdk.callJsonApi('/api/test-endpoint');
|
|
134
|
+
assert.fail('Should have thrown an error when token refresh fails');
|
|
135
|
+
} catch (error) {
|
|
136
|
+
assert.ok(error instanceof Error);
|
|
137
|
+
// When token refresh fails and no onFailedRefreshToken is provided
|
|
138
|
+
assert.equal(error.message, 'could not able to refresh token');
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await t.test('should handle errors in onFailedRefreshToken callback', async () => {
|
|
143
|
+
const sdk = setup({
|
|
144
|
+
failRefreshToken: true,
|
|
145
|
+
failOnFailedRefreshToken: true,
|
|
146
|
+
forceRefresh: true
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
await sdk.callJsonApi('/api/test-endpoint');
|
|
151
|
+
assert.fail('Should have thrown an error when onFailedRefreshToken fails');
|
|
152
|
+
} catch (error) {
|
|
153
|
+
assert.ok(error instanceof Error);
|
|
154
|
+
assert.equal(error.message, 'could not handle failed refresh token');
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await t.test('should handle invalid JSON responses', async () => {
|
|
159
|
+
const sdk = setup({ invalidJsonResponse: true });
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
await sdk.callJsonApi('/api/test-endpoint');
|
|
163
|
+
assert.fail('Should have thrown an error for invalid JSON');
|
|
164
|
+
} catch (error) {
|
|
165
|
+
assert.ok(error instanceof Error);
|
|
166
|
+
assert.ok(error.message.includes('JSON'));
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await t.test('should handle non-JSON error responses', async () => {
|
|
171
|
+
const sdk = setup({ nonJsonErrorResponse: true });
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
await sdk.callJsonApi('/api/test-endpoint');
|
|
175
|
+
assert.fail('Should have thrown an error for non-JSON error response');
|
|
176
|
+
} catch (error) {
|
|
177
|
+
assert.ok(error instanceof Error);
|
|
178
|
+
assert.equal(error.message, 'Bad request error message');
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await t.test('should handle errors during token refresh after extraHeaders error', async () => {
|
|
183
|
+
const sdk = setup({
|
|
184
|
+
failExtraHeaders: true,
|
|
185
|
+
failRefreshToken: true,
|
|
186
|
+
forceRefresh: true
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
await sdk.callJsonApi('/api/test-endpoint');
|
|
191
|
+
assert.fail('Should have thrown an error');
|
|
192
|
+
} catch (error) {
|
|
193
|
+
assert.ok(error instanceof Error);
|
|
194
|
+
assert.equal(error.message, 'could not able to refresh token');
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as assert from 'node:assert/strict';
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
import BaseSDK from '../src/base-sdk';
|
|
4
|
+
import { QelosSDKOptions } from '../src/types';
|
|
5
|
+
|
|
6
|
+
test('BaseSDK', async (t) => {
|
|
7
|
+
let options: QelosSDKOptions;
|
|
8
|
+
let sdk: BaseSDK;
|
|
9
|
+
let fetchCalls: Array<[string, RequestInit]> = [];
|
|
10
|
+
|
|
11
|
+
// Setup before each test
|
|
12
|
+
const setup = () => {
|
|
13
|
+
fetchCalls = [];
|
|
14
|
+
// Initialize SDK with mocked fetch function
|
|
15
|
+
options = {
|
|
16
|
+
appUrl: 'http://localhost:3000',
|
|
17
|
+
fetch: async (url, init) => {
|
|
18
|
+
fetchCalls.push([url.toString(), init]);
|
|
19
|
+
const res = new Response(JSON.stringify({ success: true }));
|
|
20
|
+
res.headers.set('Content-Type', 'application/json');
|
|
21
|
+
return res;
|
|
22
|
+
},
|
|
23
|
+
extraHeaders: async () => ({
|
|
24
|
+
'x-test-header': 'test-value'
|
|
25
|
+
})
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
sdk = new BaseSDK(options);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
await t.test('should normalize appUrl by removing trailing slash', () => {
|
|
32
|
+
setup();
|
|
33
|
+
const optionsWithTrailingSlash = {
|
|
34
|
+
...options,
|
|
35
|
+
appUrl: 'http://localhost:3000/'
|
|
36
|
+
};
|
|
37
|
+
const sdkWithTrailingSlash = new BaseSDK(optionsWithTrailingSlash);
|
|
38
|
+
|
|
39
|
+
return sdkWithTrailingSlash.callApi('/test').then(() => {
|
|
40
|
+
assert.equal(fetchCalls[0][0], 'http://localhost:3000/test');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await t.test('should call API with correct URL and headers', () => {
|
|
45
|
+
setup();
|
|
46
|
+
return sdk.callApi('/test').then(() => {
|
|
47
|
+
assert.equal(fetchCalls[0][0], 'http://localhost:3000/test');
|
|
48
|
+
assert.equal(fetchCalls[0][1].headers['x-test-header'], 'test-value');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await t.test('should parse JSON response correctly', () => {
|
|
53
|
+
setup();
|
|
54
|
+
return sdk.callJsonApi('/test').then(data => {
|
|
55
|
+
assert.deepEqual(data, { success: true });
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await t.test('should handle query parameters correctly', () => {
|
|
60
|
+
setup();
|
|
61
|
+
const params = { foo: 'bar', baz: 'qux' };
|
|
62
|
+
const queryString = sdk.getQueryParams(params);
|
|
63
|
+
assert.equal(queryString, '?foo=bar&baz=qux');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
await t.test('should handle empty query parameters', () => {
|
|
67
|
+
setup();
|
|
68
|
+
const queryString = sdk.getQueryParams();
|
|
69
|
+
assert.equal(queryString, '');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await t.test('should handle extraQueryParams option', () => {
|
|
73
|
+
setup();
|
|
74
|
+
options.extraQueryParams = () => ({ global: 'param' });
|
|
75
|
+
const sdkWithExtraParams = new BaseSDK(options);
|
|
76
|
+
|
|
77
|
+
const queryString = sdkWithExtraParams.getQueryParams({ local: 'param' });
|
|
78
|
+
assert.equal(queryString, '?global=param&local=param');
|
|
79
|
+
});
|
|
80
|
+
});
|