Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 216 additions & 0 deletions tests/core/config/env-interpolator.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/**
* Testes unitários para env-interpolator
*
* Cobre interpolateString, interpolateEnvVars e lintEnvPatterns.
*
* @see .aiox-core/core/config/env-interpolator.js
* @issue #52
*/

'use strict';

const {
interpolateString,
interpolateEnvVars,
lintEnvPatterns,
ENV_VAR_PATTERN,
} = require('../../../.aiox-core/core/config/env-interpolator');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Use absolute import path here.

Line 17 uses a relative require(...); please switch it to the project’s absolute import convention used in this repository.

As per coding guidelines, "**/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/config/env-interpolator.test.js` at line 17, Replace the relative
require('../../../.aiox-core/core/config/env-interpolator') in
tests/core/config/env-interpolator.test.js with the repository's absolute import
for that module (use the project's absolute import root/prefix and the module
name env-interpolator), so the test imports the same module via the established
absolute path convention rather than a relative path.


// ============================================================================
// interpolateString
// ============================================================================

describe('interpolateString', () => {
const ORIGINAL_ENV = process.env;

beforeEach(() => {
process.env = { ...ORIGINAL_ENV };
});

afterAll(() => {
process.env = ORIGINAL_ENV;
});

it('deve resolver ${VAR} existente', () => {
process.env.MY_VAR = 'hello';
expect(interpolateString('${MY_VAR}')).toBe('hello');
});

it('deve resolver ${VAR:-default} quando VAR existe', () => {
process.env.MY_VAR = 'real';
expect(interpolateString('${MY_VAR:-fallback}')).toBe('real');
});

it('deve usar default quando VAR não existe', () => {
delete process.env.MISSING_VAR;
expect(interpolateString('${MISSING_VAR:-fallback}')).toBe('fallback');
});

it('deve retornar string vazia e gerar warning quando VAR não existe sem default', () => {
delete process.env.MISSING_VAR;
const warnings = [];
const result = interpolateString('${MISSING_VAR}', { warnings });

expect(result).toBe('');
expect(warnings).toHaveLength(1);
expect(warnings[0]).toContain('MISSING_VAR');
});

it('deve resolver múltiplas variáveis na mesma string', () => {
process.env.HOST = 'localhost';
process.env.PORT = '3000';
expect(interpolateString('${HOST}:${PORT}')).toBe('localhost:3000');
});

it('deve preservar texto sem padrão ${...}', () => {
expect(interpolateString('no variables here')).toBe('no variables here');
});

it('deve resolver default vazio ${VAR:-}', () => {
delete process.env.EMPTY_DEFAULT;
expect(interpolateString('${EMPTY_DEFAULT:-}')).toBe('');
});
});

// ============================================================================
// interpolateEnvVars
// ============================================================================

describe('interpolateEnvVars', () => {
const ORIGINAL_ENV = process.env;

beforeEach(() => {
process.env = { ...ORIGINAL_ENV };
});

afterAll(() => {
process.env = ORIGINAL_ENV;
});

it('deve interpolar strings em objetos aninhados', () => {
process.env.DB_HOST = 'pg.example.com';
const config = {
database: {
host: '${DB_HOST}',
port: 5432,
},
};

const result = interpolateEnvVars(config);
expect(result.database.host).toBe('pg.example.com');
expect(result.database.port).toBe(5432);
});

it('deve interpolar strings em arrays', () => {
process.env.ITEM = 'resolved';
const config = ['${ITEM}', 'static'];

const result = interpolateEnvVars(config);
expect(result).toEqual(['resolved', 'static']);
});

it('deve preservar números, booleanos e null', () => {
expect(interpolateEnvVars(42)).toBe(42);
expect(interpolateEnvVars(true)).toBe(true);
expect(interpolateEnvVars(null)).toBeNull();
});

it('deve processar objetos profundamente aninhados', () => {
process.env.SECRET = 'top-secret';
const config = {
l1: { l2: { l3: { key: '${SECRET}' } } },
};

const result = interpolateEnvVars(config);
expect(result.l1.l2.l3.key).toBe('top-secret');
});

it('deve coletar warnings de variáveis ausentes', () => {
delete process.env.UNKNOWN;
const warnings = [];
interpolateEnvVars({ key: '${UNKNOWN}' }, { warnings });

expect(warnings).toHaveLength(1);
expect(warnings[0]).toContain('UNKNOWN');
});

it('não deve mutar o config original', () => {
process.env.VAL = 'new';
const config = { a: '${VAL}' };
const configCopy = JSON.parse(JSON.stringify(config));

interpolateEnvVars(config);
expect(config).toEqual(configCopy);
});
});

