mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-01 08:34:07 +00:00
feat: enhance axios shim error handling and add comprehensive tests (#6349)
This commit is contained in:
@@ -34,23 +34,36 @@ class ErrorBoundary extends Component {
|
||||
|
||||
const serializeArgs = (args) => {
|
||||
return args.map((arg) => {
|
||||
const seen = new WeakSet();
|
||||
|
||||
const replacer = (key, value) => {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (seen.has(value)) {
|
||||
return '[Circular Reference]';
|
||||
}
|
||||
seen.add(value);
|
||||
|
||||
if (value instanceof Error || Object.prototype.toString.call(value) === '[object Error]' || (typeof value.message === 'string' && typeof value.stack === 'string')) {
|
||||
const error = {};
|
||||
Object.getOwnPropertyNames(value).forEach((prop) => {
|
||||
error[prop] = value[prop];
|
||||
});
|
||||
return error;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
try {
|
||||
if (arg === null) return 'null';
|
||||
if (arg === undefined) return 'undefined';
|
||||
if (typeof arg === 'string' || typeof arg === 'number' || typeof arg === 'boolean') {
|
||||
return arg;
|
||||
}
|
||||
if (arg instanceof Error) {
|
||||
return {
|
||||
__type: 'Error',
|
||||
name: arg.name,
|
||||
message: arg.message,
|
||||
stack: arg.stack
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof arg === 'object') {
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(arg));
|
||||
return JSON.parse(JSON.stringify(arg, replacer));
|
||||
} catch {
|
||||
return String(arg);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,30 @@ const { marshallToVm } = require('../../utils');
|
||||
|
||||
const methods = ['get', 'post', 'put', 'patch', 'delete'];
|
||||
|
||||
const buildAxiosErrorData = (err) => {
|
||||
return {
|
||||
message: err.message,
|
||||
code: err.code,
|
||||
isAxiosError: err.isAxiosError,
|
||||
...(err.response && {
|
||||
response: {
|
||||
status: err.response.status,
|
||||
statusText: err.response.statusText,
|
||||
headers: err.response.headers,
|
||||
data: err.response.data
|
||||
}
|
||||
}),
|
||||
...(err.config && {
|
||||
config: {
|
||||
url: err.config.url,
|
||||
method: err.config.method,
|
||||
headers: err.config.headers,
|
||||
data: err.config.data
|
||||
}
|
||||
})
|
||||
};
|
||||
};
|
||||
|
||||
const addAxiosShimToContext = async (vm) => {
|
||||
methods?.forEach((method) => {
|
||||
const axiosHandle = vm.newFunction(method, (...args) => {
|
||||
@@ -15,14 +39,7 @@ const addAxiosShimToContext = async (vm) => {
|
||||
promise.resolve(marshallToVm(cleanJson({ status, headers, data }), vm));
|
||||
})
|
||||
.catch((err) => {
|
||||
promise.resolve(
|
||||
marshallToVm(
|
||||
cleanJson({
|
||||
message: err.message
|
||||
}),
|
||||
vm
|
||||
)
|
||||
);
|
||||
promise.reject(marshallToVm(cleanJson(buildAxiosErrorData(err)), vm));
|
||||
});
|
||||
promise.settled.then(vm.runtime.executePendingJobs);
|
||||
return promise.handle;
|
||||
@@ -39,14 +56,7 @@ const addAxiosShimToContext = async (vm) => {
|
||||
promise.resolve(marshallToVm(cleanJson({ status, headers, data }), vm));
|
||||
})
|
||||
.catch((err) => {
|
||||
promise.resolve(
|
||||
marshallToVm(
|
||||
cleanJson({
|
||||
message: err.message
|
||||
}),
|
||||
vm
|
||||
)
|
||||
);
|
||||
promise.reject(marshallToVm(cleanJson(buildAxiosErrorData(err)), vm));
|
||||
});
|
||||
promise.settled.then(vm.runtime.executePendingJobs);
|
||||
return promise.handle;
|
||||
|
||||
495
packages/bruno-js/src/sandbox/quickjs/shims/lib/axios.spec.js
Normal file
495
packages/bruno-js/src/sandbox/quickjs/shims/lib/axios.spec.js
Normal file
@@ -0,0 +1,495 @@
|
||||
const { describe, it, expect, beforeAll, beforeEach, afterEach, afterAll } = require('@jest/globals');
|
||||
const { newQuickJSWASMModule } = require('quickjs-emscripten');
|
||||
const addAxiosShimToContext = require('./axios');
|
||||
|
||||
// Mock axios
|
||||
jest.mock('axios');
|
||||
const axios = require('axios');
|
||||
|
||||
describe('axios shim tests', () => {
|
||||
let vm, module;
|
||||
|
||||
beforeAll(async () => {
|
||||
module = await newQuickJSWASMModule();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
vm = module.newContext();
|
||||
await addAxiosShimToContext(vm);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (vm) {
|
||||
try {
|
||||
vm.dispose();
|
||||
} catch (err) {
|
||||
console.error('Error disposing vm', err);
|
||||
}
|
||||
vm = null;
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (module) {
|
||||
try {
|
||||
module.dispose();
|
||||
} catch (err) {
|
||||
console.error('Error disposing module', err);
|
||||
}
|
||||
module = null;
|
||||
}
|
||||
});
|
||||
|
||||
describe('successful requests', () => {
|
||||
it('should resolve axios.get with response data', async () => {
|
||||
const mockResponse = {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
data: { message: 'success' }
|
||||
};
|
||||
axios.get.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = vm.evalCode(`
|
||||
(async () => {
|
||||
const response = await axios.get('https://api.example.com/data');
|
||||
return response;
|
||||
})()
|
||||
`);
|
||||
const promiseHandle = vm.unwrapResult(result);
|
||||
const resolvedResult = await vm.resolvePromise(promiseHandle);
|
||||
const resolvedHandle = vm.unwrapResult(resolvedResult);
|
||||
const responseData = vm.dump(resolvedHandle);
|
||||
|
||||
resolvedHandle.dispose();
|
||||
promiseHandle.dispose();
|
||||
|
||||
expect(responseData).toEqual({
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
data: { message: 'success' }
|
||||
});
|
||||
});
|
||||
|
||||
it('should resolve axios.post with response data', async () => {
|
||||
const mockResponse = {
|
||||
status: 201,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
data: { id: 123, created: true }
|
||||
};
|
||||
axios.post.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = vm.evalCode(`
|
||||
(async () => {
|
||||
const response = await axios.post('https://api.example.com/users', { name: 'test' });
|
||||
return response;
|
||||
})()
|
||||
`);
|
||||
const promiseHandle = vm.unwrapResult(result);
|
||||
const resolvedResult = await vm.resolvePromise(promiseHandle);
|
||||
const resolvedHandle = vm.unwrapResult(resolvedResult);
|
||||
const responseData = vm.dump(resolvedHandle);
|
||||
|
||||
resolvedHandle.dispose();
|
||||
promiseHandle.dispose();
|
||||
|
||||
expect(responseData.status).toBe(201);
|
||||
expect(responseData.data).toEqual({ id: 123, created: true });
|
||||
});
|
||||
|
||||
it('should resolve all HTTP methods', async () => {
|
||||
const mockResponse = {
|
||||
status: 200,
|
||||
headers: {},
|
||||
data: { success: true }
|
||||
};
|
||||
|
||||
const methods = ['get', 'post', 'put', 'patch', 'delete'];
|
||||
|
||||
for (const method of methods) {
|
||||
axios[method].mockResolvedValue(mockResponse);
|
||||
|
||||
const result = vm.evalCode(`
|
||||
(async () => {
|
||||
const response = await axios.${method}('https://api.example.com/endpoint');
|
||||
return response.status;
|
||||
})()
|
||||
`);
|
||||
const promiseHandle = vm.unwrapResult(result);
|
||||
const resolvedResult = await vm.resolvePromise(promiseHandle);
|
||||
const resolvedHandle = vm.unwrapResult(resolvedResult);
|
||||
const status = vm.dump(resolvedHandle);
|
||||
|
||||
resolvedHandle.dispose();
|
||||
promiseHandle.dispose();
|
||||
|
||||
expect(status).toBe(200);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling - 4xx/5xx responses', () => {
|
||||
it('should reject on 404 error with full error information', async () => {
|
||||
const mockError = {
|
||||
message: 'Request failed with status code 404',
|
||||
response: {
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
data: { error: 'Resource not found' }
|
||||
},
|
||||
config: {
|
||||
url: 'https://api.example.com/users/999',
|
||||
method: 'get',
|
||||
headers: { Accept: 'application/json' },
|
||||
data: undefined
|
||||
}
|
||||
};
|
||||
axios.get.mockRejectedValue(mockError);
|
||||
|
||||
const result = vm.evalCode(`
|
||||
(async () => {
|
||||
try {
|
||||
await axios.get('https://api.example.com/users/999');
|
||||
return { caught: false };
|
||||
} catch (error) {
|
||||
return {
|
||||
caught: true,
|
||||
message: error.message,
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
responseData: error.response?.data,
|
||||
configUrl: error.config?.url,
|
||||
configMethod: error.config?.method
|
||||
};
|
||||
}
|
||||
})()
|
||||
`);
|
||||
const promiseHandle = vm.unwrapResult(result);
|
||||
const resolvedResult = await vm.resolvePromise(promiseHandle);
|
||||
const resolvedHandle = vm.unwrapResult(resolvedResult);
|
||||
const errorData = vm.dump(resolvedHandle);
|
||||
|
||||
resolvedHandle.dispose();
|
||||
promiseHandle.dispose();
|
||||
|
||||
expect(errorData.caught).toBe(true);
|
||||
expect(errorData.message).toBe('Request failed with status code 404');
|
||||
expect(errorData.status).toBe(404);
|
||||
expect(errorData.statusText).toBe('Not Found');
|
||||
expect(errorData.responseData).toEqual({ error: 'Resource not found' });
|
||||
expect(errorData.configUrl).toBe('https://api.example.com/users/999');
|
||||
expect(errorData.configMethod).toBe('get');
|
||||
});
|
||||
|
||||
it('should reject on 500 error', async () => {
|
||||
const mockError = {
|
||||
message: 'Request failed with status code 500',
|
||||
response: {
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
headers: {},
|
||||
data: { error: 'Server error' }
|
||||
},
|
||||
config: {
|
||||
url: 'https://api.example.com/endpoint',
|
||||
method: 'post',
|
||||
headers: {},
|
||||
data: { test: 'data' }
|
||||
}
|
||||
};
|
||||
axios.post.mockRejectedValue(mockError);
|
||||
|
||||
const result = vm.evalCode(`
|
||||
(async () => {
|
||||
try {
|
||||
await axios.post('https://api.example.com/endpoint', { test: 'data' });
|
||||
return { caught: false };
|
||||
} catch (error) {
|
||||
return {
|
||||
caught: true,
|
||||
status: error.response?.status,
|
||||
message: error.message
|
||||
};
|
||||
}
|
||||
})()
|
||||
`);
|
||||
const promiseHandle = vm.unwrapResult(result);
|
||||
const resolvedResult = await vm.resolvePromise(promiseHandle);
|
||||
const resolvedHandle = vm.unwrapResult(resolvedResult);
|
||||
const errorData = vm.dump(resolvedHandle);
|
||||
|
||||
resolvedHandle.dispose();
|
||||
promiseHandle.dispose();
|
||||
|
||||
expect(errorData.caught).toBe(true);
|
||||
expect(errorData.status).toBe(500);
|
||||
expect(errorData.message).toBe('Request failed with status code 500');
|
||||
});
|
||||
|
||||
it('should reject on 401 unauthorized error', async () => {
|
||||
const mockError = {
|
||||
message: 'Request failed with status code 401',
|
||||
response: {
|
||||
status: 401,
|
||||
statusText: 'Unauthorized',
|
||||
headers: { 'www-authenticate': 'Bearer' },
|
||||
data: { error: 'Invalid token' }
|
||||
},
|
||||
config: {
|
||||
url: 'https://api.example.com/protected',
|
||||
method: 'get',
|
||||
headers: { Authorization: 'Bearer invalid' },
|
||||
data: undefined
|
||||
}
|
||||
};
|
||||
axios.get.mockRejectedValue(mockError);
|
||||
|
||||
const result = vm.evalCode(`
|
||||
(async () => {
|
||||
try {
|
||||
await axios.get('https://api.example.com/protected');
|
||||
return { caught: false };
|
||||
} catch (error) {
|
||||
return {
|
||||
caught: true,
|
||||
status: error.response?.status,
|
||||
responseData: error.response?.data
|
||||
};
|
||||
}
|
||||
})()
|
||||
`);
|
||||
const promiseHandle = vm.unwrapResult(result);
|
||||
const resolvedResult = await vm.resolvePromise(promiseHandle);
|
||||
const resolvedHandle = vm.unwrapResult(resolvedResult);
|
||||
const errorData = vm.dump(resolvedHandle);
|
||||
|
||||
resolvedHandle.dispose();
|
||||
promiseHandle.dispose();
|
||||
|
||||
expect(errorData.caught).toBe(true);
|
||||
expect(errorData.status).toBe(401);
|
||||
expect(errorData.responseData).toEqual({ error: 'Invalid token' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling - network errors', () => {
|
||||
it('should reject on network error without response', async () => {
|
||||
const mockError = {
|
||||
message: 'Network Error',
|
||||
config: {
|
||||
url: 'https://api.example.com/endpoint',
|
||||
method: 'get',
|
||||
headers: {},
|
||||
data: undefined
|
||||
}
|
||||
};
|
||||
axios.get.mockRejectedValue(mockError);
|
||||
|
||||
const result = vm.evalCode(`
|
||||
(async () => {
|
||||
try {
|
||||
await axios.get('https://api.example.com/endpoint');
|
||||
return { caught: false };
|
||||
} catch (error) {
|
||||
return {
|
||||
caught: true,
|
||||
message: error.message,
|
||||
hasResponse: !!error.response,
|
||||
configUrl: error.config?.url
|
||||
};
|
||||
}
|
||||
})()
|
||||
`);
|
||||
const promiseHandle = vm.unwrapResult(result);
|
||||
const resolvedResult = await vm.resolvePromise(promiseHandle);
|
||||
const resolvedHandle = vm.unwrapResult(resolvedResult);
|
||||
const errorData = vm.dump(resolvedHandle);
|
||||
|
||||
resolvedHandle.dispose();
|
||||
promiseHandle.dispose();
|
||||
|
||||
expect(errorData.caught).toBe(true);
|
||||
expect(errorData.message).toBe('Network Error');
|
||||
expect(errorData.hasResponse).toBe(false);
|
||||
expect(errorData.configUrl).toBe('https://api.example.com/endpoint');
|
||||
});
|
||||
|
||||
it('should reject on timeout error', async () => {
|
||||
const mockError = {
|
||||
message: 'timeout of 1000ms exceeded',
|
||||
config: {
|
||||
url: 'https://api.example.com/slow',
|
||||
method: 'get',
|
||||
headers: {},
|
||||
data: undefined
|
||||
}
|
||||
};
|
||||
axios.get.mockRejectedValue(mockError);
|
||||
|
||||
const result = vm.evalCode(`
|
||||
(async () => {
|
||||
try {
|
||||
await axios.get('https://api.example.com/slow');
|
||||
return { caught: false };
|
||||
} catch (error) {
|
||||
return {
|
||||
caught: true,
|
||||
message: error.message
|
||||
};
|
||||
}
|
||||
})()
|
||||
`);
|
||||
const promiseHandle = vm.unwrapResult(result);
|
||||
const resolvedResult = await vm.resolvePromise(promiseHandle);
|
||||
const resolvedHandle = vm.unwrapResult(resolvedResult);
|
||||
const errorData = vm.dump(resolvedHandle);
|
||||
|
||||
resolvedHandle.dispose();
|
||||
promiseHandle.dispose();
|
||||
|
||||
expect(errorData.caught).toBe(true);
|
||||
expect(errorData.message).toBe('timeout of 1000ms exceeded');
|
||||
});
|
||||
});
|
||||
|
||||
describe('base axios function', () => {
|
||||
it('should work with axios() base function', async () => {
|
||||
const mockResponse = {
|
||||
status: 200,
|
||||
headers: {},
|
||||
data: { success: true }
|
||||
};
|
||||
axios.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = vm.evalCode(`
|
||||
(async () => {
|
||||
const response = await axios({
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com/data'
|
||||
});
|
||||
return response;
|
||||
})()
|
||||
`);
|
||||
const promiseHandle = vm.unwrapResult(result);
|
||||
const resolvedResult = await vm.resolvePromise(promiseHandle);
|
||||
const resolvedHandle = vm.unwrapResult(resolvedResult);
|
||||
const responseData = vm.dump(resolvedHandle);
|
||||
|
||||
resolvedHandle.dispose();
|
||||
promiseHandle.dispose();
|
||||
|
||||
expect(responseData.status).toBe(200);
|
||||
expect(responseData.data).toEqual({ success: true });
|
||||
});
|
||||
|
||||
it('should reject on error with axios() base function', async () => {
|
||||
const mockError = {
|
||||
message: 'Request failed with status code 403',
|
||||
response: {
|
||||
status: 403,
|
||||
statusText: 'Forbidden',
|
||||
headers: {},
|
||||
data: { error: 'Access denied' }
|
||||
},
|
||||
config: {
|
||||
url: 'https://api.example.com/forbidden',
|
||||
method: 'get',
|
||||
headers: {},
|
||||
data: undefined
|
||||
}
|
||||
};
|
||||
axios.mockRejectedValue(mockError);
|
||||
|
||||
const result = vm.evalCode(`
|
||||
(async () => {
|
||||
try {
|
||||
await axios({
|
||||
method: 'GET',
|
||||
url: 'https://api.example.com/forbidden'
|
||||
});
|
||||
return { caught: false };
|
||||
} catch (error) {
|
||||
return {
|
||||
caught: true,
|
||||
status: error.response?.status,
|
||||
message: error.message
|
||||
};
|
||||
}
|
||||
})()
|
||||
`);
|
||||
const promiseHandle = vm.unwrapResult(result);
|
||||
const resolvedResult = await vm.resolvePromise(promiseHandle);
|
||||
const resolvedHandle = vm.unwrapResult(resolvedResult);
|
||||
const errorData = vm.dump(resolvedHandle);
|
||||
|
||||
resolvedHandle.dispose();
|
||||
promiseHandle.dispose();
|
||||
|
||||
expect(errorData.caught).toBe(true);
|
||||
expect(errorData.status).toBe(403);
|
||||
expect(errorData.message).toBe('Request failed with status code 403');
|
||||
});
|
||||
});
|
||||
|
||||
describe('real-world use case from issue #6342', () => {
|
||||
it('should properly handle token refresh error with full error info', async () => {
|
||||
const mockError = {
|
||||
message: 'Request failed with status code 404',
|
||||
response: {
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
data: { error: 'Realm not found' }
|
||||
},
|
||||
config: {
|
||||
url: 'https://keycloak.example.com/auth/realms/test/protocol/openid-connect/token',
|
||||
method: 'post',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
data: 'grant_type=password&client_id=test&username=user&password=pass&scope=openid'
|
||||
}
|
||||
};
|
||||
axios.post.mockRejectedValue(mockError);
|
||||
|
||||
const result = vm.evalCode(`
|
||||
(async () => {
|
||||
const url = 'https://keycloak.example.com/auth/realms/test/protocol/openid-connect/token';
|
||||
const data = 'grant_type=password&client_id=test&username=user&password=pass&scope=openid';
|
||||
|
||||
try {
|
||||
const response = await axios.post(url, data, {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||
});
|
||||
return { success: true, token: response.data?.access_token };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
errorMessage: error.message,
|
||||
hasConfig: !!error.config,
|
||||
configUrl: error.config?.url,
|
||||
configMethod: error.config?.method,
|
||||
configData: error.config?.data,
|
||||
responseStatus: error.response?.status,
|
||||
responseData: error.response?.data
|
||||
};
|
||||
}
|
||||
})()
|
||||
`);
|
||||
const promiseHandle = vm.unwrapResult(result);
|
||||
const resolvedResult = await vm.resolvePromise(promiseHandle);
|
||||
const resolvedHandle = vm.unwrapResult(resolvedResult);
|
||||
const result_data = vm.dump(resolvedHandle);
|
||||
|
||||
resolvedHandle.dispose();
|
||||
promiseHandle.dispose();
|
||||
|
||||
expect(result_data.success).toBe(false);
|
||||
expect(result_data.errorMessage).toBe('Request failed with status code 404');
|
||||
expect(result_data.hasConfig).toBe(true);
|
||||
expect(result_data.configUrl).toBe('https://keycloak.example.com/auth/realms/test/protocol/openid-connect/token');
|
||||
expect(result_data.configMethod).toBe('post');
|
||||
expect(result_data.responseStatus).toBe(404);
|
||||
expect(result_data.responseData).toEqual({ error: 'Realm not found' });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -161,13 +161,34 @@ const cleanJson = (data) => {
|
||||
].filter(Boolean);
|
||||
const binaryNames = typedArrays.map((d) => d.name);
|
||||
|
||||
const seen = new WeakSet();
|
||||
|
||||
const replacer = (key, value) => {
|
||||
const isBinary = typedArrays.find((d) => value instanceof d);
|
||||
if (isBinary) {
|
||||
return {
|
||||
__cleanJSONType: isBinary.name,
|
||||
__cleanJSONValue: Buffer.from(value.buffer).toJSON()
|
||||
};
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (seen.has(value)) {
|
||||
return '[Circular Reference]';
|
||||
}
|
||||
seen.add(value);
|
||||
|
||||
// instanceof + [[Class]] cover same-realm; duck-type fallback for cross-realm/cross-context Error-like objects
|
||||
if (value instanceof Error || Object.prototype.toString.call(value) === '[object Error]' || (typeof value.message === 'string' && typeof value.stack === 'string')) {
|
||||
const error = {};
|
||||
// name/message are often on prototype; ensure they're in the output
|
||||
error.name = value.name;
|
||||
error.message = value.message;
|
||||
Object.getOwnPropertyNames(value).forEach((prop) => {
|
||||
error[prop] = value[prop];
|
||||
});
|
||||
return error;
|
||||
}
|
||||
|
||||
const isBinary = typedArrays.find((d) => value instanceof d);
|
||||
if (isBinary) {
|
||||
return {
|
||||
__cleanJSONType: isBinary.name,
|
||||
__cleanJSONValue: Buffer.from(value.buffer).toJSON()
|
||||
};
|
||||
}
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
const { describe, it, expect } = require('@jest/globals');
|
||||
const { evaluateJsExpression, internalExpressionCache: cache, createResponseParser, cleanJson } = require('../src/utils');
|
||||
const {
|
||||
evaluateJsExpression,
|
||||
evaluateJsTemplateLiteral,
|
||||
internalExpressionCache: cache,
|
||||
createResponseParser,
|
||||
cleanJson,
|
||||
cleanCircularJson
|
||||
} = require('../src/utils');
|
||||
|
||||
describe('utils', () => {
|
||||
describe('expression evaluation', () => {
|
||||
@@ -212,5 +219,126 @@ describe('utils', () => {
|
||||
|
||||
expect(cleanJson(input)).toEqual(input);
|
||||
});
|
||||
|
||||
it('replaces circular references with [Circular Reference]', () => {
|
||||
const obj = { a: 1 };
|
||||
obj.self = obj;
|
||||
expect(cleanJson(obj)).toEqual({ a: 1, self: '[Circular Reference]' });
|
||||
});
|
||||
|
||||
it('serializes Error instances with all own properties', () => {
|
||||
const err = new Error('oops');
|
||||
const out = cleanJson(err);
|
||||
expect(out).toMatchObject({ message: 'oops', name: 'Error' });
|
||||
expect(typeof out.stack).toBe('string');
|
||||
});
|
||||
|
||||
it('serializes Error with extra own properties (code, cause)', () => {
|
||||
const err = new Error('failed');
|
||||
err.code = 'ERR_FAILED';
|
||||
err.cause = new Error('root cause');
|
||||
const out = cleanJson(err);
|
||||
expect(out.message).toBe('failed');
|
||||
expect(out.code).toBe('ERR_FAILED');
|
||||
expect(out.cause).toMatchObject({ message: 'root cause', name: 'Error' });
|
||||
expect(typeof out.cause.stack).toBe('string');
|
||||
});
|
||||
|
||||
it('serializes Error subclasses', () => {
|
||||
const err = new TypeError('type oops');
|
||||
const out = cleanJson(err);
|
||||
expect(out).toMatchObject({ message: 'type oops', name: 'TypeError' });
|
||||
expect(typeof out.stack).toBe('string');
|
||||
});
|
||||
|
||||
it('serializes duck-typed error-like objects (message + stack strings)', () => {
|
||||
const fake = { message: 'fake', stack: 'at line 1' };
|
||||
const out = cleanJson(fake);
|
||||
expect(out).toEqual(fake);
|
||||
});
|
||||
|
||||
it('does not treat plain objects with non-string message/stack as errors', () => {
|
||||
const notError = { message: 123, stack: 'at line 1' };
|
||||
const out = cleanJson(notError);
|
||||
expect(out).toEqual(notError);
|
||||
const notError2 = { message: 'ok', stack: 456 };
|
||||
const out2 = cleanJson(notError2);
|
||||
expect(out2).toEqual(notError2);
|
||||
});
|
||||
|
||||
it('serializes nested Error inside object', () => {
|
||||
const input = { err: new Error('nested'), id: 1 };
|
||||
const out = cleanJson(input);
|
||||
expect(out.id).toBe(1);
|
||||
expect(out.err).toMatchObject({ message: 'nested', name: 'Error' });
|
||||
expect(typeof out.err.stack).toBe('string');
|
||||
});
|
||||
|
||||
it('handles circular ref and Error in same structure', () => {
|
||||
const err = new Error('cycle');
|
||||
const obj = { err, ref: null };
|
||||
obj.ref = obj;
|
||||
const out = cleanJson(obj);
|
||||
expect(out.err).toMatchObject({ message: 'cycle' });
|
||||
expect(out.ref).toBe('[Circular Reference]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanCircularJson', () => {
|
||||
it('returns primitives and plain objects as-is', () => {
|
||||
expect(cleanCircularJson(1)).toBe(1);
|
||||
expect(cleanCircularJson('x')).toBe('x');
|
||||
expect(cleanCircularJson({ a: 1 })).toEqual({ a: 1 });
|
||||
});
|
||||
|
||||
it('replaces circular references with [Circular Reference]', () => {
|
||||
const obj = { a: 1 };
|
||||
obj.self = obj;
|
||||
expect(cleanCircularJson(obj)).toEqual({ a: 1, self: '[Circular Reference]' });
|
||||
});
|
||||
|
||||
it('handles deeply nested circular ref', () => {
|
||||
const obj = { level: 1, child: null };
|
||||
obj.child = { level: 2, back: obj };
|
||||
const out = cleanCircularJson(obj);
|
||||
expect(out.level).toBe(1);
|
||||
expect(out.child.level).toBe(2);
|
||||
expect(out.child.back).toBe('[Circular Reference]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluateJsTemplateLiteral', () => {
|
||||
it('returns non-string or empty input as-is', () => {
|
||||
expect(evaluateJsTemplateLiteral(null)).toBe(null);
|
||||
expect(evaluateJsTemplateLiteral('')).toBe('');
|
||||
expect(evaluateJsTemplateLiteral(42)).toBe(42);
|
||||
});
|
||||
|
||||
it('parses boolean and null literals', () => {
|
||||
expect(evaluateJsTemplateLiteral('true')).toBe(true);
|
||||
expect(evaluateJsTemplateLiteral('false')).toBe(false);
|
||||
expect(evaluateJsTemplateLiteral('null')).toBe(null);
|
||||
expect(evaluateJsTemplateLiteral('undefined')).toBe(undefined);
|
||||
});
|
||||
|
||||
it('parses quoted strings', () => {
|
||||
expect(evaluateJsTemplateLiteral('"hello"')).toBe('hello');
|
||||
expect(evaluateJsTemplateLiteral('\'world\'')).toBe('world');
|
||||
});
|
||||
|
||||
it('parses numbers', () => {
|
||||
expect(evaluateJsTemplateLiteral('42')).toBe(42);
|
||||
expect(evaluateJsTemplateLiteral('3.14')).toBe(3.14);
|
||||
});
|
||||
|
||||
it('evaluates template literal with context', () => {
|
||||
const context = { res: { data: { name: 'Bruno' } } };
|
||||
expect(evaluateJsTemplateLiteral('${res.data.name}', context)).toBe('Bruno');
|
||||
});
|
||||
|
||||
it('keeps large numbers as string (safe integer limit)', () => {
|
||||
const big = '9007199254740993';
|
||||
expect(evaluateJsTemplateLiteral(big)).toBe(big);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user