Files
bruno/packages/bruno-lang/v2/tests/annotations.spec.js
Sid 0bba66a590 feat: variable descriptions (#8269)
* feat: add description column to various tables and enhance UI interactions
- Introduced a description column in Headers, VarsTable, and other components to allow multi-line descriptions.
- Added toggle buttons to show/hide the description column in relevant tables.
- Updated EditableTable component to support dynamic row addition with customizable labels.
- Enhanced UI for better user experience with consistent button styles and spacing adjustments.

* chore: resize fix handles

* fix: restrict column check for last index

* refactor: remove unwanted style and fix bottom padding of the last child in the table row

* tests: improve testability and fix impacted locators

* tests: fix locators for value fields

* fix: improve newline handling in description prefix

* fix: ts changes

* chore: remove redundant code and fix description roundtrip

* chore: remove redundant check

* tests(bru-lang): tests for description and annotation

* test: improve locators and tests

* tests: descriptions

* chore: re-add description setup

* fix: re-add description cols

* chore: fix the double click reset

* fix: account for hidden sidebar in request pane width calculation

* refactor: ui/ux fixes for data type toggle

* chore: inline common deps into bruno-lang and bruno-schema

* chore: tests

* chore: fix ux for descriptions

* fix: layout for environment vars table

* fix: ensure correct row selection for multipart file upload in tests

* feat: enhance UID assignment for request headers and variables in collections

* chore: simplify

* chore: update pkg

* chore: abstract the e2e utils

* chore: fix exports

* tests: fix locators for datatype vars

* fix: ux issue with secrets in environment table

* tests: update locators for collection headers and vars descriptions

* tests: update locators to use data attributes for datatype selectors

* chore: update default css width for actions column

* chore: remove tooltip for string type warnings

* fix: reduce name widths

* refactor: update annotation serialization to use single-quote delimiters for descriptions

* refactor(tests): simplify description formatting in jsonToEnv tests

* feat: add multiline description escaping and unescaping functions with tests

* refactor: clean up imports and improve multiline text block handling

* refactor: update locators to use new test IDs for environment variable editors

* refactor: streamline header input handling and improve environment variable row access

* refactor: update e2e-test job to support self-hosted runners

* refactor: update E2E test runner configuration to include e2e and macOS environments

---------

Co-authored-by: Pragadesh-45 <temporaryg7904@gmail.com>
2026-07-07 21:43:39 +05:30

1615 lines
47 KiB
JavaScript

const parser = require('../src/bruToJson');
const jsonToBru = require('../src/jsonToBru');
const envParser = require('../src/envToJson');
const jsonToEnv = require('../src/jsonToEnv');
const collectionParser = require('../src/collectionBruToJson');
const jsonToCollectionBru = require('../src/jsonToCollectionBru');
const fs = require('fs');
const path = require('path');
describe('pair annotations', () => {
it('above-line annotations in asserts', () => {
const input = `
assert {
@description('hello')
res.status: eq 200
}
`;
const output = parser(input);
expect(output.assertions).toEqual([
{
name: 'res.status',
value: 'eq 200',
enabled: true,
annotations: [{ name: 'description', value: 'hello' }],
description: 'hello'
}
]);
});
it('above-line annotation', () => {
const input = `
headers {
@description('hello')
key: value
}
`;
const output = parser(input);
expect(output.headers).toEqual([
{
name: 'key',
value: 'value',
enabled: true,
annotations: [{ name: 'description', value: 'hello' }],
description: 'hello'
}
]);
});
it('annotation without args', () => {
const input = `
headers {
@string
key: value
}
`;
const output = parser(input);
expect(output.headers).toEqual([
{ name: 'key', value: 'value', enabled: true, annotations: [{ name: 'string' }] }
]);
});
it('multiple above-line annotations on same pair', () => {
const input = `
headers {
@string
@description('x')
key: value
}
`;
const output = parser(input);
expect(output.headers).toEqual([
{
name: 'key',
value: 'value',
enabled: true,
annotations: [{ name: 'string' }, { name: 'description', value: 'x' }],
description: 'x'
}
]);
});
it('multiple above-line annotations', () => {
const input = `
headers {
@string
@description('hello')
key: value
}
`;
const output = parser(input);
expect(output.headers).toEqual([
{
name: 'key',
value: 'value',
enabled: true,
annotations: [{ name: 'string' }, { name: 'description', value: 'hello' }],
description: 'hello'
}
]);
});
it('no annotation — output unchanged (backward compat)', () => {
const input = `
headers {
key: value
}
`;
const output = parser(input);
expect(output.headers).toEqual([{ name: 'key', value: 'value', enabled: true }]);
expect(output.headers[0]).not.toHaveProperty('annotations');
});
it('disabled pair with annotation', () => {
const input = `
headers {
@string
~key: value
}
`;
const output = parser(input);
expect(output.headers).toEqual([
{ name: 'key', value: 'value', enabled: false, annotations: [{ name: 'string' }] }
]);
});
it('double-quoted annotation arg', () => {
const input = `
headers {
@description("hello")
key: value
}
`;
const output = parser(input);
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: 'hello' }]);
});
it('single quote inside double-quoted annotation arg (e.g. O\'Reilly)', () => {
const input = `
headers {
@description("O'Reilly")
key: value
}
`;
const output = parser(input);
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: 'O\'Reilly' }]);
});
it('double quote inside single-quoted annotation arg (e.g. say "hello")', () => {
const input = `
headers {
@description('say "hello"')
key: value
}
`;
const output = parser(input);
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: 'say "hello"' }]);
});
it('smoke test escaping special characters', () => {
const input = fs.readFileSync(path.join(__dirname, './fixtures/annotations.bru'));
const output = parser(input);
expect(output.vars.req[0].annotations).toEqual([{ name: 'description', value: 'found in C:\\Users\\File\\Path' }]);
expect(output.vars.req[1].annotations).toEqual([{ name: 'description', value: 'height of 2\' ' }]);
});
it('unquoted annotation arg', () => {
const input = `
headers {
@version(2)
key: value
}
`;
const output = parser(input);
expect(output.headers[0].annotations).toEqual([{ name: 'version', value: '2' }]);
});
it('float (decimal) unquoted annotation arg', () => {
const input = `
headers {
@version(3.14)
key: value
}
`;
const output = parser(input);
expect(output.headers[0].annotations).toEqual([{ name: 'version', value: '3.14' }]);
});
it('empty string arg', () => {
const input = `
headers {
@description('')
key: value
}
`;
const output = parser(input);
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: '' }]);
});
it('whitespace-only string arg preserves spaces', () => {
const input = `
headers {
@description(' ')
key: value
}
`;
const output = parser(input);
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: ' ' }]);
});
it('leading and trailing whitespace in string arg is preserved', () => {
const input = `
headers {
@description(' hello ')
key: value
}
`;
const output = parser(input);
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: ' hello ' }]);
});
it('unicode characters in annotation arg', () => {
const input = `
headers {
@description('日本語')
key: value
}
`;
const output = parser(input);
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: '日本語' }]);
});
it('URL with query string in annotation arg', () => {
const input = `
headers {
@description('https://example.com/path?q=1&r=2#anchor')
key: value
}
`;
const output = parser(input);
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: 'https://example.com/path?q=1&r=2#anchor' }]);
});
it('colon inside annotation arg value', () => {
const input = `
headers {
@description('Content-Type: application/json')
key: value
}
`;
const output = parser(input);
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: 'Content-Type: application/json' }]);
});
it('email address (@ symbol) inside annotation arg value', () => {
const input = `
headers {
@description('user@example.com')
key: value
}
`;
const output = parser(input);
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: 'user@example.com' }]);
});
it('template variable syntax inside annotation arg value', () => {
const input = `
headers {
@description('{{baseUrl}}/endpoint')
key: value
}
`;
const output = parser(input);
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: '{{baseUrl}}/endpoint' }]);
});
it('tab character inside annotation arg value', () => {
const input = `headers {\n @description('col1\tcol2')\n key: value\n}\n`;
const output = parser(input);
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: 'col1\tcol2' }]);
});
it('multiline string values', () => {
const input = `headers {
@description('''
make it rain
make it rain2
''')
key: value
}`;
const output = parser(input);
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: 'make it rain\nmake it rain2' }]);
});
it('serializeAnnotations — multiline value uses triple-quote delimiters and roundtrips correctly', () => {
const json = {
meta: { name: 'test', type: 'http', seq: 1 },
http: { method: 'get', url: 'https://example.com' },
headers: [{ name: 'x-key', value: 'val', enabled: true, annotations: [{ name: 'description', value: 'line one\nline two' }] }]
};
const bru = jsonToBru(json);
expect(bru).toContain('@description(\'\'\'\n line one\n line two\n \'\'\')\n x-key: val'); const parsed = parser(bru);
expect(parsed.headers[0].annotations).toEqual([{ name: 'description', value: 'line one\nline two' }]);
});
it('serializeAnnotations — multiline value with embedded triple quotes roundtrips correctly', () => {
const json = {
meta: { name: 'test', type: 'http', seq: 1 },
http: { method: 'get', url: 'https://example.com' },
headers: [{
name: 'x-key',
value: 'val',
enabled: true,
annotations: [{ name: 'description', value: 'line one\nline two with \'\'\' inside\nline three' }]
}]
};
const bru = jsonToBru(json);
expect(bru).toContain('@description(\'\'\'\n line one\n line two with \\\'\\\'\\\' inside\n line three\n \'\'\')');
const parsed = parser(bru);
expect(parsed.headers[0].description).toBe('line one\nline two with \'\'\' inside\nline three');
});
it('serializeAnnotations — multiline value with backslash-quote and triple quotes roundtrips correctly', () => {
const json = {
meta: { name: 'test', type: 'http', seq: 1 },
http: { method: 'get', url: 'https://example.com' },
headers: [{
name: 'x-key',
value: 'val',
enabled: true,
annotations: [{ name: 'description', value: 'it\\\'s multiline\nand has \'\'\' too' }]
}]
};
const bru = jsonToBru(json);
const parsed = parser(bru);
expect(parsed.headers[0].description).toBe('it\\\'s multiline\nand has \'\'\' too');
});
it('serializeAnnotations — empty string value roundtrips correctly', () => {
const json = {
meta: { name: 'test', type: 'http', seq: 1 },
http: { method: 'get', url: 'https://example.com' },
headers: [{ name: 'x-key', value: 'val', enabled: true, annotations: [{ name: 'description', value: '' }] }]
};
const bru = jsonToBru(json);
const parsed = parser(bru);
expect(parsed.headers[0].annotations).toEqual([{ name: 'description', value: '' }]);
});
it('serializeAnnotations — URL with special chars uses single-quote delimiters', () => {
const json = {
meta: { name: 'test', type: 'http', seq: 1 },
http: { method: 'get', url: 'https://example.com' },
headers: [{ name: 'x-key', value: 'val', enabled: true, annotations: [{ name: 'description', value: 'https://example.com?q=1&r=2' }] }]
};
const bru = jsonToBru(json);
expect(bru).toContain('@description(\'https://example.com?q=1&r=2\')\n x-key: val');
});
it('serializeAnnotations — template variable in value roundtrips correctly', () => {
const json = {
meta: { name: 'test', type: 'http', seq: 1 },
http: { method: 'get', url: 'https://example.com' },
headers: [{ name: 'x-key', value: 'val', enabled: true, annotations: [{ name: 'description', value: '{{baseUrl}}/path' }] }]
};
const bru = jsonToBru(json);
const parsed = parser(bru);
expect(parsed.headers[0].annotations).toEqual([{ name: 'description', value: '{{baseUrl}}/path' }]);
});
it('serializeAnnotations — annotation on params:path', () => {
const json = {
params: [{ name: 'userId', value: '123', enabled: true, type: 'path', annotations: [{ name: 'description', value: 'user id' }] }]
};
const bru = jsonToBru(json);
expect(bru).toContain('params:path {');
expect(bru).toContain('@description(\'user id\')\n userId: 123');
});
it('serializeAnnotations — annotation on metadata', () => {
const json = {
metadata: [{ name: 'trace-id', value: 'abc123', enabled: true, annotations: [{ name: 'description', value: 'trace id' }] }]
};
const bru = jsonToBru(json);
expect(bru).toContain('metadata {');
expect(bru).toContain('@description(\'trace id\')\n trace-id: abc123');
});
it('serializeAnnotations — annotation on body:form-urlencoded', () => {
const json = {
body: {
formUrlEncoded: [{ name: 'username', value: 'alice', enabled: true, annotations: [{ name: 'description', value: 'username field' }] }]
}
};
const bru = jsonToBru(json);
expect(bru).toContain('body:form-urlencoded {');
expect(bru).toContain('@description(\'username field\')\n username: alice');
});
it('annotation on params:query block', () => {
const input = `
params:query {
@string
q: search
}
`;
const output = parser(input);
expect(output.params).toEqual([
{ name: 'q', value: 'search', enabled: true, type: 'query', annotations: [{ name: 'string' }] }
]);
});
it('annotation on params:path block', () => {
const input = `
params:path {
@description('user id')
userId: 123
}
`;
const output = parser(input);
expect(output.params).toEqual([
{
name: 'userId',
value: '123',
enabled: true,
type: 'path',
annotations: [{ name: 'description', value: 'user id' }],
description: 'user id'
}
]);
});
it('annotation on metadata block', () => {
const input = `
metadata {
@description('trace id')
trace-id: abc123
}
`;
const output = parser(input);
expect(output.metadata).toEqual([
{
name: 'trace-id',
value: 'abc123',
enabled: true,
annotations: [{ name: 'description', value: 'trace id' }],
description: 'trace id'
}
]);
});
it('annotation on body:form-urlencoded block', () => {
const input = `
body:form-urlencoded {
@description('username field')
username: alice
}
`;
const output = parser(input);
expect(output.body.formUrlEncoded).toEqual([
{
name: 'username',
value: 'alice',
enabled: true,
annotations: [{ name: 'description', value: 'username field' }],
description: 'username field'
}
]);
});
it('annotation on vars:pre-request block', () => {
const input = `
vars:pre-request {
@description('base url')
myVar: http://localhost
}
`;
const output = parser(input);
expect(output.vars.req).toEqual([
{
name: 'myVar',
value: 'http://localhost',
enabled: true,
local: false,
annotations: [{ name: 'description', value: 'base url' }],
description: 'base url'
}
]);
});
it('annotation on vars:post-response block', () => {
const input = `
vars:post-response {
@description('auth token')
token: abc123
}
`;
const output = parser(input);
expect(output.vars.res).toEqual([
{
name: 'token',
value: 'abc123',
enabled: true,
local: false,
annotations: [{ name: 'description', value: 'auth token' }],
description: 'auth token'
}
]);
});
it('annotation on local vars:pre-request pair', () => {
const input = `
vars:pre-request {
@description('local base url')
@BASE_URL: http://localhost
}
`;
const output = parser(input);
expect(output.vars.req).toEqual([
{
name: 'BASE_URL',
value: 'http://localhost',
enabled: true,
local: true,
annotations: [{ name: 'description', value: 'local base url' }],
description: 'local base url'
}
]);
});
it('annotation on local vars:post-response pair', () => {
const input = `
vars:post-response {
@description('local token')
@token: abc123
}
`;
const output = parser(input);
expect(output.vars.res).toEqual([
{
name: 'token',
value: 'abc123',
enabled: true,
local: true,
annotations: [{ name: 'description', value: 'local token' }],
description: 'local token'
}
]);
});
it('annotation on body:multipart-form text field', () => {
const input = `
body:multipart-form {
@description('plain field')
field: value @contentType(text/plain)
}
`;
const output = parser(input);
expect(output.body.multipartForm).toEqual([
{
name: 'field',
value: 'value',
enabled: true,
type: 'text',
contentType: 'text/plain',
annotations: [{ name: 'description', value: 'plain field' }],
description: 'plain field'
}
]);
});
it('annotation on body:multipart-form file field', () => {
const input = `
body:multipart-form {
@description('upload image')
upload: @file(/tmp/a.png|/tmp/b.png) @contentType(image/png)
}
`;
const output = parser(input);
expect(output.body.multipartForm).toEqual([
{
name: 'upload',
value: ['/tmp/a.png', '/tmp/b.png'],
enabled: true,
type: 'file',
contentType: 'image/png',
annotations: [{ name: 'description', value: 'upload image' }],
description: 'upload image'
}
]);
});
it('annotation on body:file', () => {
const input = `
body:file {
@description('upload doc')
file: @file(/tmp/readme.pdf) @contentType(application/pdf)
}
`;
const output = parser(input);
expect(output.body.file).toEqual([
{
filePath: '/tmp/readme.pdf',
selected: true,
contentType: 'application/pdf',
annotations: [{ name: 'description', value: 'upload doc' }],
description: 'upload doc'
}
]);
});
it('serializeAnnotations — multipart text field with contentType', () => {
const json = {
body: {
multipartForm: [
{
name: 'field',
value: 'value',
enabled: true,
type: 'text',
contentType: 'text/plain',
annotations: [{ name: 'description', value: 'plain field' }],
description: 'plain field'
}
]
}
};
const bru = jsonToBru(json);
expect(bru).toContain('@description(\'plain field\')\n field: value @contentType(text/plain)');
});
it('serializeAnnotations — multipart file field with contentType', () => {
const json = {
body: {
multipartForm: [
{
name: 'upload',
value: ['/tmp/a.png', '/tmp/b.png'],
enabled: true,
type: 'file',
contentType: 'image/png',
annotations: [{ name: 'description', value: 'upload image' }]
}
]
}
};
const bru = jsonToBru(json);
expect(bru).toContain('@description(\'upload image\')\n upload: @file(/tmp/a.png|/tmp/b.png) @contentType(image/png)');
});
it('serializeAnnotations — annotation on vars:post-response', () => {
const json = {
vars: {
res: [{ name: 'token', value: 'abc123', enabled: true, local: false, annotations: [{ name: 'description', value: 'auth token' }] }]
}
};
const bru = jsonToBru(json);
expect(bru).toContain('vars:post-response {');
expect(bru).toContain('@description(\'auth token\')\n token: abc123');
});
it('serializeAnnotations — annotation on local vars:pre-request', () => {
const json = {
vars: {
req: [{ name: 'BASE_URL', value: 'http://localhost', enabled: true, local: true, annotations: [{ name: 'description', value: 'local base url' }] }]
}
};
const bru = jsonToBru(json);
expect(bru).toContain('vars:pre-request {');
expect(bru).toContain('@description(\'local base url\')\n @BASE_URL: http://localhost');
});
it('serializeAnnotations — annotation on disabled local vars:post-response', () => {
const json = {
vars: {
res: [{ name: 'token', value: 'abc123', enabled: false, local: true, annotations: [{ name: 'description', value: 'local token' }] }]
}
};
const bru = jsonToBru(json);
expect(bru).toContain('vars:post-response {');
expect(bru).toContain('@description(\'local token\')\n ~@token: abc123');
});
it('serializeAnnotations — body:file with annotations', () => {
const json = {
body: {
file: [
{
filePath: '/tmp/readme.pdf',
selected: true,
contentType: 'application/pdf',
annotations: [{ name: 'description', value: 'upload doc' }],
description: 'upload doc'
}
]
}
};
const bru = jsonToBru(json);
expect(bru).toContain('body:file {');
expect(bru).toContain('@description(\'upload doc\')\n file: @file(/tmp/readme.pdf) @contentType(application/pdf)');
const parsed = parser(bru);
expect(parsed.body.file).toEqual(json.body.file);
});
it('roundtrip — multipart annotation survives json→bru→json', () => {
const json = {
body: {
multipartForm: [
{
name: 'upload',
value: ['/tmp/a.png'],
enabled: true,
type: 'file',
contentType: 'image/png',
annotations: [{ name: 'description', value: 'upload image' }],
description: 'upload image'
}
]
}
};
const bru = jsonToBru(json);
const parsed = parser(bru);
expect(parsed.body.multipartForm).toEqual(json.body.multipartForm);
});
it('roundtrip: bru → json → bru → json equal', () => {
const input = `get {
url: https://example.com
}
headers {
@description('Content type')
content-type: application/json
@string
~accept: */*
}
`;
const json1 = parser(input);
const bru = jsonToBru(json1);
const json2 = parser(bru);
expect(json2.headers).toEqual(json1.headers);
});
it('serializeAnnotations — annotation without value', () => {
const json = {
meta: { name: 'test', type: 'http', seq: 1 },
http: { method: 'get', url: 'https://example.com' },
headers: [{ name: 'x-key', value: 'val', enabled: true, annotations: [{ name: 'string' }] }]
};
const bru = jsonToBru(json);
expect(bru).toContain('@string\n x-key: val');
});
it('serializeAnnotations — annotation with value', () => {
const json = {
meta: { name: 'test', type: 'http', seq: 1 },
http: { method: 'get', url: 'https://example.com' },
headers: [
{ name: 'x-key', value: 'val', enabled: true, annotations: [{ name: 'description', value: 'my header' }] }
]
};
const bru = jsonToBru(json);
expect(bru).toContain('@description(\'my header\')\n x-key: val');
});
it('serializeAnnotations — disabled pair with annotation', () => {
const json = {
meta: { name: 'test', type: 'http', seq: 1 },
http: { method: 'get', url: 'https://example.com' },
headers: [{ name: 'x-key', value: 'val', enabled: false, annotations: [{ name: 'string' }] }]
};
const bru = jsonToBru(json);
expect(bru).toContain('@string\n ~x-key: val');
});
it('serializeAnnotations — value with single quote uses double-quote delimiters (e.g. O\'Reilly)', () => {
const json = {
meta: { name: 'test', type: 'http', seq: 1 },
http: { method: 'get', url: 'https://example.com' },
headers: [{ name: 'x-key', value: 'val', enabled: true, annotations: [{ name: 'description', value: 'O\'Reilly' }] }]
};
const bru = jsonToBru(json);
expect(bru).toContain('@description("O\'Reilly")\n x-key: val');
});
it('serializeAnnotations — value with double quote uses single-quote delimiters (e.g. say "hello")', () => {
const json = {
meta: { name: 'test', type: 'http', seq: 1 },
http: { method: 'get', url: 'https://example.com' },
headers: [{ name: 'x-key', value: 'val', enabled: true, annotations: [{ name: 'description', value: 'say "hello"' }] }]
};
const bru = jsonToBru(json);
expect(bru).toContain('@description(\'say "hello"\')\n x-key: val');
});
it('parseAndSerialise - bru sourced roundtrip check - headers', () => {
const input = `headers {
@description('''hello''')
key: value
}
`;
const parsed = parser(input);
const output = jsonToBru(parsed);
expect(parser(output)).toEqual(parsed);
expect(output).toContain('@description(\'hello\')');
});
it('parseAndSerialise - json sourced roundtrip check - headers', () => {
const input = {
headers: [
{
name: 'x-key',
value: 'val',
enabled: true,
annotations: [{ name: 'description', value: 'say "hello"' }],
description: 'say "hello"'
}
]
};
const stringified = jsonToBru(input);
const output = parser(stringified);
expect(input).toEqual(output);
});
it('parseAndSerialise - bru sourced roundtrip check - asserts', () => {
const input = `assert {
@description('''make it rain''')
res.status: eq 200
}
`;
const parsed = parser(input);
const output = jsonToBru(parsed);
expect(parser(output)).toEqual(parsed);
expect(output).toContain('@description(\'make it rain\')');
});
it('parseAndSerialise - json sourced roundtrip check - asserts', () => {
const input = {
assertions: [
{
annotations: [{ name: 'description', value: 'hello' }],
name: 'res.status',
value: 'eq 200',
enabled: true,
description: 'hello'
}
]
};
const parsed = jsonToBru(input);
const output = parser(parsed);
expect(input).toEqual(output);
});
it('paren inside single-quoted annotation arg — Token (JWT)', () => {
const input = `headers {
@description('Token (JWT)')
key: value
}
`;
const output = parser(input);
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: 'Token (JWT)' }]);
});
it('paren inside double-quoted annotation arg — Result (OK)', () => {
const input = `headers {
@description("Result (OK)")
key: value
}
`;
const output = parser(input);
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: 'Result (OK)' }]);
});
it('multiple parens inside single-quoted annotation arg', () => {
const input = `headers {
@description('func(a, b) returns (c)')
key: value
}
`;
const output = parser(input);
expect(output.headers[0].annotations).toEqual([{ name: 'description', value: 'func(a, b) returns (c)' }]);
});
it('roundtrip — value containing parens survives json→bru→json — Token (JWT)', () => {
const json = {
meta: { name: 'test', type: 'http', seq: 1 },
http: { method: 'get', url: 'https://example.com' },
headers: [
{ name: 'Authorization', value: 'Bearer token', enabled: true, annotations: [{ name: 'description', value: 'Token (JWT)' }] }
]
};
const bru = jsonToBru(json);
const parsed = parser(bru);
expect(parsed.headers[0].annotations).toEqual([{ name: 'description', value: 'Token (JWT)' }]);
});
it('parseAndSerialise - bru sourced roundtrip check - params:query', () => {
const input = `params:query {
@description('''search term''')
q: hello
}
`;
const parsed = parser(input);
const output = jsonToBru(parsed);
expect(parser(output)).toEqual(parsed);
});
it('parseAndSerialise - json sourced roundtrip check - params:query', () => {
const input = {
params: [
{
name: 'q',
value: 'hello',
enabled: true,
type: 'query',
annotations: [{ name: 'description', value: 'search term' }],
description: 'search term'
}
]
};
const bru = jsonToBru(input);
const output = parser(bru);
expect(output).toEqual(input);
});
it('parseAndSerialise - bru sourced roundtrip check - params:path', () => {
const input = `params:path {
@description('''user id''')
userId: 123
}
`;
const parsed = parser(input);
const output = jsonToBru(parsed);
expect(parser(output)).toEqual(parsed);
});
it('parseAndSerialise - json sourced roundtrip check - params:path', () => {
const input = {
params: [
{
name: 'userId',
value: '123',
enabled: true,
type: 'path',
annotations: [{ name: 'description', value: 'user id' }],
description: 'user id'
}
]
};
const bru = jsonToBru(input);
const output = parser(bru);
expect(output).toEqual(input);
});
it('parseAndSerialise - bru sourced roundtrip check - metadata', () => {
const input = `metadata {
@description('''trace id''')
trace-id: abc123
}
`;
const parsed = parser(input);
const output = jsonToBru(parsed);
expect(parser(output)).toEqual(parsed);
});
it('parseAndSerialise - json sourced roundtrip check - metadata', () => {
const input = {
metadata: [
{
name: 'trace-id',
value: 'abc123',
enabled: true,
annotations: [{ name: 'description', value: 'trace id' }],
description: 'trace id'
}
]
};
const bru = jsonToBru(input);
const output = parser(bru);
expect(output).toEqual(input);
});
it('parseAndSerialise - bru sourced roundtrip check - body:form-urlencoded', () => {
const input = `body:form-urlencoded {
@description('''username field''')
username: alice
}
`;
const parsed = parser(input);
const output = jsonToBru(parsed);
expect(parser(output)).toEqual(parsed);
});
it('parseAndSerialise - json sourced roundtrip check - body:form-urlencoded', () => {
const input = {
body: {
formUrlEncoded: [
{
name: 'username',
value: 'alice',
enabled: true,
annotations: [{ name: 'description', value: 'username field' }],
description: 'username field'
}
]
}
};
const bru = jsonToBru(input);
const output = parser(bru);
expect(output).toEqual(input);
});
it('parseAndSerialise - bru sourced roundtrip check - body:multipart-form text field', () => {
const input = `body:multipart-form {
@description('''plain field''')
field: value @contentType(text/plain)
}
`;
const parsed = parser(input);
const output = jsonToBru(parsed);
expect(parser(output)).toEqual(parsed);
});
it('parseAndSerialise - bru sourced roundtrip check - body:multipart-form file field', () => {
const input = `body:multipart-form {
@description('''upload image''')
upload: @file(/tmp/a.png|/tmp/b.png) @contentType(image/png)
}
`;
const parsed = parser(input);
const output = jsonToBru(parsed);
expect(parser(output)).toEqual(parsed);
});
it('parseAndSerialise - bru sourced roundtrip check - body:file', () => {
const input = `body:file {
@description('''upload doc''')
file: @file(/tmp/readme.pdf) @contentType(application/pdf)
}
`;
const parsed = parser(input);
const output = jsonToBru(parsed);
expect(parser(output)).toEqual(parsed);
});
it('parseAndSerialise - bru sourced roundtrip check - vars:pre-request', () => {
const input = `vars:pre-request {
@description('''base url''')
BASE_URL: http://localhost
}
`;
const parsed = parser(input);
const output = jsonToBru(parsed);
expect(parser(output)).toEqual(parsed);
});
it('parseAndSerialise - json sourced roundtrip check - vars:pre-request', () => {
const input = {
vars: {
req: [
{
name: 'BASE_URL',
value: 'http://localhost',
enabled: true,
local: false,
annotations: [{ name: 'description', value: 'base url' }],
description: 'base url'
}
]
}
};
const bru = jsonToBru(input);
const output = parser(bru);
expect(output).toEqual(input);
});
it('parseAndSerialise - bru sourced roundtrip check - vars:post-response', () => {
const input = `vars:post-response {
@description('''auth token''')
token: abc123
}
`;
const parsed = parser(input);
const output = jsonToBru(parsed);
expect(parser(output)).toEqual(parsed);
});
it('parseAndSerialise - json sourced roundtrip check - vars:post-response', () => {
const input = {
vars: {
res: [
{
name: 'token',
value: 'abc123',
enabled: true,
local: false,
annotations: [{ name: 'description', value: 'auth token' }],
description: 'auth token'
}
]
}
};
const bru = jsonToBru(input);
const output = parser(bru);
expect(output).toEqual(input);
});
it('inline annotation on a header is rejected', () => {
const input = `
headers {
@string key: value
}
`;
expect(() => parser(input)).toThrow();
});
});
describe('env pair annotations', () => {
it('above-line annotation with string arg on a var', () => {
const input = `vars {
@description('my api key')
API_KEY: abc123
}
`;
const output = envParser(input);
expect(output.variables).toEqual([
{
name: 'API_KEY',
value: 'abc123',
enabled: true,
secret: false,
annotations: [{ name: 'description', value: 'my api key' }],
description: 'my api key'
}
]);
});
it('above-line annotation on a var', () => {
const input = `vars {
@deprecated
OLD_KEY: old_value
}
`;
const output = envParser(input);
expect(output.variables).toEqual([
{ name: 'OLD_KEY', value: 'old_value', enabled: true, secret: false, annotations: [{ name: 'deprecated' }] }
]);
});
it('annotation without args on a var', () => {
const input = `vars {
@string
API_KEY: abc
}
`;
const output = envParser(input);
expect(output.variables[0].annotations).toEqual([{ name: 'string' }]);
});
it('multiple annotations on a var', () => {
const input = `vars {
@string
@description('base url')
BASE_URL: http://localhost
}
`;
const output = envParser(input);
expect(output.variables[0].annotations).toEqual([{ name: 'string' }, { name: 'description', value: 'base url' }]);
});
it('disabled var with annotation', () => {
const input = `vars {
@deprecated
~OLD_KEY: old_value
}
`;
const output = envParser(input);
expect(output.variables).toEqual([
{ name: 'OLD_KEY', value: 'old_value', enabled: false, secret: false, annotations: [{ name: 'deprecated' }] }
]);
});
it('no annotation — output unchanged (backward compat)', () => {
const input = `vars {
API_KEY: abc123
}
`;
const output = envParser(input);
expect(output.variables[0]).not.toHaveProperty('annotations');
expect(output.variables[0]).toEqual({ name: 'API_KEY', value: 'abc123', enabled: true, secret: false });
});
it('multiple annotations on a secret var', () => {
const input = `vars:secret [
@string
@description('api token')
API_TOKEN
]
`;
const output = envParser(input);
expect(output.variables[0].annotations).toEqual([{ name: 'string' }, { name: 'description', value: 'api token' }]);
});
it('annotations on multiple secret vars', () => {
const input = `vars:secret [
env_secret_str,
@number
env_secret_num,
@object
env_secret_obj,
@boolean
env_secret_boolean,
env_secret_new
]
`;
const output = envParser(input);
expect(output.variables).toEqual([
{ name: 'env_secret_str', value: '', enabled: true, secret: true },
{ name: 'env_secret_num', value: '', enabled: true, secret: true, annotations: [{ name: 'number' }], dataType: 'number' },
{ name: 'env_secret_obj', value: '', enabled: true, secret: true, annotations: [{ name: 'object' }], dataType: 'object' },
{ name: 'env_secret_boolean', value: '', enabled: true, secret: true, annotations: [{ name: 'boolean' }], dataType: 'boolean' },
{ name: 'env_secret_new', value: '', enabled: true, secret: true }
]);
});
it('parseAndSerialise - bru sourced roundtrip check - multiple secret vars with annotations', () => {
const input = `vars:secret [
env_secret_str,
@number
env_secret_num,
@object
env_secret_obj,
@boolean
env_secret_boolean,
env_secret_new
]
`;
const parsed = envParser(input);
expect(jsonToEnv(parsed)).toEqual(input);
});
it('disabled secret var with annotation', () => {
const input = `vars:secret [
@deprecated
~OLD_SECRET
]
`;
const output = envParser(input);
expect(output.variables).toEqual([
{ name: 'OLD_SECRET', value: '', enabled: false, secret: true, annotations: [{ name: 'deprecated' }] }
]);
});
it('serializeAnnotations in jsonToEnv — disabled secret var with annotation', () => {
const json = {
variables: [{ name: 'OLD_SECRET', value: '', enabled: false, secret: true, annotations: [{ name: 'deprecated' }] }]
};
const bru = jsonToEnv(json);
expect(bru).toContain('@deprecated\n ~OLD_SECRET');
});
it('parseAndSerialise - json sourced roundtrip check - secret env vars', () => {
const input = {
variables: [
{
name: 'SECRET_KEY',
value: '',
enabled: true,
secret: true,
annotations: [{ name: 'description', value: 'my secret key' }],
description: 'my secret key'
}
]
};
const bru = jsonToEnv(input);
const output = envParser(bru);
expect(output).toEqual(input);
});
it('serializeAnnotations in jsonToEnv — annotation without value', () => {
const json = {
variables: [{ name: 'API_KEY', value: 'abc', enabled: true, secret: false, annotations: [{ name: 'deprecated' }] }]
};
const bru = jsonToEnv(json);
expect(bru).toContain('@deprecated\n API_KEY: abc');
});
it('serializeAnnotations in jsonToEnv — annotation with value', () => {
const json = {
variables: [{ name: 'BASE_URL', value: 'http://localhost', enabled: true, secret: false, annotations: [{ name: 'description', value: 'base url' }] }]
};
const bru = jsonToEnv(json);
expect(bru).toContain('@description(\'base url\')\n BASE_URL: http://localhost');
});
it('serializeAnnotations in jsonToEnv — disabled var with annotation', () => {
const json = {
variables: [{ name: 'OLD_KEY', value: 'old', enabled: false, secret: false, annotations: [{ name: 'deprecated' }] }]
};
const bru = jsonToEnv(json);
expect(bru).toContain('@deprecated\n ~OLD_KEY: old');
});
it('parseAndSerialise - bru sourced roundtrip check - env vars', () => {
const input = `vars {
@description('''api key''')
API_KEY: abc123
}
`;
const parsed = envParser(input);
const output = jsonToEnv(parsed);
expect(envParser(output)).toEqual(parsed);
});
it('parseAndSerialise - json sourced roundtrip check - env vars', () => {
const input = {
variables: [
{
name: 'API_KEY',
value: 'abc123',
enabled: true,
secret: false,
annotations: [{ name: 'description', value: 'api key' }],
description: 'api key'
}
]
};
const bru = jsonToEnv(input);
const output = envParser(bru);
expect(output).toEqual(input);
});
it('inline annotation on an env var is rejected', () => {
const input = `vars {
@deprecated API_KEY: abc
}
`;
expect(() => envParser(input)).toThrow();
});
});
describe('env external secrets', () => {
it('parses an external secrets block into { type, variables }', () => {
const input = `vars:externalsecrets:my-vault {
secret: secret/data/secret
password: secret/data/password
}
`;
const output = envParser(input);
expect(output.externalSecrets).toEqual({
type: 'my-vault',
variables: [
{ name: 'secret', value: 'secret/data/secret' },
{ name: 'password', value: 'secret/data/password' }
]
});
});
it('parses an external secrets block with no variables', () => {
const input = `vars:externalsecrets:my-vault {
}
`;
const output = envParser(input);
expect(output.externalSecrets).toEqual({ type: 'my-vault', variables: [] });
});
it('parseAndSerialise - bru sourced roundtrip check - external secrets', () => {
const input = `vars {
}
vars:externalsecrets:my-vault {
secret: secret/data/secret
password: secret/data/password
}
`;
const parsed = envParser(input);
expect(jsonToEnv(parsed)).toEqual(input);
});
});
describe('collection pair annotations', () => {
it('above-line annotation on a header (collection)', () => {
const input = `headers {
@description('content type')
content-type: application/json
}
`;
const output = collectionParser(input);
expect(output.headers).toEqual([
{
name: 'content-type',
value: 'application/json',
enabled: true,
annotations: [{ name: 'description', value: 'content type' }],
description: 'content type'
}
]);
});
it('above-line annotation on a header', () => {
const input = `headers {
@deprecated
old-header: old-value
}
`;
const output = collectionParser(input);
expect(output.headers).toEqual([
{ name: 'old-header', value: 'old-value', enabled: true, annotations: [{ name: 'deprecated' }] }
]);
});
it('annotation on a query param', () => {
const input = `query {
@string
q: search
}
`;
const output = collectionParser(input);
expect(output.query).toEqual([
{ name: 'q', value: 'search', enabled: true, annotations: [{ name: 'string' }] }
]);
});
it('disabled header with annotation', () => {
const input = `headers {
@deprecated
~x-old: value
}
`;
const output = collectionParser(input);
expect(output.headers).toEqual([
{ name: 'x-old', value: 'value', enabled: false, annotations: [{ name: 'deprecated' }] }
]);
});
it('annotation on vars:pre-request', () => {
const input = `vars:pre-request {
@description('base url')
BASE_URL: http://localhost
}
`;
const output = collectionParser(input);
expect(output.vars.req).toEqual([
{
name: 'BASE_URL',
value: 'http://localhost',
enabled: true,
local: false,
annotations: [{ name: 'description', value: 'base url' }],
description: 'base url'
}
]);
});
it('annotation on vars:post-response', () => {
const input = `vars:post-response {
@string
token: abc
}
`;
const output = collectionParser(input);
expect(output.vars.res).toEqual([
{ name: 'token', value: 'abc', enabled: true, local: false, annotations: [{ name: 'string' }] }
]);
});
it('local var (@-prefixed) is not misidentified as annotation', () => {
const input = `vars:pre-request {
@localVar: http://localhost
}
`;
const output = collectionParser(input);
expect(output.vars.req).toEqual([
{ name: 'localVar', value: 'http://localhost', enabled: true, local: true }
]);
expect(output.vars.req[0]).not.toHaveProperty('annotations');
});
it('no annotation — output unchanged (backward compat)', () => {
const input = `headers {
content-type: application/json
}
`;
const output = collectionParser(input);
expect(output.headers[0]).not.toHaveProperty('annotations');
expect(output.headers[0]).toEqual({ name: 'content-type', value: 'application/json', enabled: true });
});
it('serializeAnnotations in jsonToCollectionBru — header without value', () => {
const json = {
headers: [{ name: 'x-key', value: 'val', enabled: true, annotations: [{ name: 'string' }] }]
};
const bru = jsonToCollectionBru(json);
expect(bru).toContain('@string\n x-key: val');
});
it('serializeAnnotations in jsonToCollectionBru — header with annotation value', () => {
const json = {
headers: [{ name: 'content-type', value: 'application/json', enabled: true, annotations: [{ name: 'description', value: 'content type' }] }]
};
const bru = jsonToCollectionBru(json);
expect(bru).toContain('@description(\'content type\')\n content-type: application/json');
});
it('serializeAnnotations in jsonToCollectionBru — disabled header with annotation', () => {
const json = {
headers: [{ name: 'x-old', value: 'val', enabled: false, annotations: [{ name: 'deprecated' }] }]
};
const bru = jsonToCollectionBru(json);
expect(bru).toContain('@deprecated\n ~x-old: val');
});
it('serializeAnnotations in jsonToCollectionBru — query param with annotation', () => {
const json = {
query: [{ name: 'q', value: 'search', enabled: true, annotations: [{ name: 'string' }] }]
};
const bru = jsonToCollectionBru(json);
expect(bru).toContain('@string\n q: search');
});
it('serializeAnnotations in jsonToCollectionBru — vars:pre-request with annotation', () => {
const json = {
vars: {
req: [{ name: 'BASE_URL', value: 'http://localhost', enabled: true, local: false, annotations: [{ name: 'description', value: 'base url' }] }]
}
};
const bru = jsonToCollectionBru(json);
expect(bru).toContain('@description(\'base url\')\n BASE_URL: http://localhost');
});
it('serializeAnnotations in jsonToCollectionBru — vars:post-response with annotation', () => {
const json = {
vars: {
res: [{ name: 'token', value: 'abc123', enabled: true, local: false, annotations: [{ name: 'description', value: 'auth token' }] }]
}
};
const bru = jsonToCollectionBru(json);
expect(bru).toContain('vars:post-response {');
expect(bru).toContain('@description(\'auth token\')\n token: abc123');
});
it('serializeAnnotations in jsonToCollectionBru — local vars:pre-request with annotation', () => {
const json = {
vars: {
req: [{ name: 'BASE_URL', value: 'http://localhost', enabled: true, local: true, annotations: [{ name: 'description', value: 'local base url' }] }]
}
};
const bru = jsonToCollectionBru(json);
expect(bru).toContain('vars:pre-request {');
expect(bru).toContain('@description(\'local base url\')\n @BASE_URL: http://localhost');
});
it('serializeAnnotations in jsonToCollectionBru — disabled local vars:post-response with annotation', () => {
const json = {
vars: {
res: [{ name: 'token', value: 'abc123', enabled: false, local: true, annotations: [{ name: 'description', value: 'local token' }] }]
}
};
const bru = jsonToCollectionBru(json);
expect(bru).toContain('vars:post-response {');
expect(bru).toContain('@description(\'local token\')\n ~@token: abc123');
});
it('parseAndSerialise - bru sourced roundtrip check - collection headers', () => {
const input = `headers {
@description('''content type''')
content-type: application/json
}
`;
const parsed = collectionParser(input);
const output = jsonToCollectionBru(parsed);
expect(collectionParser(output)).toEqual(parsed);
});
it('parseAndSerialise - json sourced roundtrip check - collection headers', () => {
const input = {
headers: [
{
name: 'content-type',
value: 'application/json',
enabled: true,
annotations: [{ name: 'description', value: 'content type' }],
description: 'content type'
}
]
};
const bru = jsonToCollectionBru(input);
const output = collectionParser(bru);
expect(output).toEqual(input);
});
it('parseAndSerialise - bru sourced roundtrip check - collection vars:pre-request', () => {
const input = `vars:pre-request {
@description('''base url''')
BASE_URL: http://localhost
}
`;
const parsed = collectionParser(input);
const output = jsonToCollectionBru(parsed);
expect(collectionParser(output)).toEqual(parsed);
});
it('inline annotation on a collection header is rejected', () => {
const input = `headers {
@string x-key: val
}
`;
expect(() => collectionParser(input)).toThrow();
});
});