Skip to content

Commit fcde6da

Browse files
[ui-core] add put and path methods in api utils and useSaveData hook (#4199)
1 parent 76dffc6 commit fcde6da

File tree

4 files changed

+206
-41
lines changed

4 files changed

+206
-41
lines changed

desktop/core/src/desktop/js/api/utils.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ export interface DefaultApiResponse {
4242
content?: string;
4343
}
4444

45+
export enum HttpMethod {
46+
POST = 'post',
47+
PUT = 'put',
48+
PATCH = 'patch'
49+
}
50+
4551
export interface ApiFetchOptions<T, E = AxiosError<DefaultApiResponse>> extends AxiosRequestConfig {
4652
silenceErrors?: boolean;
4753
ignoreSuccessErrors?: boolean;
@@ -193,7 +199,9 @@ const getCancelToken = (): { cancelToken: CancelToken; cancel: () => void } => {
193199
return { cancelToken: cancelTokenSource.token, cancel: cancelTokenSource.cancel };
194200
};
195201

196-
export const post = <T, U = unknown, E = AxiosError>(
202+
// Shared HTTP method for post, put, patch requests
203+
export const sendApiRequest = <T, U = unknown, E = AxiosError>(
204+
method: HttpMethod,
197205
url: string,
198206
data?: U,
199207
options?: ApiFetchOptions<T, E>
@@ -204,11 +212,10 @@ export const post = <T, U = unknown, E = AxiosError>(
204212

205213
const encodeData = options?.qsEncodeData == undefined || options?.qsEncodeData;
206214

207-
axiosInstance
208-
.post<T & DefaultApiResponse>(url, encodeData ? qs.stringify(data) : data, {
209-
cancelToken,
210-
...options
211-
})
215+
axiosInstance[method]<T & DefaultApiResponse>(url, encodeData ? qs.stringify(data) : data, {
216+
cancelToken,
217+
...options
218+
})
212219
.then(response => {
213220
handleResponse(response, resolve, reject, options);
214221
})
@@ -233,6 +240,24 @@ export const post = <T, U = unknown, E = AxiosError>(
233240
});
234241
});
235242

243+
export const post = <T, U = unknown, E = AxiosError>(
244+
url: string,
245+
data?: U,
246+
options?: ApiFetchOptions<T, E>
247+
): CancellablePromise<T> => sendApiRequest(HttpMethod.POST, url, data, options);
248+
249+
export const put = <T, U = unknown, E = AxiosError>(
250+
url: string,
251+
data?: U,
252+
options?: ApiFetchOptions<T, E>
253+
): CancellablePromise<T> => sendApiRequest(HttpMethod.PUT, url, data, options);
254+
255+
export const patch = <T, U = unknown, E = AxiosError>(
256+
url: string,
257+
data?: U,
258+
options?: ApiFetchOptions<T, E>
259+
): CancellablePromise<T> => sendApiRequest(HttpMethod.PATCH, url, data, options);
260+
236261
export const get = <T, U = unknown, E = AxiosError<DefaultApiResponse>>(
237262
url: string,
238263
data?: U,

desktop/core/src/desktop/js/apps/storageBrowser/StorageFilePage/StorageFilePage.test.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,14 @@ jest.mock('../../../utils/huePubSub', () => ({
3232
publish: jest.fn()
3333
}));
3434

35-
const mockSave = jest.fn();
36-
jest.mock('../../../api/utils', () => ({
37-
post: () => mockSave()
38-
}));
35+
const mockSendApiRequest = jest.fn();
36+
jest.mock('../../../api/utils', () => {
37+
const original = jest.requireActual('../../../api/utils');
38+
return {
39+
...original,
40+
sendApiRequest: () => mockSendApiRequest()
41+
};
42+
});
3943

4044
const mockLoadData = jest.fn().mockReturnValue({
4145
contents: 'Initial file content'

desktop/core/src/desktop/js/utils/hooks/useSaveData/useSaveData.test.tsx

Lines changed: 154 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,35 @@
1616

1717
import { renderHook, act, waitFor } from '@testing-library/react';
1818
import useSaveData from './useSaveData';
19-
import { post } from '../../../api/utils';
20-
21-
jest.mock('../../../api/utils', () => ({
22-
post: jest.fn()
23-
}));
19+
import { HttpMethod, sendApiRequest } from '../../../api/utils';
20+
21+
jest.mock('../../../api/utils', () => {
22+
const original = jest.requireActual('../../../api/utils');
23+
return {
24+
...original,
25+
post: jest.fn(),
26+
put: jest.fn(),
27+
patch: jest.fn(),
28+
sendApiRequest: jest.fn()
29+
};
30+
});
2431

25-
const mockPost = post as jest.MockedFunction<typeof post>;
32+
const mockSendApiRequest = sendApiRequest as jest.MockedFunction<typeof sendApiRequest>;
2633
const mockUrlPrefix = 'https://api.example.com';
2734
const mockEndpoint = '/save-endpoint';
2835
const mockUrl = `${mockUrlPrefix}${mockEndpoint}`;
2936
const mockData = { id: 1, product: 'Hue' };
3037
const mockBody = { id: 1 };
38+
const mockRequestOptions = {
39+
ignoreSuccessErrors: true,
40+
qsEncodeData: false,
41+
silenceErrors: true
42+
};
3143

3244
describe('useSaveData', () => {
3345
beforeEach(() => {
3446
jest.clearAllMocks();
35-
mockPost.mockResolvedValue(mockData);
47+
mockSendApiRequest.mockResolvedValue(mockData);
3648
});
3749

3850
it('should save data successfully and update state', async () => {
@@ -49,8 +61,13 @@ describe('useSaveData', () => {
4961
expect(result.current.loading).toBe(true);
5062

5163
await waitFor(() => {
52-
expect(mockPost).toHaveBeenCalledTimes(1);
53-
expect(mockPost).toHaveBeenCalledWith(mockUrl, mockBody, expect.any(Object));
64+
expect(mockSendApiRequest).toHaveBeenCalledTimes(1);
65+
expect(mockSendApiRequest).toHaveBeenCalledWith(
66+
HttpMethod.POST,
67+
mockUrl,
68+
mockBody,
69+
mockRequestOptions
70+
);
5471
expect(result.current.data).toEqual(mockData);
5572
expect(result.current.error).toBeUndefined();
5673
expect(result.current.loading).toBe(false);
@@ -59,7 +76,7 @@ describe('useSaveData', () => {
5976

6077
it('should handle errors and update error state', async () => {
6178
const mockError = new Error('Save error');
62-
mockPost.mockRejectedValue(mockError);
79+
mockSendApiRequest.mockRejectedValue(mockError);
6380

6481
const { result } = renderHook(() => useSaveData(mockUrl));
6582

@@ -74,7 +91,12 @@ describe('useSaveData', () => {
7491
expect(result.current.loading).toBe(true);
7592

7693
await waitFor(() => {
77-
expect(mockPost).toHaveBeenCalledWith(mockUrl, mockBody, expect.any(Object));
94+
expect(mockSendApiRequest).toHaveBeenCalledWith(
95+
HttpMethod.POST,
96+
mockUrl,
97+
mockBody,
98+
mockRequestOptions
99+
);
78100
expect(result.current.data).toBeUndefined();
79101
expect(result.current.error).toEqual(mockError);
80102
expect(result.current.loading).toBe(false);
@@ -91,7 +113,7 @@ describe('useSaveData', () => {
91113
expect(result.current.data).toBeUndefined();
92114
expect(result.current.error).toBeUndefined();
93115
expect(result.current.loading).toBe(false);
94-
expect(mockPost).not.toHaveBeenCalled();
116+
expect(mockSendApiRequest).not.toHaveBeenCalled();
95117
});
96118

97119
it('should update options when props change', async () => {
@@ -110,15 +132,20 @@ describe('useSaveData', () => {
110132
expect(result.current.loading).toBe(true);
111133

112134
await waitFor(() => {
113-
expect(mockPost).toHaveBeenCalledWith(mockUrl, mockBody, expect.any(Object));
135+
expect(mockSendApiRequest).toHaveBeenCalledWith(
136+
HttpMethod.POST,
137+
mockUrl,
138+
mockBody,
139+
mockRequestOptions
140+
);
114141
expect(result.current.data).toEqual(mockData);
115142
expect(result.current.error).toBeUndefined();
116143
expect(result.current.loading).toBe(false);
117144
});
118145

119146
const newBody = { id: 2 };
120147
const newMockData = { ...mockData, id: 2 };
121-
mockPost.mockResolvedValueOnce(newMockData);
148+
mockSendApiRequest.mockResolvedValueOnce(newMockData);
122149

123150
rerender({ url: mockUrl });
124151

@@ -129,7 +156,12 @@ describe('useSaveData', () => {
129156
expect(result.current.loading).toBe(true);
130157

131158
await waitFor(() => {
132-
expect(mockPost).toHaveBeenCalledWith(mockUrl, newBody, expect.any(Object));
159+
expect(mockSendApiRequest).toHaveBeenCalledWith(
160+
HttpMethod.POST,
161+
mockUrl,
162+
newBody,
163+
mockRequestOptions
164+
);
133165
expect(result.current.data).toEqual(newMockData);
134166
expect(result.current.error).toBeUndefined();
135167
expect(result.current.loading).toBe(false);
@@ -157,7 +189,12 @@ describe('useSaveData', () => {
157189
expect(result.current.loading).toBe(true);
158190

159191
await waitFor(() => {
160-
expect(mockPost).toHaveBeenCalledWith(mockUrl, mockBody, expect.any(Object));
192+
expect(mockSendApiRequest).toHaveBeenCalledWith(
193+
HttpMethod.POST,
194+
mockUrl,
195+
mockBody,
196+
mockRequestOptions
197+
);
161198
expect(result.current.data).toEqual(mockData);
162199
expect(result.current.error).toBeUndefined();
163200
expect(result.current.loading).toBe(false);
@@ -168,7 +205,7 @@ describe('useSaveData', () => {
168205

169206
it('should call onError callback when provided', async () => {
170207
const mockError = new Error('Save error');
171-
mockPost.mockRejectedValue(mockError);
208+
mockSendApiRequest.mockRejectedValue(mockError);
172209

173210
const mockOnSuccess = jest.fn();
174211
const mockOnError = jest.fn();
@@ -190,7 +227,12 @@ describe('useSaveData', () => {
190227
expect(result.current.loading).toBe(true);
191228

192229
await waitFor(() => {
193-
expect(mockPost).toHaveBeenCalledWith(mockUrl, mockBody, expect.any(Object));
230+
expect(mockSendApiRequest).toHaveBeenCalledWith(
231+
HttpMethod.POST,
232+
mockUrl,
233+
mockBody,
234+
mockRequestOptions
235+
);
194236
expect(result.current.data).toBeUndefined();
195237
expect(result.current.error).toEqual(mockError);
196238
expect(result.current.loading).toBe(false);
@@ -207,7 +249,8 @@ describe('useSaveData', () => {
207249
});
208250

209251
await waitFor(() => {
210-
expect(mockPost).toHaveBeenCalledWith(
252+
expect(mockSendApiRequest).toHaveBeenCalledWith(
253+
HttpMethod.POST,
211254
mockUrl,
212255
'hue data',
213256
expect.objectContaining({ qsEncodeData: true })
@@ -228,7 +271,8 @@ describe('useSaveData', () => {
228271
});
229272

230273
await waitFor(() => {
231-
expect(mockPost).toHaveBeenCalledWith(
274+
expect(mockSendApiRequest).toHaveBeenCalledWith(
275+
HttpMethod.POST,
232276
mockUrl,
233277
payload,
234278
expect.objectContaining({ qsEncodeData: false })
@@ -249,7 +293,8 @@ describe('useSaveData', () => {
249293
});
250294

251295
await waitFor(() => {
252-
expect(mockPost).toHaveBeenCalledWith(
296+
expect(mockSendApiRequest).toHaveBeenCalledWith(
297+
HttpMethod.POST,
253298
mockUrl,
254299
payload,
255300
expect.objectContaining({ qsEncodeData: false })
@@ -274,7 +319,8 @@ describe('useSaveData', () => {
274319
});
275320

276321
await waitFor(() => {
277-
expect(mockPost).toHaveBeenCalledWith(
322+
expect(mockSendApiRequest).toHaveBeenCalledWith(
323+
HttpMethod.POST,
278324
mockUrl,
279325
payload,
280326
expect.objectContaining({ qsEncodeData: true })
@@ -284,4 +330,90 @@ describe('useSaveData', () => {
284330
expect(result.current.loading).toBe(false);
285331
});
286332
});
333+
334+
it('should use PUT method when specified in options', async () => {
335+
mockSendApiRequest.mockResolvedValue(mockData);
336+
337+
const { result } = renderHook(() => useSaveData(mockUrl, { method: HttpMethod.PUT }));
338+
339+
act(() => {
340+
result.current.save(mockBody);
341+
});
342+
343+
await waitFor(() => {
344+
expect(mockSendApiRequest).toHaveBeenCalledTimes(1);
345+
expect(mockSendApiRequest).toHaveBeenCalledWith(
346+
HttpMethod.PUT,
347+
mockUrl,
348+
mockBody,
349+
mockRequestOptions
350+
);
351+
expect(result.current.data).toEqual(mockData);
352+
expect(result.current.loading).toBe(false);
353+
});
354+
});
355+
356+
it('should use PATCH method when specified in saveOptions', async () => {
357+
mockSendApiRequest.mockResolvedValue(mockData);
358+
359+
const { result } = renderHook(() => useSaveData(mockUrl));
360+
361+
act(() => {
362+
result.current.save(mockBody, { method: HttpMethod.PATCH });
363+
});
364+
365+
await waitFor(() => {
366+
expect(mockSendApiRequest).toHaveBeenCalledTimes(1);
367+
expect(mockSendApiRequest).toHaveBeenCalledWith(
368+
HttpMethod.PATCH,
369+
mockUrl,
370+
mockBody,
371+
mockRequestOptions
372+
);
373+
expect(result.current.data).toEqual(mockData);
374+
expect(result.current.loading).toBe(false);
375+
});
376+
});
377+
378+
it('should prioritize saveOptions method over options method', async () => {
379+
mockSendApiRequest.mockResolvedValue(mockData);
380+
381+
const { result } = renderHook(() => useSaveData(mockUrl, { method: HttpMethod.PUT }));
382+
383+
act(() => {
384+
result.current.save(mockBody, { method: HttpMethod.PATCH });
385+
});
386+
387+
await waitFor(() => {
388+
expect(mockSendApiRequest).toHaveBeenCalledTimes(1);
389+
expect(mockSendApiRequest).toHaveBeenCalledWith(
390+
HttpMethod.PATCH,
391+
mockUrl,
392+
mockBody,
393+
mockRequestOptions
394+
);
395+
expect(result.current.data).toEqual(mockData);
396+
expect(result.current.loading).toBe(false);
397+
});
398+
});
399+
400+
it('should default to POST when no method is specified', async () => {
401+
const { result } = renderHook(() => useSaveData(mockUrl));
402+
403+
act(() => {
404+
result.current.save(mockBody);
405+
});
406+
407+
await waitFor(() => {
408+
expect(mockSendApiRequest).toHaveBeenCalledTimes(1);
409+
expect(mockSendApiRequest).toHaveBeenCalledWith(
410+
HttpMethod.POST,
411+
mockUrl,
412+
mockBody,
413+
mockRequestOptions
414+
);
415+
expect(result.current.data).toEqual(mockData);
416+
expect(result.current.loading).toBe(false);
417+
});
418+
});
287419
});

0 commit comments

Comments
 (0)