feat: enhance json environment file support in bruno-cli (#5660)

* feat: enhance json environment file support in bruno-cli

feat: add parseEnvironmentJson function to normalize environment JSON structure

lint fixes

feat: added tests for invalid JSON environment files in CLI and added missing constant defenition.

feat: improve JSON environment file handling and update tests

Trigger test

fix: update CLI command syntax for non-existent JSON environment file test

fix: correct CLI command syntax in test for non-existent JSON environment file

fix: update CLI command syntax in test for non-existent JSON environment file

fix: update test to use temporary path for non-existent JSON environment file

trying to fix the tests

fix tests

refactor: rename ERROR_INVALID_JSON to ERROR_INVALID_FILE and update related error handling in CLI commands and tests

fix: update parseEnvironmentJson to preserve secret flag

test: improved tests

* refactor: move parseEnvironmentJson function to utils/ environment.js file and update imports

* test: update tests
This commit is contained in:
Sanjai Kumar
2025-10-07 12:49:22 +05:30
committed by GitHub
parent 1cc3a6432a
commit 8d2f087206
8 changed files with 233 additions and 6 deletions

View File

@@ -6,6 +6,7 @@ const { getRunnerSummary } = require('@usebruno/common/runner');
const { exists, isFile, isDirectory } = require('../utils/filesystem');
const { runSingleRequest } = require('../runner/run-single-request');
const { getEnvVars } = require('../utils/bru');
const { parseEnvironmentJson } = require('../utils/environment');
const { isRequestTagsIncluded } = require("@usebruno/common")
const makeJUnitOutput = require('../reporters/junit');
const makeHtmlOutput = require('../reporters/html');
@@ -131,7 +132,7 @@ const builder = async (yargs) => {
type: 'string'
})
.option('env-file', {
describe: 'Path to environment file (.bru) - can be absolute or relative path',
describe: 'Path to environment file (.bru or .json) - absolute or relative',
type: 'string'
})
.option('env-var', {
@@ -306,7 +307,7 @@ const handler = async function (argv) {
clientCertConfigJson = JSON.parse(clientCertConfigFileContent);
} catch (err) {
console.error(chalk.red(`Failed to parse Client Certificate Config JSON: ${err.message}`));
process.exit(constants.EXIT_STATUS.ERROR_INVALID_JSON);
process.exit(constants.EXIT_STATUS.ERROR_INVALID_FILE);
}
if (clientCertConfigJson?.enabled && Array.isArray(clientCertConfigJson?.certs)) {
@@ -346,10 +347,29 @@ const handler = async function (argv) {
process.exit(constants.EXIT_STATUS.ERROR_ENV_NOT_FOUND);
}
const envBruContent = fs.readFileSync(envFilePath, 'utf8').replace(/\r\n/g, '\n');
const envJson = parseEnvironment(envBruContent);
envVars = getEnvVars(envJson);
envVars.__name__ = envFile ? path.basename(envFilePath, '.bru') : env;
const ext = path.extname(envFilePath).toLowerCase();
if (ext === '.json') {
// Parse Bruno schema JSON environment
let envJsonContent;
try {
envJsonContent = fs.readFileSync(envFilePath, 'utf8');
const parsed = JSON.parse(envJsonContent);
const normalizedEnv = parseEnvironmentJson(parsed);
envVars = getEnvVars(normalizedEnv);
const rawName = normalizedEnv?.name;
const trimmedName = typeof rawName === 'string' ? rawName.trim() : '';
envVars.__name__ = trimmedName || path.basename(envFilePath, '.json');
} catch (err) {
console.error(chalk.red(`Failed to parse Environment JSON: ${err.message}`));
process.exit(constants.EXIT_STATUS.ERROR_INVALID_FILE);
}
} else {
// Default to .bru parsing
const envBruContent = fs.readFileSync(envFilePath, 'utf8').replace(/\r\n/g, '\n');
const envJson = parseEnvironment(envBruContent);
envVars = getEnvVars(envJson);
envVars.__name__ = envFile ? path.basename(envFilePath, '.bru') : env;
}
}
if (envVar) {

View File

@@ -23,6 +23,8 @@ const EXIT_STATUS = {
ERROR_INCORRECT_ENV_OVERRIDE: 8,
// Invalid output format requested
ERROR_INCORRECT_OUTPUT_FORMAT: 9,
// Invalid file format
ERROR_INVALID_FILE: 10,
// Everything else
ERROR_GENERIC: 255
};

View File

@@ -0,0 +1,26 @@
/**
* Parse a Bruno JSON environment object and normalize variables
* Accepts only single environment object: { name?, uid?, variables: [...] }
*/
const parseEnvironmentJson = (parsed = {}) => {
if (!parsed || !Array.isArray(parsed.variables)) {
throw new Error('Invalid environment JSON: expected a single environment object with a "variables" array');
}
const normalized = {
name: parsed.name,
variables: (parsed.variables || []).filter(Boolean).map((variable) => ({
name: variable.name,
value: variable.value,
type: variable.type || 'text',
enabled: variable.enabled !== false,
secret: variable.secret || false
}))
};
return normalized;
};
module.exports = {
parseEnvironmentJson
};

View File

@@ -0,0 +1,91 @@
const { describe, it, expect } = require('@jest/globals');
const { getEnvVars } = require('../../src/utils/bru');
const { parseEnvironmentJson } = require('../../src/utils/environment');
describe('parseEnvironmentJson', () => {
it('normalizes single environment object', () => {
const input = {
name: 'My Env',
variables: [
{ name: 'host', value: 'https://www.httpfaker.org' },
{ name: 'token', value: 'abc', enabled: false, secret: true }
]
};
const env = parseEnvironmentJson(input);
expect(Array.isArray(env.variables)).toBe(true);
expect(env.variables[0]).toEqual({
name: 'host',
value: 'https://www.httpfaker.org',
type: 'text',
enabled: true,
secret: false
});
expect(env.variables[1].enabled).toBe(false);
expect(env.variables[1].secret).toBe(true);
const vars = getEnvVars(env);
expect(vars).toEqual({ host: 'https://www.httpfaker.org' });
});
it('throws on invalid shape', () => {
expect(() => parseEnvironmentJson({ name: 'x' })).toThrow(/Invalid environment JSON/i);
});
it('respects explicit fields and preserves secret flag', () => {
const input = {
name: 'My Env',
variables: [
{ name: 'one', value: '1', type: 'text', enabled: true, secret: true },
{ name: 'two', value: '2', type: 'file', enabled: false, secret: false }
]
};
const env = parseEnvironmentJson(input);
expect(env.variables[0]).toEqual({
name: 'one',
value: '1',
type: 'text',
enabled: true,
secret: true
});
expect(env.variables[1]).toEqual({
name: 'two',
value: '2',
type: 'file',
enabled: false,
secret: false
});
const vars = getEnvVars(env);
expect(vars).toEqual({ one: '1' });
});
it('defaults secret to false for undefined and null', () => {
const input = {
name: 'My Env',
variables: [
{ name: 'three', value: '3', enabled: true },
{ name: 'four', value: '4', enabled: true, secret: null }
]
};
const env = parseEnvironmentJson(input);
expect(env.variables[0]).toEqual({
name: 'three',
value: '3',
type: 'text',
enabled: true,
secret: false
});
expect(env.variables[1]).toEqual({
name: 'four',
value: '4',
type: 'text',
enabled: true,
secret: false
});
const vars = getEnvVars(env);
expect(vars).toEqual({ three: '3', four: '4' });
});
});

View File

@@ -0,0 +1,61 @@
import { test, expect } from '../../../playwright';
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import constants from '../../../packages/bruno-cli/src/constants.js';
test.describe('CLI JSON Environment File Support', () => {
const collectionPath = path.resolve(__dirname, 'collection');
const BRU = 'node ../../../../packages/bruno-cli/bin/bru.js';
// Helper: emulate `bru run` from a given working directory and
// return the process exit code (0 on success). We use execSync so
// these tests behave like invoking the CLI directly in a shell.
const runFrom = (cwd: string, args: string): number => {
try {
execSync(`cd "${cwd}" && ${BRU} ${args}`, { stdio: 'pipe' });
return 0;
} catch (error: any) {
return error?.status ?? 1;
}
};
test('CLI: Run with invalid JSON environment file should fail', async () => {
// Create a temporary invalid JSON file
const tempDir = '/tmp/bruno-cli-test';
const invalidEnvPath = path.join(tempDir, 'invalid-env.json');
fs.mkdirSync(tempDir, { recursive: true });
fs.writeFileSync(invalidEnvPath,
JSON.stringify({
name: 'Invalid Env'
// missing variables array - invalid JSON
}));
const status = runFrom(collectionPath, `run --env-file "${invalidEnvPath}"`);
expect(status).toBe(constants.EXIT_STATUS.ERROR_INVALID_FILE);
try {
// Cleanup
fs.unlinkSync(invalidEnvPath);
fs.rmdirSync(tempDir);
} catch (e) {}
});
test('CLI: Run with valid JSON env and interpolates variables', async () => {
const envPath = path.join(collectionPath, 'env.json');
const outputPath = path.join(collectionPath, 'out.json');
// Even if exit is non-zero (network warnings), reporter should be written
runFrom(collectionPath, `run request.bru --env-file "${envPath}" --reporter-json "${outputPath}"`);
expect(fs.existsSync(outputPath)).toBe(true);
const report = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
const result = report.results[0];
expect(result.request.url).toBe('https://echo.usebruno.com');
expect(result.response.status).toBe(200);
try {
fs.unlinkSync(outputPath);
} catch (_) {}
});
});

View File

@@ -0,0 +1,6 @@
{
"version": "1",
"name": "cli-json-env-fixture",
"type": "collection"
}

View File

@@ -0,0 +1,4 @@
{
"name": "CLI JSON Env",
"variables": [{ "name": "baseUrl", "value": "https://echo.usebruno.com", "enabled": true }]
}

View File

@@ -0,0 +1,17 @@
meta {
name: cli-json-env-test
type: http
}
http {
method: POST
url: {{baseUrl}}
body: json
auth: none
}
body:json {
{
"ping": "pong"
}
}