Skip to content

Commit c23853c

Browse files
committed
Add more tests for axios utils
1 parent 5c6b18f commit c23853c

File tree

2 files changed

+312
-67
lines changed

2 files changed

+312
-67
lines changed

test/util/axios.test.ts

Lines changed: 0 additions & 67 deletions
This file was deleted.

test/util/axiosConstructor.test.ts

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
import axios, { AxiosError, AxiosHeaders, AxiosResponse } from 'axios';
2+
import { createAxiosClient, sanitizeAxiosError } from '../../src/util/axiosConstructor';
3+
4+
describe('Axios', () => {
5+
beforeEach(() => {
6+
jest.clearAllMocks();
7+
});
8+
9+
describe('sanitizeAxiosError', () => {
10+
it('returns non-error as error', () => {
11+
const result = sanitizeAxiosError('Test error');
12+
13+
expect(result).toBeInstanceOf(Error);
14+
});
15+
16+
it('returns non-axios errors as-is', () => {
17+
const error = new Error('Test error');
18+
19+
const result = sanitizeAxiosError(error);
20+
21+
expect(result).toBe(error);
22+
});
23+
24+
it('sanitizes sensitive data from axios error', () => {
25+
const secrets = {
26+
token: 'Bearer secret-token',
27+
apiKey: 'secret-api-key',
28+
cookie: 'session=secret-session',
29+
password: 'secret123',
30+
};
31+
const originalError = new AxiosError(
32+
'Test error',
33+
'TEST_CODE',
34+
{
35+
url: 'http://example.com',
36+
method: 'POST',
37+
headers: new AxiosHeaders({
38+
Authorization: secrets.token,
39+
'x-api-key': secrets.apiKey,
40+
'safe-header': 'safe-value',
41+
}),
42+
data: {
43+
password: secrets.password,
44+
token: secrets.token,
45+
safeData: 'public-info',
46+
},
47+
},
48+
undefined,
49+
{
50+
status: 400,
51+
statusText: 'Bad Request',
52+
data: { error: 'Invalid request' },
53+
headers: new AxiosHeaders({
54+
'set-cookie': secrets.cookie,
55+
}),
56+
config: {
57+
headers: new AxiosHeaders({
58+
Authorization: secrets.token,
59+
}),
60+
},
61+
},
62+
);
63+
64+
const result = sanitizeAxiosError(originalError) as AxiosError;
65+
66+
expect(result).toBeInstanceOf(AxiosError);
67+
expect(result.message).toBe('Test error');
68+
expect(result.code).toBe('TEST_CODE');
69+
70+
// Verify config is sanitized
71+
expect(result.config?.headers).toEqual(new AxiosHeaders());
72+
expect(result.config?.data).toBeUndefined();
73+
74+
// Verify response is sanitized
75+
expect(result.response?.headers).toEqual(new AxiosHeaders());
76+
expect(result.response?.config.headers).toEqual(new AxiosHeaders());
77+
78+
// Verify sensitive data is not in stringified version
79+
const errorStr = JSON.stringify(result);
80+
expect(errorStr).not.toContain(secrets.token);
81+
expect(errorStr).not.toContain(secrets.apiKey);
82+
expect(errorStr).not.toContain(secrets.password);
83+
expect(errorStr).not.toContain(secrets.cookie);
84+
});
85+
86+
it('handles errors with missing properties', () => {
87+
const originalError = new AxiosError('Test error', 'TEST_CODE');
88+
originalError.config = undefined;
89+
originalError.response = undefined;
90+
originalError.request = undefined;
91+
92+
const result = sanitizeAxiosError(originalError) as AxiosError;
93+
94+
expect(result).toMatchObject({
95+
name: AxiosError.name,
96+
message: 'Test error',
97+
code: 'TEST_CODE',
98+
config: expect.any(Object),
99+
response: expect.any(Object),
100+
request: expect.any(Object),
101+
});
102+
});
103+
104+
it('preserves stack trace', () => {
105+
const originalError = new AxiosError('Test error', 'TEST_CODE');
106+
const originalStack = 'Error: Test error\n at Test.it';
107+
originalError.stack = originalStack;
108+
109+
const result = sanitizeAxiosError(originalError) as AxiosError;
110+
111+
expect(result.stack).toBe(originalStack);
112+
});
113+
});
114+
115+
it('default client leaks credentials in AxiosError', async () => {
116+
const client = axios.create({
117+
headers: {
118+
Authorization: 'Bearer 1234',
119+
},
120+
proxy: {
121+
host: 'example.com',
122+
port: 80,
123+
auth: {
124+
username: 'myWhackyUsername',
125+
password: 'myWhackyPassword',
126+
},
127+
},
128+
});
129+
try {
130+
await client.get('http://example.com');
131+
} catch (error) {
132+
expect(error.config).toBeDefined();
133+
const errorStr = JSON.stringify(error);
134+
expect(errorStr).toContain('Bearer 1234');
135+
expect(errorStr).toContain('myWhackyUsername');
136+
expect(errorStr).toContain('myWhackyPassword');
137+
}
138+
});
139+
140+
describe('createAxiosClient', () => {
141+
describe('Client Creation and Configuration', () => {
142+
it('creates client with default config when no config provided', () => {
143+
const client = createAxiosClient();
144+
145+
expect(client.defaults.headers).toBeDefined();
146+
// eslint-disable-next-line @typescript-eslint/unbound-method
147+
expect(client.request).toBeInstanceOf(Function);
148+
});
149+
150+
it('merges custom headers with default headers', async () => {
151+
const client = createAxiosClient({
152+
headers: {
153+
'x-custom-header': 'custom-value',
154+
authorization: 'Bearer token',
155+
},
156+
});
157+
158+
const mockAdapter = jest.fn().mockImplementation((config) => {
159+
expect(config.headers).toEqual(
160+
expect.objectContaining({
161+
Accept: expect.any(String),
162+
'x-custom-header': 'custom-value',
163+
authorization: 'Bearer token',
164+
}),
165+
);
166+
return Promise.resolve({} as AxiosResponse);
167+
});
168+
client.defaults.adapter = mockAdapter;
169+
170+
await client.get('http://example.com');
171+
});
172+
});
173+
174+
describe('Error Handling', () => {
175+
it('does not convert non-axios errors to AxiosError', async () => {
176+
const client = createAxiosClient();
177+
const customError = new Error('Custom error');
178+
179+
const mockAdapter = jest.fn().mockRejectedValue(customError);
180+
client.defaults.adapter = mockAdapter;
181+
182+
try {
183+
await client.get('http://example.com');
184+
fail('Should have thrown an error');
185+
} catch (error) {
186+
expect(error).toBeInstanceOf(Error);
187+
expect(error.message).toBe('Custom error');
188+
expect(error).not.toBeInstanceOf(AxiosError);
189+
}
190+
});
191+
192+
it('preserves error stack traces after sanitization', async () => {
193+
const client = createAxiosClient();
194+
const originalError = new AxiosError('Test error', 'TEST_CODE');
195+
const originalStack = 'Error: Test error\n at Test.it';
196+
originalError.stack = originalStack;
197+
198+
const mockAdapter = jest.fn().mockRejectedValue(originalError);
199+
client.defaults.adapter = mockAdapter;
200+
201+
try {
202+
await client.get('http://example.com');
203+
fail('Should have thrown an error');
204+
} catch (error) {
205+
expect(error.stack).toBe(originalStack);
206+
}
207+
});
208+
});
209+
210+
describe('Data Sanitization', () => {
211+
it('sanitizes sensitive data from request headers', async () => {
212+
const client = createAxiosClient();
213+
const originalError = new AxiosError('Test error', 'TEST_CODE', {
214+
headers: new AxiosHeaders({
215+
Authorization: 'Bearer secret-token',
216+
'x-api-key': 'secret-api-key',
217+
'safe-header': 'safe-value',
218+
}),
219+
});
220+
221+
const mockAdapter = jest.fn().mockRejectedValue(originalError);
222+
client.defaults.adapter = mockAdapter;
223+
224+
try {
225+
await client.get('http://example.com');
226+
fail('Should have thrown an error');
227+
} catch (error) {
228+
expect(error.config.headers).toBeInstanceOf(AxiosHeaders);
229+
expect(error.config.headers.get('Authorization')).toBeUndefined();
230+
expect(error.config.headers.get('x-api-key')).toBeUndefined();
231+
expect(error.config.headers.get('safe-header')).toBeUndefined();
232+
expect(JSON.stringify(error)).not.toContain('secret-token');
233+
}
234+
});
235+
236+
it('sanitizes sensitive data from request body', async () => {
237+
const client = createAxiosClient();
238+
const sensitiveData = {
239+
password: 'secret123',
240+
token: 'sensitive-token',
241+
safeData: 'public-info',
242+
};
243+
244+
const originalError = new AxiosError('Test error', 'TEST_CODE', {
245+
data: sensitiveData,
246+
headers: new AxiosHeaders(),
247+
});
248+
249+
const mockAdapter = jest.fn().mockRejectedValue(originalError);
250+
client.defaults.adapter = mockAdapter;
251+
252+
try {
253+
await client.post('http://example.com', sensitiveData);
254+
fail('Should have thrown an error');
255+
} catch (error) {
256+
expect(error.config.data).toBeUndefined();
257+
const errorStr = JSON.stringify(error);
258+
expect(errorStr).not.toContain('secret123');
259+
expect(errorStr).not.toContain('sensitive-token');
260+
}
261+
});
262+
});
263+
264+
describe('Response Handling', () => {
265+
it('preserves response status and status text', async () => {
266+
const client = createAxiosClient();
267+
const originalError = new AxiosError('Test error', 'TEST_CODE', { headers: new AxiosHeaders() }, undefined, {
268+
status: 404,
269+
statusText: 'Not Found',
270+
data: { message: 'Resource not found' },
271+
headers: new AxiosHeaders(),
272+
config: { headers: new AxiosHeaders() },
273+
});
274+
275+
const mockAdapter = jest.fn().mockRejectedValue(originalError);
276+
client.defaults.adapter = mockAdapter;
277+
278+
try {
279+
await client.get('http://example.com');
280+
fail('Should have thrown an error');
281+
} catch (error) {
282+
expect(error.response?.status).toBe(404);
283+
expect(error.response?.statusText).toBe('Not Found');
284+
expect(error.response?.data).toEqual({ message: 'Resource not found' });
285+
}
286+
});
287+
288+
it('handles errors with missing response data', async () => {
289+
const client = createAxiosClient();
290+
const originalError = new AxiosError('Test error', 'TEST_CODE', { headers: new AxiosHeaders() }, undefined, {
291+
data: undefined,
292+
status: 500,
293+
statusText: 'Internal Server Error',
294+
headers: new AxiosHeaders(),
295+
config: { headers: new AxiosHeaders() },
296+
});
297+
298+
const mockAdapter = jest.fn().mockRejectedValue(originalError);
299+
client.defaults.adapter = mockAdapter;
300+
301+
try {
302+
await client.get('http://example.com');
303+
fail('Should have thrown an error');
304+
} catch (error) {
305+
expect(error.response?.status).toBe(500);
306+
expect(error.response?.statusText).toBe('Internal Server Error');
307+
expect(error.response?.data).toBeUndefined();
308+
}
309+
});
310+
});
311+
});
312+
});

0 commit comments

Comments
 (0)