@qelos/sdk 3.8.0 → 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.
@@ -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"}
@@ -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,37 +15,77 @@ export default class BaseSDK {
10
15
  async callApi(relativeUrl, data) {
11
16
  data = data || {};
12
17
  data.headers = data.headers || {};
13
- Object.assign(data.headers, await this.qlOptions.extraHeaders(relativeUrl));
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
- callJsonApi(relativeUrl, data) {
17
- return this.callApi(relativeUrl, data).then(async (res) => {
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
- const headers = await this.qlOptions.extraHeaders(relativeUrl, true);
20
- if (!headers.authorization) {
21
- if (this.qlOptions.onFailedRefreshToken) {
22
- try {
23
- await this.qlOptions.onFailedRefreshToken();
24
- }
25
- catch (e) {
26
- throw new Error('could not able to refresh token');
27
- }
63
+ let headers = {};
64
+ if (this.qlOptions.extraHeaders) {
65
+ try {
66
+ headers = await this.qlOptions.extraHeaders(relativeUrl, true);
28
67
  }
29
- throw new Error('could not able to refresh token');
68
+ catch {
69
+ // Ignore error during refresh attempt
70
+ }
71
+ }
72
+ if (!headers?.authorization) {
73
+ await this.handleFailedRefreshToken();
30
74
  }
31
75
  res = await this.callApi(relativeUrl, data);
32
76
  }
33
- const isJson = (res.headers.get('Content-Type') ||
34
- res.headers.get('content-type') ||
35
- res.headers.get('ContentType') ||
36
- res.headers.get('contenttype') ||
37
- res.headers.get('contentType') || 'text').includes('json');
38
- const body = await (isJson ? res.json() : res.text());
39
- if (!res.ok) {
40
- throw (typeof body === 'string' ? new Error(body) : body);
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);
41
86
  }
42
- return body;
43
- });
87
+ throw err;
88
+ }
44
89
  }
45
90
  getQueryParams(moreQuery) {
46
91
  const qs = this.qlOptions.extraQueryParams ? { ...this.qlOptions.extraQueryParams(), ...(moreQuery || {}) } : moreQuery;
@@ -1 +1 @@
1
- {"version":3,"file":"base-sdk.js","sourceRoot":"","sources":["../src/base-sdk.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"base-sdk.js","sourceRoot":"","sources":["../src/base-sdk.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qelos/sdk",
3
- "version": "3.8.0",
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.0"
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": "cb6281585059df8baa9b2ce388fb00577b2cf99b"
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,37 +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
- Object.assign(data.headers, await this.qlOptions.extraHeaders(relativeUrl));
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
- callJsonApi<T>(relativeUrl: string, data?: RequestInit): Promise<T> {
20
- return this.callApi(relativeUrl, data).then(async res => {
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
- const headers = await this.qlOptions.extraHeaders(relativeUrl, true);
23
- if (!headers.authorization) {
24
- if (this.qlOptions.onFailedRefreshToken) {
25
- try {
26
- await this.qlOptions.onFailedRefreshToken();
27
- } catch (e) {
28
- throw new Error('could not able to refresh token');
29
- }
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
30
80
  }
31
- throw new Error('could not able to refresh token');
32
81
  }
82
+
83
+ if (!headers?.authorization) {
84
+ await this.handleFailedRefreshToken();
85
+ }
86
+
33
87
  res = await this.callApi(relativeUrl, data);
34
88
  }
35
- const isJson = (res.headers.get('Content-Type') ||
36
- res.headers.get('content-type') ||
37
- res.headers.get('ContentType') ||
38
- res.headers.get('contenttype') ||
39
- res.headers.get('contentType') || 'text').includes('json');
40
- const body = await (isJson ? res.json() : res.text());
41
- if (!res.ok) {
42
- throw (typeof body === 'string' ? new Error(body) : body);
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);
43
99
  }
44
- return body
45
- });
100
+
101
+ throw err;
102
+ }
46
103
  }
47
104
 
48
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
+ });