mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-23 12:45:38 +00:00
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:
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
26
packages/bruno-cli/src/utils/environment.js
Normal file
26
packages/bruno-cli/src/utils/environment.js
Normal 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
|
||||
};
|
||||
@@ -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' });
|
||||
});
|
||||
});
|
||||
61
tests/runner/cli-json-env-file/cli-json-env-file.spec.ts
Normal file
61
tests/runner/cli-json-env-file/cli-json-env-file.spec.ts
Normal 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 (_) {}
|
||||
});
|
||||
});
|
||||
6
tests/runner/cli-json-env-file/collection/bruno.json
Normal file
6
tests/runner/cli-json-env-file/collection/bruno.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "cli-json-env-fixture",
|
||||
"type": "collection"
|
||||
}
|
||||
|
||||
4
tests/runner/cli-json-env-file/collection/env.json
Normal file
4
tests/runner/cli-json-env-file/collection/env.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "CLI JSON Env",
|
||||
"variables": [{ "name": "baseUrl", "value": "https://echo.usebruno.com", "enabled": true }]
|
||||
}
|
||||
17
tests/runner/cli-json-env-file/collection/request.bru
Normal file
17
tests/runner/cli-json-env-file/collection/request.bru
Normal 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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user