Skip to content

Commit 4257fb3

Browse files
nickclaude
andcommitted
test(sdk): increase test coverage for V4UserService and client error handling
- Add comprehensive unit tests for V4UserService (from 6% to 100% function coverage) - AC1-AC13: Sub-account, Organization, Viewer Record, Label, Product, Order APIs - Template APIs: Donate, Marquee, RoleConfig, Playback, Moderation settings - User Settings APIs: Callback, GlobalSwitch, GlobalFooter, PvShowEnable - Other APIs: MicDuration, Sms, BillUseDetail, WatchLog, LotteryWin - Add error handling tests for PolyVClient (from 81% to 100% statement coverage) - Request cancellation handling - Axios error with response/timeout/no response - Non-Axios error handling - Request/Response interceptor tests - Add type definition tests for error types Overall SDK coverage improved: - Statements: 89.35% -> 92.41% - Functions: 70.75% -> 81.88% Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c670c2b commit 4257fb3

3 files changed

Lines changed: 1273 additions & 0 deletions

File tree

packages/sdk/src/client.test.ts

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,198 @@ describe('PolyVClient', () => {
202202
);
203203
});
204204
});
205+
206+
describe('error handling', () => {
207+
it('[P1] should handle request cancellation', () => {
208+
const mockAxiosCreate = vi.mocked(axios.create);
209+
const mockInstance = mockAxiosCreate();
210+
const responseInterceptorCalls = vi.mocked(mockInstance.interceptors.response.use).mock.calls;
211+
const errorHandler = responseInterceptorCalls[0][1];
212+
213+
const cancelError = { __CANCEL__: true, message: 'Request cancelled' };
214+
vi.mocked(axios.isCancel).mockReturnValueOnce(true);
215+
216+
// Error handler throws synchronously
217+
expect(() => errorHandler(cancelError)).toThrow('Request cancelled');
218+
});
219+
220+
it('[P1] should handle axios error with response', () => {
221+
const mockAxiosCreate = vi.mocked(axios.create);
222+
const mockInstance = mockAxiosCreate();
223+
const responseInterceptorCalls = vi.mocked(mockInstance.interceptors.response.use).mock.calls;
224+
const errorHandler = responseInterceptorCalls[0][1];
225+
226+
const axiosError = {
227+
response: {
228+
status: 400,
229+
data: { message: 'Bad request', error: { code: 'INVALID_PARAM' } },
230+
},
231+
message: 'Request failed',
232+
};
233+
vi.mocked(axios.isAxiosError).mockReturnValueOnce(true);
234+
vi.mocked(axios.isCancel).mockReturnValueOnce(false);
235+
236+
expect(() => errorHandler(axiosError)).toThrow('Bad request');
237+
});
238+
239+
it('[P1] should handle axios error with timeout', () => {
240+
const mockAxiosCreate = vi.mocked(axios.create);
241+
const mockInstance = mockAxiosCreate();
242+
const responseInterceptorCalls = vi.mocked(mockInstance.interceptors.response.use).mock.calls;
243+
const errorHandler = responseInterceptorCalls[0][1];
244+
245+
const axiosError = {
246+
code: 'ECONNABORTED',
247+
message: 'timeout of 30000ms exceeded',
248+
request: {},
249+
};
250+
vi.mocked(axios.isAxiosError).mockReturnValueOnce(true);
251+
vi.mocked(axios.isCancel).mockReturnValueOnce(false);
252+
253+
expect(() => errorHandler(axiosError)).toThrow('Request timeout');
254+
});
255+
256+
it('[P1] should handle axios error with no response', () => {
257+
const mockAxiosCreate = vi.mocked(axios.create);
258+
const mockInstance = mockAxiosCreate();
259+
const responseInterceptorCalls = vi.mocked(mockInstance.interceptors.response.use).mock.calls;
260+
const errorHandler = responseInterceptorCalls[0][1];
261+
262+
const axiosError = {
263+
message: 'Network error',
264+
request: {},
265+
};
266+
vi.mocked(axios.isAxiosError).mockReturnValueOnce(true);
267+
vi.mocked(axios.isCancel).mockReturnValueOnce(false);
268+
269+
expect(() => errorHandler(axiosError)).toThrow('No response from server');
270+
});
271+
272+
it('[P1] should handle non-axios errors', () => {
273+
const mockAxiosCreate = vi.mocked(axios.create);
274+
const mockInstance = mockAxiosCreate();
275+
const responseInterceptorCalls = vi.mocked(mockInstance.interceptors.response.use).mock.calls;
276+
const errorHandler = responseInterceptorCalls[0][1];
277+
278+
const genericError = new Error('Unknown error');
279+
vi.mocked(axios.isAxiosError).mockReturnValueOnce(false);
280+
vi.mocked(axios.isCancel).mockReturnValueOnce(false);
281+
282+
expect(() => errorHandler(genericError)).toThrow('Unknown error');
283+
});
284+
285+
it('[P1] should handle non-Error objects', () => {
286+
const mockAxiosCreate = vi.mocked(axios.create);
287+
const mockInstance = mockAxiosCreate();
288+
const responseInterceptorCalls = vi.mocked(mockInstance.interceptors.response.use).mock.calls;
289+
const errorHandler = responseInterceptorCalls[0][1];
290+
291+
vi.mocked(axios.isAxiosError).mockReturnValueOnce(false);
292+
vi.mocked(axios.isCancel).mockReturnValueOnce(false);
293+
294+
expect(() => errorHandler('string error')).toThrow('Unknown error');
295+
});
296+
});
297+
298+
describe('request interceptor', () => {
299+
it('[P1] should skip auth when X-Skip-Auth header is set', async () => {
300+
const mockAxiosCreate = vi.mocked(axios.create);
301+
const mockInstance = mockAxiosCreate();
302+
const requestInterceptorCalls = vi.mocked(mockInstance.interceptors.request.use).mock.calls;
303+
const requestHandler = requestInterceptorCalls[0][0];
304+
305+
const config = {
306+
headers: { 'X-Skip-Auth': true },
307+
params: { customParam: 'value' },
308+
};
309+
310+
const result = requestHandler(config);
311+
312+
expect(result.headers['X-Skip-Auth']).toBeUndefined();
313+
});
314+
315+
it('[P0] should inject signature params', async () => {
316+
const mockAxiosCreate = vi.mocked(axios.create);
317+
const mockInstance = mockAxiosCreate();
318+
const requestInterceptorCalls = vi.mocked(mockInstance.interceptors.request.use).mock.calls;
319+
const requestHandler = requestInterceptorCalls[0][0];
320+
321+
const config = {
322+
headers: {},
323+
params: { customParam: 'value' },
324+
};
325+
326+
const result = requestHandler(config);
327+
328+
expect(result.params.appId).toBe('test-app-id');
329+
expect(result.params.timestamp).toBeDefined();
330+
expect(result.params.sign).toBeDefined();
331+
});
332+
333+
it('[P1] should handle request interceptor error', async () => {
334+
const mockAxiosCreate = vi.mocked(axios.create);
335+
const mockInstance = mockAxiosCreate();
336+
const requestInterceptorCalls = vi.mocked(mockInstance.interceptors.request.use).mock.calls;
337+
const errorHandler = requestInterceptorCalls[0][1];
338+
339+
const error = new Error('Request setup failed');
340+
341+
await expect(errorHandler(error)).rejects.toThrow('Request setup failed');
342+
});
343+
});
344+
345+
describe('response interceptor success', () => {
346+
it('[P0] should extract data from successful response', async () => {
347+
const mockAxiosCreate = vi.mocked(axios.create);
348+
const mockInstance = mockAxiosCreate();
349+
const responseInterceptorCalls = vi.mocked(mockInstance.interceptors.response.use).mock.calls;
350+
const successHandler = responseInterceptorCalls[0][0];
351+
352+
const response = {
353+
data: {
354+
code: 200,
355+
data: { id: '123', name: 'test' },
356+
},
357+
};
358+
359+
const result = successHandler(response);
360+
361+
expect(result).toEqual({ id: '123', name: 'test' });
362+
});
363+
364+
it('[P1] should throw API error for non-200 code', async () => {
365+
const mockAxiosCreate = vi.mocked(axios.create);
366+
const mockInstance = mockAxiosCreate();
367+
const responseInterceptorCalls = vi.mocked(mockInstance.interceptors.response.use).mock.calls;
368+
const successHandler = responseInterceptorCalls[0][0];
369+
370+
const response = {
371+
data: {
372+
code: 400,
373+
message: 'Bad request',
374+
error: { code: 'INVALID_PARAM', desc: 'Invalid parameter' },
375+
},
376+
status: 400,
377+
};
378+
379+
expect(() => successHandler(response)).toThrow('Invalid parameter');
380+
});
381+
382+
it('[P1] should use message when error.desc is not available', async () => {
383+
const mockAxiosCreate = vi.mocked(axios.create);
384+
const mockInstance = mockAxiosCreate();
385+
const responseInterceptorCalls = vi.mocked(mockInstance.interceptors.response.use).mock.calls;
386+
const successHandler = responseInterceptorCalls[0][0];
387+
388+
const response = {
389+
data: {
390+
code: 500,
391+
message: 'Internal server error',
392+
},
393+
status: 500,
394+
};
395+
396+
expect(() => successHandler(response)).toThrow('Internal server error');
397+
});
398+
});
205399
});
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/**
2+
* @fileoverview Unit tests for error types
3+
* @module errors/types.test
4+
*/
5+
6+
import { describe, it, expect } from 'vitest';
7+
import type { PolyVErrorOptions, ValidationConstraints } from './types.js';
8+
import type { PolyVAPIErrorOptions, PolyVAPIErrorResponse } from './polyv-api-error.js';
9+
10+
describe('Error Types', () => {
11+
describe('PolyVErrorOptions', () => {
12+
it('should allow code and details properties', () => {
13+
const options: PolyVErrorOptions = {
14+
code: 'TEST_ERROR',
15+
details: { foo: 'bar' },
16+
};
17+
expect(options.code).toBe('TEST_ERROR');
18+
expect(options.details).toEqual({ foo: 'bar' });
19+
});
20+
21+
it('should allow empty options', () => {
22+
const options: PolyVErrorOptions = {};
23+
expect(options.code).toBeUndefined();
24+
expect(options.details).toBeUndefined();
25+
});
26+
});
27+
28+
describe('PolyVAPIErrorOptions', () => {
29+
it('should allow polyvCode and polyvMessage', () => {
30+
const options: PolyVAPIErrorOptions = {
31+
code: 'API_ERROR',
32+
polyvCode: 200001,
33+
polyvMessage: 'Invalid parameter',
34+
};
35+
expect(options.code).toBe('API_ERROR');
36+
expect(options.polyvCode).toBe(200001);
37+
expect(options.polyvMessage).toBe('Invalid parameter');
38+
});
39+
40+
it('should allow empty options', () => {
41+
const options: PolyVAPIErrorOptions = {};
42+
expect(options.code).toBeUndefined();
43+
expect(options.polyvCode).toBeUndefined();
44+
});
45+
});
46+
47+
describe('PolyVAPIErrorResponse', () => {
48+
it('should have correct structure', () => {
49+
const response: PolyVAPIErrorResponse = {
50+
code: 200,
51+
status: 'success',
52+
message: 'Request successful',
53+
data: {
54+
polyvCode: 0,
55+
polyvMessage: 'OK',
56+
},
57+
};
58+
expect(response.code).toBe(200);
59+
expect(response.status).toBe('success');
60+
expect(response.message).toBe('Request successful');
61+
});
62+
63+
it('should allow response without data', () => {
64+
const response: PolyVAPIErrorResponse = {
65+
code: 400,
66+
status: 'error',
67+
message: 'Bad request',
68+
};
69+
expect(response.code).toBe(400);
70+
expect(response.data).toBeUndefined();
71+
});
72+
});
73+
74+
describe('ValidationConstraints', () => {
75+
it('should allow validation constraint properties', () => {
76+
const constraints: ValidationConstraints = {
77+
minLength: 1,
78+
maxLength: 100,
79+
min: 0,
80+
max: 1000,
81+
};
82+
expect(constraints.minLength).toBe(1);
83+
expect(constraints.maxLength).toBe(100);
84+
});
85+
86+
it('should allow empty constraints', () => {
87+
const constraints: ValidationConstraints = {};
88+
expect(constraints.minLength).toBeUndefined();
89+
expect(constraints.maxLength).toBeUndefined();
90+
});
91+
});
92+
});

0 commit comments

Comments
 (0)