Files
bruno/packages/bruno-requests/src/scripting/scripted-entry.spec.ts

233 lines
8.0 KiB
TypeScript

// Network behavior of sendRequest lives in send-request.spec.ts.
import { createSendRequest, buildScriptedEntry } from './send-request';
jest.mock('../network', () => ({
makeAxiosInstance: jest.fn()
}));
jest.mock('../utils/http-https-agents', () => ({
getHttpHttpsAgents: jest.fn()
}));
import { makeAxiosInstance } from '../network';
import { getHttpHttpsAgents } from '../utils/http-https-agents';
const mockMakeAxiosInstance = makeAxiosInstance as jest.Mock;
const mockGetHttpHttpsAgents = getHttpHttpsAgents as jest.Mock;
describe('buildScriptedEntry', () => {
test('normalizes method to upper case and preserves request fields', () => {
const entry = buildScriptedEntry({
request: { method: 'get', url: 'https://example.com', headers: { 'x-a': '1' }, data: undefined },
response: null,
error: null,
startedAt: 1000,
completedAt: 1042
});
expect(entry.request.method).toBe('GET');
expect(entry.request.url).toBe('https://example.com');
expect(entry.request.headers).toEqual({ 'x-a': '1' });
expect(entry.response).toBeNull();
expect(entry.error).toBeNull();
expect(entry.startedAt).toBe(1000);
expect(entry.completedAt).toBe(1042);
});
test('defaults method to GET when not provided', () => {
const entry = buildScriptedEntry({
request: { url: 'https://example.com' },
response: null,
error: null,
startedAt: 0,
completedAt: 0
});
expect(entry.request.method).toBe('GET');
});
test('flattens AxiosHeaders-like objects via toJSON for both request and response', () => {
const headersLike = {
toJSON: () => ({ 'content-type': 'application/json', 'x-trace': 'abc' })
};
const entry = buildScriptedEntry({
request: { method: 'post', url: 'https://example.com', headers: headersLike, data: { hi: 1 } },
response: { status: 200, statusText: 'OK', headers: headersLike, data: { ok: true } },
error: null,
startedAt: 0,
completedAt: 10
});
expect(entry.request.headers).toEqual({ 'content-type': 'application/json', 'x-trace': 'abc' });
expect(entry.response?.headers).toEqual({ 'content-type': 'application/json', 'x-trace': 'abc' });
});
test('encodes string body to base64 dataBuffer and derives size/duration when not supplied', () => {
const entry = buildScriptedEntry({
request: { method: 'GET', url: 'https://example.com' },
response: { status: 200, statusText: 'OK', headers: {}, data: 'hello' },
error: null,
startedAt: 5,
completedAt: 15
});
expect(entry.response?.dataBuffer).toBe(Buffer.from('hello').toString('base64'));
expect(entry.response?.size).toBe(Buffer.from('hello').length);
expect(entry.response?.duration).toBe(10);
});
test('JSON-stringifies object body for dataBuffer when not provided', () => {
const body = { foo: 'bar' };
const entry = buildScriptedEntry({
request: { method: 'GET', url: 'https://example.com' },
response: { status: 201, statusText: 'Created', headers: {}, data: body },
error: null,
startedAt: 0,
completedAt: 0
});
expect(entry.response?.dataBuffer).toBe(Buffer.from(JSON.stringify(body)).toString('base64'));
});
test('honors explicit dataBuffer / size / duration on response', () => {
const explicitBuffer = Buffer.from('payload').toString('base64');
const entry = buildScriptedEntry({
request: { method: 'GET', url: 'https://example.com' },
response: {
status: 200,
statusText: 'OK',
headers: {},
data: 'ignored-for-size',
dataBuffer: explicitBuffer,
size: 999,
duration: 123
},
error: null,
startedAt: 0,
completedAt: 50
});
expect(entry.response?.dataBuffer).toBe(explicitBuffer);
expect(entry.response?.size).toBe(999);
expect(entry.response?.duration).toBe(123);
});
test('maps error to { message, code } and leaves response null when absent', () => {
const err = Object.assign(new Error('boom'), { code: 'ECONNREFUSED' });
const entry = buildScriptedEntry({
request: { method: 'GET', url: 'https://example.com' },
response: null,
error: err,
startedAt: 0,
completedAt: 0
});
expect(entry.response).toBeNull();
expect(entry.error).toEqual({ message: 'boom', code: 'ECONNREFUSED' });
});
});
describe('createSendRequest onComplete', () => {
let mockAxios: jest.Mock;
beforeEach(() => {
jest.clearAllMocks();
mockAxios = jest.fn();
mockMakeAxiosInstance.mockReturnValue(mockAxios);
mockGetHttpHttpsAgents.mockResolvedValue({ httpAgent: null, httpsAgent: null });
});
test('fires once with the entry on a successful no-callback call', async () => {
mockAxios.mockResolvedValue({
status: 200,
statusText: 'OK',
headers: { 'content-type': 'text/plain' },
data: 'pong'
});
const onComplete = jest.fn();
const send = createSendRequest(undefined, { onComplete });
await send({ method: 'get', url: 'https://example.com/ping' });
expect(onComplete).toHaveBeenCalledTimes(1);
const entry = onComplete.mock.calls[0][0];
expect(entry.request).toEqual(
expect.objectContaining({ method: 'GET', url: 'https://example.com/ping' })
);
expect(entry.response).toEqual(
expect.objectContaining({
statusCode: 200,
statusText: 'OK',
headers: { 'content-type': 'text/plain' }
})
);
expect(entry.error).toBeNull();
});
test('records the response carried by a 4xx/5xx axios error', async () => {
const axiosError: any = new Error('Request failed with status code 404');
axiosError.response = {
status: 404,
statusText: 'Not Found',
headers: {},
data: 'missing'
};
mockAxios.mockRejectedValue(axiosError);
const onComplete = jest.fn();
const send = createSendRequest(undefined, { onComplete });
await expect(send({ url: 'https://example.com/missing' })).rejects.toBe(axiosError);
expect(onComplete).toHaveBeenCalledTimes(1);
const entry = onComplete.mock.calls[0][0];
expect(entry.response).toEqual(
expect.objectContaining({ statusCode: 404, statusText: 'Not Found' })
);
expect(entry.error?.message).toContain('404');
});
test('records error with null response on a pure network failure', async () => {
const netErr = Object.assign(new Error('ECONNREFUSED'), { code: 'ECONNREFUSED' });
mockAxios.mockRejectedValue(netErr);
const onComplete = jest.fn();
const send = createSendRequest(undefined, { onComplete });
await expect(send({ url: 'https://nope.invalid' })).rejects.toBe(netErr);
expect(onComplete).toHaveBeenCalledTimes(1);
const entry = onComplete.mock.calls[0][0];
expect(entry.response).toBeNull();
expect(entry.error).toEqual({ message: 'ECONNREFUSED', code: 'ECONNREFUSED' });
});
test('fires exactly once even when a callback is provided', async () => {
mockAxios.mockResolvedValue({ status: 200, statusText: 'OK', headers: {}, data: 'ok' });
const onComplete = jest.fn();
const callback = jest.fn();
const send = createSendRequest(undefined, { onComplete });
await send({ url: 'https://example.com' }, callback);
expect(callback).toHaveBeenCalledTimes(1);
expect(onComplete).toHaveBeenCalledTimes(1);
});
test('a throwing onComplete does not break the request result', async () => {
const mockResponse = { status: 200, statusText: 'OK', headers: {}, data: 'ok' };
mockAxios.mockResolvedValue(mockResponse);
const onComplete = jest.fn(() => { throw new Error('sink blew up'); });
const send = createSendRequest(undefined, { onComplete });
await expect(send({ url: 'https://example.com' })).resolves.toBe(mockResponse);
expect(onComplete).toHaveBeenCalledTimes(1);
});
test('does nothing when no onComplete is provided', async () => {
mockAxios.mockResolvedValue({ status: 200, statusText: 'OK', headers: {}, data: 'ok' });
const send = createSendRequest();
await expect(send({ url: 'https://example.com' })).resolves.toBeDefined();
});
});