fix(qenv): Handle falsy environment values correctly, improve env source resolution, add tests and update test script
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-08-16 - 6.1.3 - fix(qenv)
|
||||||
|
Handle falsy environment values correctly, improve env source resolution, add tests and update test script
|
||||||
|
|
||||||
|
- Treat 0, false and empty string as valid environment values by checking for !== undefined when resolving env vars
|
||||||
|
- Refactor source resolution to iterate over environment, env file, Docker secrets and secret.json, returning the first defined value
|
||||||
|
- Ensure env.json and Docker secret JSON return strings for scalar values and base64-encode object values
|
||||||
|
- Add tests covering falsy values and lookup behavior (test/test.falsy.ts)
|
||||||
|
- Update package.json test script to run tstest with --verbose --testlog --timeout flags
|
||||||
|
|
||||||
## 2025-08-14 - 6.1.2 - fix(readme)
|
## 2025-08-14 - 6.1.2 - fix(readme)
|
||||||
Correct DATABASE_CONFIG example formatting in README and add local settings configuration file
|
Correct DATABASE_CONFIG example formatting in README and add local settings configuration file
|
||||||
|
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
"typings": "dist_ts/index.d.ts",
|
"typings": "dist_ts/index.d.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "(tstest test/)",
|
"test": "(tstest test/ --verbose --testlog --timeout 20)",
|
||||||
"build": "(tsbuild --web --allowimplicitany)",
|
"build": "(tsbuild --web --allowimplicitany)",
|
||||||
"buildDocs": "tsdoc"
|
"buildDocs": "tsdoc"
|
||||||
},
|
},
|
||||||
|
119
test/test.falsy.ts
Normal file
119
test/test.falsy.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import * as path from 'path';
|
||||||
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
import * as qenv from '../ts/index.js';
|
||||||
|
|
||||||
|
const testDir = path.dirname(new URL(import.meta.url).pathname);
|
||||||
|
|
||||||
|
// Test falsy values handling
|
||||||
|
tap.test('should handle falsy values correctly', async () => {
|
||||||
|
// Set up environment variables with falsy values
|
||||||
|
process.env['FALSY_FALSE'] = 'false';
|
||||||
|
process.env['FALSY_ZERO'] = '0';
|
||||||
|
process.env['FALSY_EMPTY'] = '';
|
||||||
|
|
||||||
|
const testQenv = new qenv.Qenv(testDir, testDir, false);
|
||||||
|
|
||||||
|
// Test that falsy values are returned, not undefined
|
||||||
|
expect(await testQenv.getEnvVarOnDemand('FALSY_FALSE')).toEqual('false');
|
||||||
|
expect(await testQenv.getEnvVarOnDemand('FALSY_ZERO')).toEqual('0');
|
||||||
|
expect(await testQenv.getEnvVarOnDemand('FALSY_EMPTY')).toEqual('');
|
||||||
|
|
||||||
|
// Test sync versions
|
||||||
|
expect(testQenv.getEnvVarOnDemandSync('FALSY_FALSE')).toEqual('false');
|
||||||
|
expect(testQenv.getEnvVarOnDemandSync('FALSY_ZERO')).toEqual('0');
|
||||||
|
expect(testQenv.getEnvVarOnDemandSync('FALSY_EMPTY')).toEqual('');
|
||||||
|
|
||||||
|
// Test that undefined is still returned for non-existent variables
|
||||||
|
expect(await testQenv.getEnvVarOnDemand('NON_EXISTENT')).toBeUndefined();
|
||||||
|
expect(testQenv.getEnvVarOnDemandSync('NON_EXISTENT')).toBeUndefined();
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
delete process.env['FALSY_FALSE'];
|
||||||
|
delete process.env['FALSY_ZERO'];
|
||||||
|
delete process.env['FALSY_EMPTY'];
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should handle falsy values in env.json file', async () => {
|
||||||
|
// Create a test env.json with falsy values
|
||||||
|
const testAssetsDir = path.join(testDir, 'assets-falsy');
|
||||||
|
const fs = await import('fs');
|
||||||
|
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
if (!fs.existsSync(testAssetsDir)) {
|
||||||
|
fs.mkdirSync(testAssetsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create env.json with falsy values
|
||||||
|
const envJsonContent = {
|
||||||
|
JSON_FALSE: false,
|
||||||
|
JSON_ZERO: 0,
|
||||||
|
JSON_EMPTY: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(testAssetsDir, 'env.json'),
|
||||||
|
JSON.stringify(envJsonContent, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
const testQenv = new qenv.Qenv(testAssetsDir, testAssetsDir, false);
|
||||||
|
|
||||||
|
// Test that falsy values from JSON are returned correctly
|
||||||
|
expect(await testQenv.getEnvVarOnDemand('JSON_FALSE')).toEqual('false');
|
||||||
|
expect(await testQenv.getEnvVarOnDemand('JSON_ZERO')).toEqual('0');
|
||||||
|
expect(await testQenv.getEnvVarOnDemand('JSON_EMPTY')).toEqual('');
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
fs.rmSync(testAssetsDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should throw error for undefined in strict mode', async () => {
|
||||||
|
const testQenv = new qenv.Qenv(testDir, testDir, false);
|
||||||
|
|
||||||
|
// Set a falsy value
|
||||||
|
process.env['FALSY_VALUE'] = '0';
|
||||||
|
|
||||||
|
// Should NOT throw for falsy value
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = await testQenv.getEnvVarOnDemandStrict('FALSY_VALUE');
|
||||||
|
} catch (error) {
|
||||||
|
// Should not reach here
|
||||||
|
expect(true).toBeFalse();
|
||||||
|
}
|
||||||
|
expect(result).toEqual('0');
|
||||||
|
|
||||||
|
// Should throw for non-existent variable
|
||||||
|
let threwError = false;
|
||||||
|
try {
|
||||||
|
await testQenv.getEnvVarOnDemandStrict('NON_EXISTENT_VAR');
|
||||||
|
} catch (error) {
|
||||||
|
threwError = true;
|
||||||
|
expect(error.message).toContain('is not set');
|
||||||
|
}
|
||||||
|
expect(threwError).toBeTrue();
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
delete process.env['FALSY_VALUE'];
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('should handle array of env vars with falsy values', async () => {
|
||||||
|
const testQenv = new qenv.Qenv(testDir, testDir, false);
|
||||||
|
|
||||||
|
// Set up test environment
|
||||||
|
process.env['FIRST_VAR'] = '0';
|
||||||
|
process.env['SECOND_VAR'] = 'false';
|
||||||
|
|
||||||
|
// Test that it returns the first defined value, even if falsy
|
||||||
|
const result = await testQenv.getEnvVarOnDemand(['NON_EXISTENT', 'FIRST_VAR', 'SECOND_VAR']);
|
||||||
|
expect(result).toEqual('0');
|
||||||
|
|
||||||
|
// Test sync version
|
||||||
|
const resultSync = testQenv.getEnvVarOnDemandSync(['NON_EXISTENT', 'FIRST_VAR', 'SECOND_VAR']);
|
||||||
|
expect(resultSync).toEqual('0');
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
delete process.env['FIRST_VAR'];
|
||||||
|
delete process.env['SECOND_VAR'];
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/qenv',
|
name: '@push.rocks/qenv',
|
||||||
version: '6.1.2',
|
version: '6.1.3',
|
||||||
description: 'A module for easily handling environment variables in Node.js projects with support for .yml and .json configuration.'
|
description: 'A module for easily handling environment variables in Node.js projects with support for .yml and .json configuration.'
|
||||||
}
|
}
|
||||||
|
@@ -71,7 +71,7 @@ export class Qenv {
|
|||||||
private loadAvailableEnvVars() {
|
private loadAvailableEnvVars() {
|
||||||
for (const envVar of this.requiredEnvVars) {
|
for (const envVar of this.requiredEnvVars) {
|
||||||
const value = this.getEnvVarOnDemand(envVar);
|
const value = this.getEnvVarOnDemand(envVar);
|
||||||
if (value) {
|
if (value !== undefined) {
|
||||||
this.availableEnvVars.push(envVar);
|
this.availableEnvVars.push(envVar);
|
||||||
this.keyValueObject[envVar] = value;
|
this.keyValueObject[envVar] = value;
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ export class Qenv {
|
|||||||
if (Array.isArray(envVarNameOrNames)) {
|
if (Array.isArray(envVarNameOrNames)) {
|
||||||
for (const envVarName of envVarNameOrNames) {
|
for (const envVarName of envVarNameOrNames) {
|
||||||
const value = await this.tryGetEnvVar(envVarName);
|
const value = await this.tryGetEnvVar(envVarName);
|
||||||
if (value) {
|
if (value !== undefined) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,7 +120,7 @@ export class Qenv {
|
|||||||
envVarNameOrNames: TEnvVarRef | TEnvVarRef[]
|
envVarNameOrNames: TEnvVarRef | TEnvVarRef[]
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const value = await this.getEnvVarOnDemand(envVarNameOrNames);
|
const value = await this.getEnvVarOnDemand(envVarNameOrNames);
|
||||||
if (!value) {
|
if (value === undefined) {
|
||||||
throw new Error(`Env var ${envVarNameOrNames} is not set!`);
|
throw new Error(`Env var ${envVarNameOrNames} is not set!`);
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
@@ -132,7 +132,7 @@ export class Qenv {
|
|||||||
if (Array.isArray(envVarNameOrNames)) {
|
if (Array.isArray(envVarNameOrNames)) {
|
||||||
for (const envVarName of envVarNameOrNames) {
|
for (const envVarName of envVarNameOrNames) {
|
||||||
const value = this.tryGetEnvVarSync(envVarName);
|
const value = this.tryGetEnvVarSync(envVarName);
|
||||||
if (value) {
|
if (value !== undefined) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,21 +156,37 @@ export class Qenv {
|
|||||||
return await envVarRefArg();
|
return await envVarRefArg();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const sources = [
|
||||||
this.getFromEnvironmentVariable(envVarRefArg) ||
|
this.getFromEnvironmentVariable(envVarRefArg),
|
||||||
this.getFromEnvYamlOrJsonFile(envVarRefArg) ||
|
this.getFromEnvYamlOrJsonFile(envVarRefArg),
|
||||||
this.getFromDockerSecret(envVarRefArg) ||
|
this.getFromDockerSecret(envVarRefArg),
|
||||||
this.getFromDockerSecretJson(envVarRefArg)
|
this.getFromDockerSecretJson(envVarRefArg)
|
||||||
);
|
];
|
||||||
|
|
||||||
|
for (const value of sources) {
|
||||||
|
if (value !== undefined) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private tryGetEnvVarSync(envVarName: string): string | undefined {
|
private tryGetEnvVarSync(envVarName: string): string | undefined {
|
||||||
return (
|
const sources = [
|
||||||
this.getFromEnvironmentVariable(envVarName) ||
|
this.getFromEnvironmentVariable(envVarName),
|
||||||
this.getFromEnvYamlOrJsonFile(envVarName) ||
|
this.getFromEnvYamlOrJsonFile(envVarName),
|
||||||
this.getFromDockerSecret(envVarName) ||
|
this.getFromDockerSecret(envVarName),
|
||||||
this.getFromDockerSecretJson(envVarName)
|
this.getFromDockerSecretJson(envVarName)
|
||||||
);
|
];
|
||||||
|
|
||||||
|
for (const value of sources) {
|
||||||
|
if (value !== undefined) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getFromEnvironmentVariable(envVarName: string): string | undefined {
|
private getFromEnvironmentVariable(envVarName: string): string | undefined {
|
||||||
@@ -184,10 +200,13 @@ export class Qenv {
|
|||||||
try {
|
try {
|
||||||
const envJson = plugins.smartfile.fs.toObjectSync(this.envFilePathAbsolute);
|
const envJson = plugins.smartfile.fs.toObjectSync(this.envFilePathAbsolute);
|
||||||
const value = envJson[envVarName];
|
const value = envJson[envVarName];
|
||||||
|
if (value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
if (typeof value === 'object') {
|
if (typeof value === 'object') {
|
||||||
return 'base64Object:' + this.encodeBase64(value);
|
return 'base64Object:' + this.encodeBase64(value);
|
||||||
}
|
}
|
||||||
return value;
|
return String(value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -208,10 +227,13 @@ export class Qenv {
|
|||||||
if (secret.includes('secret.json')) {
|
if (secret.includes('secret.json')) {
|
||||||
const secretObject = plugins.smartfile.fs.toObjectSync(`/run/secrets/${secret}`);
|
const secretObject = plugins.smartfile.fs.toObjectSync(`/run/secrets/${secret}`);
|
||||||
const value = secretObject[envVarName];
|
const value = secretObject[envVarName];
|
||||||
|
if (value === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (typeof value === 'object') {
|
if (typeof value === 'object') {
|
||||||
return 'base64Object:' + this.encodeBase64(value);
|
return 'base64Object:' + this.encodeBase64(value);
|
||||||
}
|
}
|
||||||
return value;
|
return String(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user