@growi/sdk-typescript 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,369 @@
1
+ import type { AxiosRequestConfig, AxiosResponse } from 'axios';
2
+ import Axios from 'axios';
3
+ /**
4
+ * Tests for v3/axios-instance.ts
5
+ *
6
+ * This file provides custom Axios instance creation functionality for GROWI API v3,
7
+ * including automatic base URL configuration (with v3 suffix), cancellation capability, and request configuration merging.
8
+ */
9
+ import { type MockedFunction, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
10
+ import { AXIOS_DEFAULT } from '../axios-default-instance.js';
11
+ import { customInstance } from './axios-instance.js';
12
+
13
+ // Mock Axios
14
+ vi.mock('axios');
15
+ vi.mock('../axios-default-instance.js');
16
+
17
+ const mockedAxios = vi.mocked(Axios);
18
+ const mockedAXIOS_DEFAULT = vi.mocked(AXIOS_DEFAULT);
19
+
20
+ // Type for the promise returned by customInstance with cancel functionality
21
+ type CancellablePromise<T> = Promise<T> & {
22
+ cancel: () => void;
23
+ };
24
+
25
+ describe('v3 customInstance', () => {
26
+ let mockCancelTokenSource: {
27
+ token: string;
28
+ cancel: MockedFunction<() => void>;
29
+ };
30
+ let mockAxiosInstance: MockedFunction<(config: AxiosRequestConfig) => Promise<AxiosResponse<unknown>>>;
31
+ beforeEach(() => {
32
+ vi.clearAllMocks();
33
+
34
+ // Mock for CancelTokenSource
35
+ mockCancelTokenSource = {
36
+ token: 'mock-cancel-token',
37
+ cancel: vi.fn(),
38
+ };
39
+
40
+ // Mock for Axios.CancelToken.source
41
+ mockedAxios.CancelToken = {
42
+ source: vi.fn().mockReturnValue(mockCancelTokenSource),
43
+ } as unknown as typeof Axios.CancelToken;
44
+
45
+ // Mock for Axios instance
46
+ mockAxiosInstance = vi.fn().mockResolvedValue({
47
+ data: { result: 'success' },
48
+ status: 200,
49
+ statusText: 'OK',
50
+ headers: {},
51
+ config: {},
52
+ } as AxiosResponse<{ result: string }>);
53
+
54
+ // Mock for AXIOS_DEFAULT
55
+ mockedAXIOS_DEFAULT.instance = mockAxiosInstance as unknown as typeof AXIOS_DEFAULT.instance;
56
+ Object.defineProperty(mockedAXIOS_DEFAULT.instance, 'defaults', {
57
+ value: {
58
+ baseURL: 'http://localhost',
59
+ headers: {
60
+ common: {},
61
+ delete: {},
62
+ get: {},
63
+ head: {},
64
+ post: {},
65
+ put: {},
66
+ patch: {},
67
+ },
68
+ },
69
+ writable: true,
70
+ configurable: true,
71
+ });
72
+ });
73
+
74
+ afterEach(() => {
75
+ vi.resetAllMocks();
76
+ });
77
+
78
+ describe('Base URL configuration', () => {
79
+ it('should append _api/v3 suffix to default base URL', async () => {
80
+ // Arrange
81
+ const config: AxiosRequestConfig = { method: 'GET', url: '/test' };
82
+
83
+ // Act
84
+ await customInstance(config);
85
+
86
+ // Assert
87
+ expect(mockAxiosInstance).toHaveBeenCalledWith(
88
+ expect.objectContaining({
89
+ baseURL: 'http://localhost/_api/v3',
90
+ cancelToken: 'mock-cancel-token',
91
+ }),
92
+ );
93
+ });
94
+
95
+ it('should append _api/v3 suffix to baseURL specified in options', async () => {
96
+ // Arrange
97
+ const config: AxiosRequestConfig = { method: 'GET', url: '/test' };
98
+ const options: AxiosRequestConfig = { baseURL: 'https://custom.example.com' };
99
+
100
+ // Act
101
+ await customInstance(config, options);
102
+
103
+ // Assert
104
+ expect(mockAxiosInstance).toHaveBeenCalledWith(
105
+ expect.objectContaining({
106
+ baseURL: 'https://custom.example.com/_api/v3',
107
+ }),
108
+ );
109
+ });
110
+
111
+ it('should append _api/v3 suffix to baseURL specified in config', async () => {
112
+ // Arrange
113
+ const config: AxiosRequestConfig = {
114
+ method: 'GET',
115
+ url: '/test',
116
+ baseURL: 'https://config.example.com',
117
+ };
118
+
119
+ // Act
120
+ await customInstance(config);
121
+
122
+ // Assert
123
+ expect(mockAxiosInstance).toHaveBeenCalledWith(
124
+ expect.objectContaining({
125
+ baseURL: 'https://config.example.com/_api/v3',
126
+ }),
127
+ );
128
+ });
129
+
130
+ it('should set only _api/v3 when baseURL is empty string', async () => {
131
+ // Arrange
132
+ mockedAXIOS_DEFAULT.instance.defaults.baseURL = '';
133
+ const config: AxiosRequestConfig = { method: 'GET', url: '/test' };
134
+
135
+ // Act
136
+ await customInstance(config);
137
+
138
+ // Assert
139
+ expect(mockAxiosInstance).toHaveBeenCalledWith(
140
+ expect.objectContaining({
141
+ baseURL: '/_api/v3',
142
+ }),
143
+ );
144
+ });
145
+ });
146
+
147
+ describe('Configuration merging', () => {
148
+ it('should correctly merge config and options', async () => {
149
+ // Arrange
150
+ const config: AxiosRequestConfig = {
151
+ method: 'POST',
152
+ url: '/pages',
153
+ data: { title: 'Test Page', body: 'Content' },
154
+ };
155
+ const options: AxiosRequestConfig = {
156
+ headers: { 'X-API-Version': 'v3' },
157
+ timeout: 10000,
158
+ };
159
+
160
+ // Act
161
+ await customInstance(config, options);
162
+
163
+ // Assert
164
+ expect(mockAxiosInstance).toHaveBeenCalledWith(
165
+ expect.objectContaining({
166
+ method: 'POST',
167
+ url: '/pages',
168
+ data: { title: 'Test Page', body: 'Content' },
169
+ headers: { 'X-API-Version': 'v3' },
170
+ timeout: 10000,
171
+ baseURL: 'http://localhost/_api/v3',
172
+ cancelToken: 'mock-cancel-token',
173
+ }),
174
+ );
175
+ });
176
+
177
+ it('should override config values with options values', async () => {
178
+ // Arrange
179
+ const config: AxiosRequestConfig = {
180
+ method: 'GET',
181
+ timeout: 1000,
182
+ headers: { 'Content-Type': 'application/json' },
183
+ };
184
+ const options: AxiosRequestConfig = {
185
+ timeout: 5000,
186
+ headers: { 'Content-Type': 'application/xml' },
187
+ };
188
+
189
+ // Act
190
+ await customInstance(config, options);
191
+
192
+ // Assert
193
+ expect(mockAxiosInstance).toHaveBeenCalledWith(
194
+ expect.objectContaining({
195
+ timeout: 5000,
196
+ headers: { 'Content-Type': 'application/xml' },
197
+ }),
198
+ );
199
+ });
200
+ });
201
+
202
+ describe('Response processing', () => {
203
+ it('should return the data property from response', async () => {
204
+ // Arrange
205
+ const expectedData = {
206
+ pages: [
207
+ { id: '1', title: 'Page 1', body: 'Content 1' },
208
+ { id: '2', title: 'Page 2', body: 'Content 2' },
209
+ ],
210
+ };
211
+ mockAxiosInstance.mockResolvedValue({
212
+ data: expectedData,
213
+ status: 200,
214
+ statusText: 'OK',
215
+ headers: {},
216
+ config: {},
217
+ } as AxiosResponse<{
218
+ pages: Array<{ id: string; title: string; body: string }>;
219
+ }>);
220
+
221
+ const config: AxiosRequestConfig = { method: 'GET', url: '/pages' };
222
+
223
+ // Act
224
+ const result = await customInstance(config);
225
+
226
+ // Assert
227
+ expect(result).toEqual(expectedData);
228
+ });
229
+
230
+ it('should handle empty response data correctly', async () => {
231
+ // Arrange
232
+ const expectedData = null;
233
+ mockAxiosInstance.mockResolvedValue({
234
+ data: expectedData,
235
+ status: 204,
236
+ statusText: 'No Content',
237
+ headers: {},
238
+ config: {},
239
+ } as AxiosResponse<null>);
240
+
241
+ const config: AxiosRequestConfig = { method: 'DELETE', url: '/pages/1' };
242
+
243
+ // Act
244
+ const result = await customInstance(config);
245
+
246
+ // Assert
247
+ expect(result).toBeNull();
248
+ });
249
+ });
250
+
251
+ describe('Cancellation functionality', () => {
252
+ it('should add cancel function to returned Promise', async () => {
253
+ // Arrange
254
+ const config: AxiosRequestConfig = { method: 'GET', url: '/test' };
255
+
256
+ // Act
257
+ const promise = customInstance(config) as CancellablePromise<unknown>;
258
+
259
+ // Assert
260
+ expect(promise).toHaveProperty('cancel');
261
+ expect(typeof promise.cancel).toBe('function');
262
+
263
+ // cleanup
264
+ await promise;
265
+ });
266
+
267
+ it('should call CancelTokenSource cancel when cancel function is invoked', async () => {
268
+ // Arrange
269
+ const config: AxiosRequestConfig = { method: 'GET', url: '/test' };
270
+
271
+ // Act
272
+ const promise = customInstance(config) as CancellablePromise<unknown>;
273
+ promise.cancel();
274
+
275
+ // Assert
276
+ expect(mockCancelTokenSource.cancel).toHaveBeenCalledWith('Query cancelled');
277
+
278
+ // cleanup
279
+ await promise;
280
+ });
281
+
282
+ it('should allow cancellation during request execution', async () => {
283
+ // Arrange
284
+ let resolveRequest: (value: unknown) => void = () => {};
285
+ const requestPromise = new Promise((resolve) => {
286
+ resolveRequest = resolve;
287
+ });
288
+
289
+ mockAxiosInstance.mockImplementation(() => requestPromise);
290
+
291
+ const config: AxiosRequestConfig = { method: 'GET', url: '/long-request' };
292
+
293
+ // Act
294
+ const promise = customInstance(config) as CancellablePromise<unknown>;
295
+ promise.cancel();
296
+
297
+ // Assert
298
+ expect(mockCancelTokenSource.cancel).toHaveBeenCalledWith('Query cancelled');
299
+
300
+ // cleanup
301
+ if (resolveRequest) {
302
+ resolveRequest({ data: 'test' });
303
+ }
304
+ await promise;
305
+ });
306
+ });
307
+
308
+ describe('Error handling', () => {
309
+ it('should reject Promise when Axios error occurs', async () => {
310
+ // Arrange
311
+ const error = new Error('API v3 endpoint not found');
312
+ mockAxiosInstance.mockRejectedValue(error);
313
+
314
+ const config: AxiosRequestConfig = { method: 'GET', url: '/invalid' };
315
+
316
+ // Act & Assert
317
+ await expect(customInstance(config)).rejects.toThrow('API v3 endpoint not found');
318
+ });
319
+
320
+ it('should handle network errors appropriately', async () => {
321
+ // Arrange
322
+ const networkError = new Error('Network Error');
323
+ networkError.name = 'NetworkError';
324
+ mockAxiosInstance.mockRejectedValue(networkError);
325
+
326
+ const config: AxiosRequestConfig = { method: 'GET', url: '/test' };
327
+
328
+ // Act & Assert
329
+ await expect(customInstance(config)).rejects.toThrow('Network Error');
330
+ });
331
+ });
332
+
333
+ describe('Type safety', () => {
334
+ it('should apply generic types correctly', async () => {
335
+ // Arrange
336
+ interface User {
337
+ id: string;
338
+ name: string;
339
+ email: string;
340
+ }
341
+
342
+ const expectedUser: User = {
343
+ id: '1',
344
+ name: 'John Doe',
345
+ email: 'john@example.com',
346
+ };
347
+
348
+ mockAxiosInstance.mockResolvedValue({
349
+ data: expectedUser,
350
+ status: 200,
351
+ statusText: 'OK',
352
+ headers: {},
353
+ config: {},
354
+ } as AxiosResponse<User>);
355
+
356
+ const config: AxiosRequestConfig = { method: 'GET', url: '/user/1' };
357
+
358
+ // Act
359
+ const result = await customInstance<User>(config);
360
+
361
+ // Assert
362
+ expect(result).toEqual(expectedUser);
363
+ // TypeScript type checking ensures result is treated as User type
364
+ expect(result.id).toBe('1');
365
+ expect(result.name).toBe('John Doe');
366
+ expect(result.email).toBe('john@example.com');
367
+ });
368
+ });
369
+ });