Skip to content

Commit 32e0dd7

Browse files
committed
test: add config and rate limit coverage
1 parent 11c130d commit 32e0dd7

3 files changed

Lines changed: 706 additions & 0 deletions

File tree

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
/**
2+
* Testes unitários para env-interpolator
3+
*
4+
* Cobre interpolateString, interpolateEnvVars e lintEnvPatterns.
5+
*
6+
* @see .aiox-core/core/config/env-interpolator.js
7+
* @issue #52
8+
*/
9+
10+
'use strict';
11+
12+
const {
13+
interpolateString,
14+
interpolateEnvVars,
15+
lintEnvPatterns,
16+
ENV_VAR_PATTERN,
17+
} = require('../../../.aiox-core/core/config/env-interpolator');
18+
19+
// ============================================================================
20+
// interpolateString
21+
// ============================================================================
22+
23+
describe('interpolateString', () => {
24+
const ORIGINAL_ENV = process.env;
25+
26+
beforeEach(() => {
27+
process.env = { ...ORIGINAL_ENV };
28+
});
29+
30+
afterAll(() => {
31+
process.env = ORIGINAL_ENV;
32+
});
33+
34+
it('deve resolver ${VAR} existente', () => {
35+
process.env.MY_VAR = 'hello';
36+
expect(interpolateString('${MY_VAR}')).toBe('hello');
37+
});
38+
39+
it('deve resolver ${VAR:-default} quando VAR existe', () => {
40+
process.env.MY_VAR = 'real';
41+
expect(interpolateString('${MY_VAR:-fallback}')).toBe('real');
42+
});
43+
44+
it('deve usar default quando VAR não existe', () => {
45+
delete process.env.MISSING_VAR;
46+
expect(interpolateString('${MISSING_VAR:-fallback}')).toBe('fallback');
47+
});
48+
49+
it('deve retornar string vazia e gerar warning quando VAR não existe sem default', () => {
50+
delete process.env.MISSING_VAR;
51+
const warnings = [];
52+
const result = interpolateString('${MISSING_VAR}', { warnings });
53+
54+
expect(result).toBe('');
55+
expect(warnings).toHaveLength(1);
56+
expect(warnings[0]).toContain('MISSING_VAR');
57+
});
58+
59+
it('deve resolver múltiplas variáveis na mesma string', () => {
60+
process.env.HOST = 'localhost';
61+
process.env.PORT = '3000';
62+
expect(interpolateString('${HOST}:${PORT}')).toBe('localhost:3000');
63+
});
64+
65+
it('deve preservar texto sem padrão ${...}', () => {
66+
expect(interpolateString('no variables here')).toBe('no variables here');
67+
});
68+
69+
it('deve resolver default vazio ${VAR:-}', () => {
70+
delete process.env.EMPTY_DEFAULT;
71+
expect(interpolateString('${EMPTY_DEFAULT:-}')).toBe('');
72+
});
73+
});
74+
75+
// ============================================================================
76+
// interpolateEnvVars
77+
// ============================================================================
78+
79+
describe('interpolateEnvVars', () => {
80+
const ORIGINAL_ENV = process.env;
81+
82+
beforeEach(() => {
83+
process.env = { ...ORIGINAL_ENV };
84+
});
85+
86+
afterAll(() => {
87+
process.env = ORIGINAL_ENV;
88+
});
89+
90+
it('deve interpolar strings em objetos aninhados', () => {
91+
process.env.DB_HOST = 'pg.example.com';
92+
const config = {
93+
database: {
94+
host: '${DB_HOST}',
95+
port: 5432,
96+
},
97+
};
98+
99+
const result = interpolateEnvVars(config);
100+
expect(result.database.host).toBe('pg.example.com');
101+
expect(result.database.port).toBe(5432);
102+
});
103+
104+
it('deve interpolar strings em arrays', () => {
105+
process.env.ITEM = 'resolved';
106+
const config = ['${ITEM}', 'static'];
107+
108+
const result = interpolateEnvVars(config);
109+
expect(result).toEqual(['resolved', 'static']);
110+
});
111+
112+
it('deve preservar números, booleanos e null', () => {
113+
expect(interpolateEnvVars(42)).toBe(42);
114+
expect(interpolateEnvVars(true)).toBe(true);
115+
expect(interpolateEnvVars(null)).toBeNull();
116+
});
117+
118+
it('deve processar objetos profundamente aninhados', () => {
119+
process.env.SECRET = 'top-secret';
120+
const config = {
121+
l1: { l2: { l3: { key: '${SECRET}' } } },
122+
};
123+
124+
const result = interpolateEnvVars(config);
125+
expect(result.l1.l2.l3.key).toBe('top-secret');
126+
});
127+
128+
it('deve coletar warnings de variáveis ausentes', () => {
129+
delete process.env.UNKNOWN;
130+
const warnings = [];
131+
interpolateEnvVars({ key: '${UNKNOWN}' }, { warnings });
132+
133+
expect(warnings).toHaveLength(1);
134+
expect(warnings[0]).toContain('UNKNOWN');
135+
});
136+
137+
it('não deve mutar o config original', () => {
138+
process.env.VAL = 'new';
139+
const config = { a: '${VAL}' };
140+
const configCopy = JSON.parse(JSON.stringify(config));
141+
142+
interpolateEnvVars(config);
143+
expect(config).toEqual(configCopy);
144+
});
145+
});
146+
147+
// ============================================================================
148+
// lintEnvPatterns
149+
// ============================================================================
150+
151+
describe('lintEnvPatterns', () => {
152+
it('deve detectar padrões ${...} em strings', () => {
153+
const config = { api: { key: '${API_KEY}' } };
154+
const findings = lintEnvPatterns(config, 'config.yaml');
155+
156+
expect(findings).toHaveLength(1);
157+
expect(findings[0]).toContain('config.yaml');
158+
expect(findings[0]).toContain('api.key');
159+
expect(findings[0]).toContain('${API_KEY}');
160+
});
161+
162+
it('deve detectar padrões em arrays', () => {
163+
const config = { hosts: ['static', '${DYNAMIC_HOST}'] };
164+
const findings = lintEnvPatterns(config, 'test.yaml');
165+
166+
expect(findings).toHaveLength(1);
167+
expect(findings[0]).toContain('hosts[1]');
168+
});
169+
170+
it('deve retornar array vazio quando não há padrões', () => {
171+
const config = { name: 'static', port: 3000 };
172+
const findings = lintEnvPatterns(config, 'clean.yaml');
173+
174+
expect(findings).toEqual([]);
175+
});
176+
177+
it('deve detectar múltiplos padrões', () => {
178+
const config = {
179+
db: { host: '${DB_HOST}', pass: '${DB_PASS}' },
180+
api: '${API_URL}',
181+
};
182+
const findings = lintEnvPatterns(config, 'app.yaml');
183+
184+
expect(findings).toHaveLength(3);
185+
});
186+
187+
it('deve funcionar com objetos profundamente aninhados', () => {
188+
const config = { a: { b: { c: { d: '${DEEP}' } } } };
189+
const findings = lintEnvPatterns(config, 'deep.yaml');
190+
191+
expect(findings).toHaveLength(1);
192+
expect(findings[0]).toContain('a.b.c.d');
193+
});
194+
});
195+
196+
// ============================================================================
197+
// ENV_VAR_PATTERN
198+
// ============================================================================
199+
200+
describe('ENV_VAR_PATTERN', () => {
201+
it('deve ser uma regex global', () => {
202+
expect(ENV_VAR_PATTERN).toBeInstanceOf(RegExp);
203+
expect(ENV_VAR_PATTERN.global).toBe(true);
204+
});
205+
206+
it('deve capturar nome da variável', () => {
207+
const match = '${MY_VAR}'.match(new RegExp(ENV_VAR_PATTERN.source));
208+
expect(match[1]).toBe('MY_VAR');
209+
});
210+
211+
it('deve capturar valor default', () => {
212+
const match = '${MY_VAR:-default}'.match(new RegExp(ENV_VAR_PATTERN.source));
213+
expect(match[1]).toBe('MY_VAR');
214+
expect(match[2]).toBe('default');
215+
});
216+
});
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/**
2+
* Testes unitários para merge-utils
3+
*
4+
* Cobre deepMerge, mergeAll e isPlainObject conforme ADR-PRO-002.
5+
*
6+
* @see .aiox-core/core/config/merge-utils.js
7+
* @issue #52
8+
*/
9+
10+
'use strict';
11+
12+
const { deepMerge, mergeAll, isPlainObject } = require('../../../.aiox-core/core/config/merge-utils');
13+
14+
// ============================================================================
15+
// isPlainObject
16+
// ============================================================================
17+
18+
describe('isPlainObject', () => {
19+
it('deve retornar true para objetos literais', () => {
20+
expect(isPlainObject({})).toBe(true);
21+
expect(isPlainObject({ a: 1 })).toBe(true);
22+
});
23+
24+
it('deve retornar true para Object.create(null)', () => {
25+
expect(isPlainObject(Object.create(null))).toBe(true);
26+
});
27+
28+
it('deve retornar false para arrays', () => {
29+
expect(isPlainObject([])).toBe(false);
30+
expect(isPlainObject([1, 2])).toBe(false);
31+
});
32+
33+
it('deve retornar false para null e undefined', () => {
34+
expect(isPlainObject(null)).toBe(false);
35+
expect(isPlainObject(undefined)).toBe(false);
36+
});
37+
38+
it('deve retornar false para primitivos', () => {
39+
expect(isPlainObject(42)).toBe(false);
40+
expect(isPlainObject('string')).toBe(false);
41+
expect(isPlainObject(true)).toBe(false);
42+
});
43+
44+
it('deve retornar false para Date e RegExp', () => {
45+
expect(isPlainObject(new Date())).toBe(false);
46+
expect(isPlainObject(/regex/)).toBe(false);
47+
});
48+
});
49+
50+
// ============================================================================
51+
// deepMerge
52+
// ============================================================================
53+
54+
describe('deepMerge', () => {
55+
it('deve fazer last-wins para escalares', () => {
56+
const result = deepMerge({ a: 1 }, { a: 2 });
57+
expect(result.a).toBe(2);
58+
});
59+
60+
it('deve preservar chaves do target ausentes no source', () => {
61+
const result = deepMerge({ a: 1, b: 2 }, { a: 10 });
62+
expect(result).toEqual({ a: 10, b: 2 });
63+
});
64+
65+
it('deve adicionar chaves novas do source', () => {
66+
const result = deepMerge({ a: 1 }, { b: 2 });
67+
expect(result).toEqual({ a: 1, b: 2 });
68+
});
69+
70+
it('deve fazer deep merge de objetos aninhados', () => {
71+
const target = { db: { host: 'localhost', port: 5432 } };
72+
const source = { db: { port: 3306, name: 'mydb' } };
73+
const result = deepMerge(target, source);
74+
75+
expect(result.db).toEqual({ host: 'localhost', port: 3306, name: 'mydb' });
76+
});
77+
78+
it('deve substituir arrays (não concatenar) por padrão', () => {
79+
const target = { tags: ['a', 'b'] };
80+
const source = { tags: ['c'] };
81+
const result = deepMerge(target, source);
82+
83+
expect(result.tags).toEqual(['c']);
84+
});
85+
86+
it('deve concatenar arrays com +append', () => {
87+
const target = { plugins: ['core', 'auth'] };
88+
const source = { 'plugins+append': ['analytics'] };
89+
const result = deepMerge(target, source);
90+
91+
expect(result.plugins).toEqual(['core', 'auth', 'analytics']);
92+
});
93+
94+
it('deve criar array quando +append mas target não tem o campo', () => {
95+
const target = {};
96+
const source = { 'items+append': [1, 2] };
97+
const result = deepMerge(target, source);
98+
99+
expect(result.items).toEqual([1, 2]);
100+
});
101+
102+
it('deve deletar chave quando value é null', () => {
103+
const target = { a: 1, b: 2, c: 3 };
104+
const source = { b: null };
105+
const result = deepMerge(target, source);
106+
107+
expect(result).toEqual({ a: 1, c: 3 });
108+
expect('b' in result).toBe(false);
109+
});
110+
111+
it('não deve mutar os inputs', () => {
112+
const target = { a: { x: 1 } };
113+
const source = { a: { y: 2 } };
114+
const targetCopy = JSON.parse(JSON.stringify(target));
115+
const sourceCopy = JSON.parse(JSON.stringify(source));
116+
117+
deepMerge(target, source);
118+
119+
expect(target).toEqual(targetCopy);
120+
expect(source).toEqual(sourceCopy);
121+
});
122+
123+
it('deve retornar source quando target não é objeto', () => {
124+
expect(deepMerge('string', { a: 1 })).toEqual({ a: 1 });
125+
expect(deepMerge(null, { a: 1 })).toEqual({ a: 1 });
126+
});
127+
128+
it('deve retornar target quando source é undefined', () => {
129+
expect(deepMerge({ a: 1 }, undefined)).toEqual({ a: 1 });
130+
});
131+
132+
it('deve fazer merge profundo em 3+ níveis', () => {
133+
const target = { l1: { l2: { l3: { a: 1, b: 2 } } } };
134+
const source = { l1: { l2: { l3: { b: 20, c: 30 } } } };
135+
const result = deepMerge(target, source);
136+
137+
expect(result.l1.l2.l3).toEqual({ a: 1, b: 20, c: 30 });
138+
});
139+
});
140+
141+
// ============================================================================
142+
// mergeAll
143+
// ============================================================================
144+
145+
describe('mergeAll', () => {
146+
it('deve fazer merge de múltiplas camadas em ordem', () => {
147+
const base = { a: 1, b: 2 };
148+
const override = { b: 20, c: 30 };
149+
const final = { c: 300 };
150+
const result = mergeAll(base, override, final);
151+
152+
expect(result).toEqual({ a: 1, b: 20, c: 300 });
153+
});
154+
155+
it('deve ignorar camadas null/undefined', () => {
156+
const result = mergeAll({ a: 1 }, null, undefined, { b: 2 });
157+
expect(result).toEqual({ a: 1, b: 2 });
158+
});
159+
160+
it('deve retornar objeto vazio quando sem argumentos', () => {
161+
expect(mergeAll()).toEqual({});
162+
});
163+
164+
it('deve retornar cópia quando apenas uma camada', () => {
165+
const layer = { a: 1 };
166+
const result = mergeAll(layer);
167+
168+
expect(result).toEqual({ a: 1 });
169+
expect(result).not.toBe(layer); // Deve ser cópia
170+
});
171+
});

0 commit comments

Comments
 (0)