fix(qenv): Handle falsy environment values correctly, improve env source resolution, add tests and update test script

This commit is contained in:
2025-08-16 12:35:35 +00:00
parent a7e3bf1223
commit 5a81caa7bb
5 changed files with 168 additions and 18 deletions

View File

@@ -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

View 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
View 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();

View File

@@ -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.'
} }

View File

@@ -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);
} }
} }
} }