Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions packages/bruno-js/src/bruno-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ class BrunoRequest {
* It must be noted that the user cannot set these properties directly.
* They should use the respective setter methods to set these properties.
*/
constructor(req) {
constructor(req, { interpolate } = {}) {
this.req = req;
this.__interpolate = interpolate;
this.url = req.url;
this.method = req.method;
this.headers = req.headers;
Expand Down Expand Up @@ -48,7 +49,7 @@ class BrunoRequest {

getHost() {
try {
const url = new URL(this.req.url);
const url = new URL(this.__getInterpolatedUrl());
return url.host;
} catch (e) {
return '';
Expand All @@ -57,7 +58,7 @@ class BrunoRequest {

getPath() {
try {
const url = new URL(this.req.url);
const url = new URL(this.__getInterpolatedUrl());
let pathname = url.pathname;

// If path params exist, interpolate them into the pathname
Expand Down Expand Up @@ -91,7 +92,7 @@ class BrunoRequest {

getQueryString() {
try {
const url = new URL(this.req.url);
const url = new URL(this.__getInterpolatedUrl());
// Return query string without the leading '?'
return url.search ? url.search.substring(1) : '';
} catch (e) {
Expand Down Expand Up @@ -246,6 +247,10 @@ class BrunoRequest {
}
}

__getInterpolatedUrl() {
return this.__interpolate ? this.__interpolate(this.req.url) : this.req.url;
}

__safeParseJSON(str) {
try {
return JSON.parse(str);
Expand Down
4 changes: 3 additions & 1 deletion packages/bruno-js/src/runtime/script-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ class ScriptRuntime {
certsAndProxyConfig,
requestUrl: request?.url
});
const req = new BrunoRequest(request);
// Pre-request scripts run before the request is interpolated, so the URL helpers
// (getHost, getPath, getQueryString) resolve variables themselves
const req = new BrunoRequest(request, { interpolate: bru.interpolate });

// extend bru with result getter methods
const { __brunoTestResults, test, waitForPendingTests } = createBruTestResultMethods(bru, assertionResults, chai);
Expand Down
95 changes: 95 additions & 0 deletions packages/bruno-js/tests/bruno-request-url-helpers.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
const { describe, it, expect } = require('@jest/globals');
const { interpolate } = require('@usebruno/common');
const BrunoRequest = require('../src/bruno-request');

const makeReq = (overrides = {}) => ({
url: 'https://example.com/api',
method: 'GET',
headers: {},
data: undefined,
...overrides
});

const createInterpolator = (vars) => (str) => interpolate(str, vars);

describe('BrunoRequest - URL helpers', () => {
describe('without an interpolator', () => {
it('parses a literal URL', () => {
const req = new BrunoRequest(makeReq({ url: 'https://example.com:8080/api/users?page=2' }));

expect(req.getHost()).toBe('example.com:8080');
expect(req.getPath()).toBe('/api/users');
expect(req.getQueryString()).toBe('page=2');
});
});

describe('with an interpolator', () => {
it('resolves a host variable that includes the scheme', () => {
const req = new BrunoRequest(makeReq({ url: '{{HOST}}/test' }), {
interpolate: createInterpolator({ HOST: 'https://example.com' })
});

expect(req.getHost()).toBe('example.com');
});

it('resolves a host variable that follows a literal scheme without lowercasing the variable name', () => {
const req = new BrunoRequest(makeReq({ url: 'https://{{HOST}}/test' }), {
interpolate: createInterpolator({ HOST: 'example.com' })
});

expect(req.getHost()).toBe('example.com');
});

it('resolves host and port variables', () => {
const req = new BrunoRequest(makeReq({ url: 'https://{{HOST}}:{{PORT}}/test' }), {
interpolate: createInterpolator({ HOST: 'example.com', PORT: '8080' })
});

expect(req.getHost()).toBe('example.com:8080');
});

it('resolves variables in getPath() and getQueryString()', () => {
const req = new BrunoRequest(
makeReq({
url: '{{baseUrl}}/users/:userId?name={{name}}&age=30',
pathParams: [{ name: 'userId', value: '123', type: 'path' }]
}),
{ interpolate: createInterpolator({ baseUrl: 'https://example.com/api', name: 'john' }) }
);

expect(req.getPath()).toBe('/api/users/123');
expect(req.getQueryString()).toBe('name=john&age=30');
});

it('uses the variable values at call time', () => {
const vars = { HOST: 'https://example.com' };
const req = new BrunoRequest(makeReq({ url: '{{HOST}}/test' }), {
interpolate: createInterpolator(vars)
});

vars.HOST = 'https://api.example.com';

expect(req.getHost()).toBe('api.example.com');
});

it('leaves the raw URL untouched', () => {
const rawReq = makeReq({ url: '{{HOST}}/test' });
const req = new BrunoRequest(rawReq, {
interpolate: createInterpolator({ HOST: 'https://example.com' })
});

req.getHost();

expect(req.getUrl()).toBe('{{HOST}}/test');
expect(rawReq.url).toBe('{{HOST}}/test');
});

it('returns an empty string when the resolved URL is not parseable', () => {
const req = new BrunoRequest(makeReq({ url: '{{HOST}}/test' }), {
interpolate: createInterpolator({})
});

expect(req.getHost()).toBe('');
});
});
});
41 changes: 41 additions & 0 deletions packages/bruno-js/tests/runtime.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,47 @@ describe('runtime', () => {
expect(result.envVariables.environmentToken).toBe('after');
expect(result.globalEnvironmentVariables.globalToken).toBe('after');
});

describe.each(['nodevm', 'quickjs'])('URL helpers with variables (%s)', (runtimeName) => {
const onConsoleLog = () => {};

beforeAll(async () => {
if (runtimeName === 'quickjs') {
await quickJsLoader();
}
});

it('should resolve variables in req.getHost(), req.getPath() and req.getQueryString()', async () => {
const script = `
bru.setVar('resolvedHost', req.getHost());
bru.setVar('resolvedPath', req.getPath());
bru.setVar('resolvedQueryString', req.getQueryString());
`;
const request = { ...baseRequest, url: '{{HOST}}/test?page=1' };
const envVariables = { HOST: 'https://example.com' };
const runtime = new ScriptRuntime({ runtime: runtimeName });

const result = await runtime.runRequestScript(script, request, envVariables, {}, '.', onConsoleLog, process.env);

expect(result.runtimeVariables.resolvedHost).toBe('example.com');
expect(result.runtimeVariables.resolvedPath).toBe('/test');
expect(result.runtimeVariables.resolvedQueryString).toBe('page=1');
expect(request.url).toBe('{{HOST}}/test?page=1');
});

it('should resolve variables set earlier in the same script', async () => {
const script = `
bru.setVar('HOST', 'https://api.example.com');
bru.setVar('resolvedHost', req.getHost());
`;
const request = { ...baseRequest, url: '{{HOST}}/test' };
const runtime = new ScriptRuntime({ runtime: runtimeName });

const result = await runtime.runRequestScript(script, request, {}, {}, '.', onConsoleLog, process.env);

expect(result.runtimeVariables.resolvedHost).toBe('api.example.com');
});
});
});

describe('run-response-script', () => {
Expand Down