// ============================================================================
// lintEnvPatterns
// ============================================================================

describe('lintEnvPatterns', () => {
it('deve detectar padrões ${...} em strings', () => {
const config = { api: { key: '${API_KEY}' } };
const findings = lintEnvPatterns(config, 'config.yaml');

expect(findings).toHaveLength(1);
expect(findings[0]).toContain('config.yaml');
expect(findings[0]).toContain('api.key');
expect(findings[0]).toContain('${API_KEY}');
});

it('deve detectar padrões em arrays', () => {
const config = { hosts: ['static', '${DYNAMIC_HOST}'] };
const findings = lintEnvPatterns(config, 'test.yaml');

expect(findings).toHaveLength(1);
expect(findings[0]).toContain('hosts[1]');
});

it('deve retornar array vazio quando não há padrões', () => {
const config = { name: 'static', port: 3000 };
const findings = lintEnvPatterns(config, 'clean.yaml');

expect(findings).toEqual([]);
});

it('deve detectar múltiplos padrões', () => {
const config = {
db: { host: '${DB_HOST}', pass: '${DB_PASS}' },
api: '${API_URL}',
};
const findings = lintEnvPatterns(config, 'app.yaml');

expect(findings).toHaveLength(3);
});

it('deve funcionar com objetos profundamente aninhados', () => {
const config = { a: { b: { c: { d: '${DEEP}' } } } };
const findings = lintEnvPatterns(config, 'deep.yaml');

expect(findings).toHaveLength(1);
expect(findings[0]).toContain('a.b.c.d');
});
});

// ============================================================================
// ENV_VAR_PATTERN
// ============================================================================

describe('ENV_VAR_PATTERN', () => {
it('deve ser uma regex global', () => {
expect(ENV_VAR_PATTERN).toBeInstanceOf(RegExp);
expect(ENV_VAR_PATTERN.global).toBe(true);
});

it('deve capturar nome da variável', () => {
const match = '${MY_VAR}'.match(new RegExp(ENV_VAR_PATTERN.source));
expect(match[1]).toBe('MY_VAR');
});

it('deve capturar valor default', () => {
const match = '${MY_VAR:-default}'.match(new RegExp(ENV_VAR_PATTERN.source));
expect(match[1]).toBe('MY_VAR');
expect(match[2]).toBe('default');
});
});
171 changes: 171 additions & 0 deletions tests/core/config/merge-utils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/**
* Testes unitários para merge-utils
*
* Cobre deepMerge, mergeAll e isPlainObject conforme ADR-PRO-002.
*
* @see .aiox-core/core/config/merge-utils.js
* @issue #52
*/

'use strict';

const { deepMerge, mergeAll, isPlainObject } = require('../../../.aiox-core/core/config/merge-utils');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Replace relative import with absolute import.

Line 12 should follow the repository absolute-import rule instead of ../../../....

As per coding guidelines, "**/*.{js,jsx,ts,tsx}: Use absolute imports instead of relative imports in all code".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/core/config/merge-utils.test.js` at line 12, Replace the relative
require in the test that imports deepMerge, mergeAll, and isPlainObject with the
repository's absolute-import path: update the require statement that currently
points to '../../../.aiox-core/core/config/merge-utils' so it imports from the
package/root absolute module path used across the repo (keeping the same
exported symbols deepMerge, mergeAll, isPlainObject) to follow the
absolute-import rule.


// ============================================================================
// isPlainObject
// ============================================================================

describe('isPlainObject', () => {
it('deve retornar true para objetos literais', () => {
expect(isPlainObject({})).toBe(true);
expect(isPlainObject({ a: 1 })).toBe(true);
});

it('deve retornar true para Object.create(null)', () => {
expect(isPlainObject(Object.create(null))).toBe(true);
});

it('deve retornar false para arrays', () => {
expect(isPlainObject([])).toBe(false);
expect(isPlainObject([1, 2])).toBe(false);
});

it('deve retornar false para null e undefined', () => {
expect(isPlainObject(null)).toBe(false);
expect(isPlainObject(undefined)).toBe(false);
});

it('deve retornar false para primitivos', () => {
expect(isPlainObject(42)).toBe(false);
expect(isPlainObject('string')).toBe(false);
expect(isPlainObject(true)).toBe(false);
});

it('deve retornar false para Date e RegExp', () => {
expect(isPlainObject(new Date())).toBe(false);
expect(isPlainObject(/regex/)).toBe(false);
});
});

// ============================================================================
// deepMerge
// ============================================================================

describe('deepMerge', () => {
it('deve fazer last-wins para escalares', () => {
const result = deepMerge({ a: 1 }, { a: 2 });
expect(result.a).toBe(2);
});

it('deve preservar chaves do target ausentes no source', () => {
const result = deepMerge({ a: 1, b: 2 }, { a: 10 });
expect(result).toEqual({ a: 10, b: 2 });
});

it('deve adicionar chaves novas do source', () => {
const result = deepMerge({ a: 1 }, { b: 2 });
expect(result).toEqual({ a: 1, b: 2 });
});

it('deve fazer deep merge de objetos aninhados', () => {
const target = { db: { host: 'localhost', port: 5432 } };
const source = { db: { port: 3306, name: 'mydb' } };
const result = deepMerge(target, source);

expect(result.db).toEqual({ host: 'localhost', port: 3306, name: 'mydb' });
});

it('deve substituir arrays (não concatenar) por padrão', () => {
const target = { tags: ['a', 'b'] };
const source = { tags: ['c'] };
const result = deepMerge(target, source);

expect(result.tags).toEqual(['c']);
});

it('deve concatenar arrays com +append', () => {
const target = { plugins: ['core', 'auth'] };
const source = { 'plugins+append': ['analytics'] };
const result = deepMerge(target, source);

expect(result.plugins).toEqual(['core', 'auth', 'analytics']);
});

it('deve criar array quando +append mas target não tem o campo', () => {
const target = {};
const source = { 'items+append': [1, 2] };
const result = deepMerge(target, source);

expect(result.items).toEqual([1, 2]);
});

it('deve deletar chave quando value é null', () => {
const target = { a: 1, b: 2, c: 3 };
const source = { b: null };
const result = deepMerge(target, source);

expect(result).toEqual({ a: 1, c: 3 });
expect('b' in result).toBe(false);
});

it('não deve mutar os inputs', () => {
const target = { a: { x: 1 } };
const source = { a: { y: 2 } };
const targetCopy = JSON.parse(JSON.stringify(target));
const sourceCopy = JSON.parse(JSON.stringify(source));

deepMerge(target, source);

expect(target).toEqual(targetCopy);
expect(source).toEqual(sourceCopy);
});

it('deve retornar source quando target não é objeto', () => {
expect(deepMerge('string', { a: 1 })).toEqual({ a: 1 });
expect(deepMerge(null, { a: 1 })).toEqual({ a: 1 });
});

it('deve retornar target quando source é undefined', () => {
expect(deepMerge({ a: 1 }, undefined)).toEqual({ a: 1 });
});

it('deve fazer merge profundo em 3+ níveis', () => {
const target = { l1: { l2: { l3: { a: 1, b: 2 } } } };
const source = { l1: { l2: { l3: { b: 20, c: 30 } } } };
const result = deepMerge(target, source);

expect(result.l1.l2.l3).toEqual({ a: 1, b: 20, c: 30 });
});
});

// ============================================================================
// mergeAll
// ============================================================================

describe('mergeAll', () => {
it('deve fazer merge de múltiplas camadas em ordem', () => {
const base = { a: 1, b: 2 };
const override = { b: 20, c: 30 };
const final = { c: 300 };
const result = mergeAll(base, override, final);

expect(result).toEqual({ a: 1, b: 20, c: 300 });
});

it('deve ignorar camadas null/undefined', () => {
const result = mergeAll({ a: 1 }, null, undefined, { b: 2 });
expect(result).toEqual({ a: 1, b: 2 });
});

it('deve retornar objeto vazio quando sem argumentos', () => {
expect(mergeAll()).toEqual({});
});

it('deve retornar cópia quando apenas uma camada', () => {
const layer = { a: 1 };
const result = mergeAll(layer);

expect(result).toEqual({ a: 1 });
expect(result).not.toBe(layer); // Deve ser cópia
});
});
Loading