added cli-covarage tool and fixed more tests
This commit is contained in:
parent
b63e58784c
commit
b845a8eade
57 changed files with 11917 additions and 295 deletions
519
libs/core/config/test/utils.test.ts
Normal file
519
libs/core/config/test/utils.test.ts
Normal file
|
|
@ -0,0 +1,519 @@
|
|||
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
SecretValue,
|
||||
secret,
|
||||
isSecret,
|
||||
redactSecrets,
|
||||
isSecretEnvVar,
|
||||
wrapSecretEnvVars,
|
||||
secretSchema,
|
||||
secretStringSchema,
|
||||
COMMON_SECRET_PATTERNS,
|
||||
validateConfig,
|
||||
checkRequiredEnvVars,
|
||||
validateCompleteness,
|
||||
formatValidationResult,
|
||||
createStrictSchema,
|
||||
mergeSchemas,
|
||||
type ValidationResult,
|
||||
} from '../src';
|
||||
|
||||
describe('Config Utils', () => {
|
||||
describe('SecretValue', () => {
|
||||
it('should create a secret value', () => {
|
||||
const secret = new SecretValue('my-secret');
|
||||
expect(secret).toBeInstanceOf(SecretValue);
|
||||
expect(secret.toString()).toBe('***');
|
||||
});
|
||||
|
||||
it('should use custom mask', () => {
|
||||
const secret = new SecretValue('my-secret', 'HIDDEN');
|
||||
expect(secret.toString()).toBe('HIDDEN');
|
||||
});
|
||||
|
||||
it('should reveal value with reason', () => {
|
||||
const secret = new SecretValue('my-secret');
|
||||
expect(secret.reveal('testing')).toBe('my-secret');
|
||||
});
|
||||
|
||||
it('should throw when revealing without reason', () => {
|
||||
const secret = new SecretValue('my-secret');
|
||||
expect(() => secret.reveal('')).toThrow('Reason required for revealing secret value');
|
||||
});
|
||||
|
||||
it('should mask value in JSON', () => {
|
||||
const secret = new SecretValue('my-secret');
|
||||
expect(JSON.stringify(secret)).toBe('"***"');
|
||||
expect(secret.toJSON()).toBe('***');
|
||||
});
|
||||
|
||||
it('should compare values without revealing', () => {
|
||||
const secret = new SecretValue('my-secret');
|
||||
expect(secret.equals('my-secret')).toBe(true);
|
||||
expect(secret.equals('other-secret')).toBe(false);
|
||||
});
|
||||
|
||||
it('should map secret values', () => {
|
||||
const secret = new SecretValue('hello');
|
||||
const mapped = secret.map(val => val.toUpperCase(), 'testing transformation');
|
||||
expect(mapped.reveal('checking result')).toBe('HELLO');
|
||||
expect(mapped.toString()).toBe('***');
|
||||
});
|
||||
|
||||
it('should work with non-string types', () => {
|
||||
const numberSecret = new SecretValue(12345, 'XXX');
|
||||
expect(numberSecret.reveal('test')).toBe(12345);
|
||||
expect(numberSecret.toString()).toBe('XXX');
|
||||
|
||||
const objectSecret = new SecretValue({ key: 'value' }, '[OBJECT]');
|
||||
expect(objectSecret.reveal('test')).toEqual({ key: 'value' });
|
||||
expect(objectSecret.toString()).toBe('[OBJECT]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('secret helper function', () => {
|
||||
it('should create secret values', () => {
|
||||
const s = secret('my-secret');
|
||||
expect(s).toBeInstanceOf(SecretValue);
|
||||
expect(s.reveal('test')).toBe('my-secret');
|
||||
});
|
||||
|
||||
it('should accept custom mask', () => {
|
||||
const s = secret('my-secret', 'REDACTED');
|
||||
expect(s.toString()).toBe('REDACTED');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSecret', () => {
|
||||
it('should identify secret values', () => {
|
||||
expect(isSecret(new SecretValue('test'))).toBe(true);
|
||||
expect(isSecret(secret('test'))).toBe(true);
|
||||
expect(isSecret('test')).toBe(false);
|
||||
expect(isSecret(null)).toBe(false);
|
||||
expect(isSecret(undefined)).toBe(false);
|
||||
expect(isSecret({})).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('secretSchema', () => {
|
||||
it('should validate SecretValue instances', () => {
|
||||
const schema = secretSchema(z.string());
|
||||
const secretVal = new SecretValue('test');
|
||||
|
||||
expect(() => schema.parse(secretVal)).not.toThrow();
|
||||
expect(() => schema.parse('test')).toThrow();
|
||||
expect(() => schema.parse(null)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('secretStringSchema', () => {
|
||||
it('should transform string to SecretValue', () => {
|
||||
const result = secretStringSchema.parse('my-secret');
|
||||
expect(result).toBeInstanceOf(SecretValue);
|
||||
expect(result.reveal('test')).toBe('my-secret');
|
||||
});
|
||||
|
||||
it('should reject non-strings', () => {
|
||||
expect(() => secretStringSchema.parse(123)).toThrow();
|
||||
expect(() => secretStringSchema.parse(null)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('redactSecrets', () => {
|
||||
it('should redact specified paths', () => {
|
||||
const obj = {
|
||||
username: 'admin',
|
||||
password: 'secret123',
|
||||
nested: {
|
||||
apiKey: 'key123',
|
||||
public: 'visible',
|
||||
},
|
||||
};
|
||||
|
||||
const redacted = redactSecrets(obj, ['password', 'nested.apiKey']);
|
||||
|
||||
expect(redacted).toEqual({
|
||||
username: 'admin',
|
||||
password: '***REDACTED***',
|
||||
nested: {
|
||||
apiKey: '***REDACTED***',
|
||||
public: 'visible',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should redact SecretValue instances', () => {
|
||||
const obj = {
|
||||
normal: 'value',
|
||||
secret: new SecretValue('hidden', 'MASKED'),
|
||||
nested: {
|
||||
anotherSecret: secret('also-hidden'),
|
||||
},
|
||||
};
|
||||
|
||||
const redacted = redactSecrets(obj);
|
||||
|
||||
expect(redacted).toEqual({
|
||||
normal: 'value',
|
||||
secret: 'MASKED',
|
||||
nested: {
|
||||
anotherSecret: '***',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle arrays', () => {
|
||||
const obj = {
|
||||
items: [
|
||||
{ name: 'item1', secret: new SecretValue('s1') },
|
||||
{ name: 'item2', secret: new SecretValue('s2') },
|
||||
],
|
||||
};
|
||||
|
||||
const redacted = redactSecrets(obj);
|
||||
|
||||
expect(redacted.items).toEqual([
|
||||
{ name: 'item1', secret: '***' },
|
||||
{ name: 'item2', secret: '***' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle null and undefined', () => {
|
||||
const obj = {
|
||||
nullValue: null,
|
||||
undefinedValue: undefined,
|
||||
secret: new SecretValue('test'),
|
||||
};
|
||||
|
||||
const redacted = redactSecrets(obj);
|
||||
|
||||
expect(redacted).toEqual({
|
||||
nullValue: null,
|
||||
undefinedValue: undefined,
|
||||
secret: '***',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle non-existent paths gracefully', () => {
|
||||
const obj = { a: 'value' };
|
||||
const redacted = redactSecrets(obj, ['b.c.d']);
|
||||
expect(redacted).toEqual({ a: 'value' });
|
||||
});
|
||||
|
||||
it('should not modify original object', () => {
|
||||
const obj = { password: 'secret' };
|
||||
const original = { ...obj };
|
||||
redactSecrets(obj, ['password']);
|
||||
expect(obj).toEqual(original);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSecretEnvVar', () => {
|
||||
it('should identify common secret patterns', () => {
|
||||
// Positive cases
|
||||
expect(isSecretEnvVar('PASSWORD')).toBe(true);
|
||||
expect(isSecretEnvVar('DB_PASSWORD')).toBe(true);
|
||||
expect(isSecretEnvVar('API_KEY')).toBe(true);
|
||||
expect(isSecretEnvVar('API-KEY')).toBe(true);
|
||||
expect(isSecretEnvVar('SECRET_TOKEN')).toBe(true);
|
||||
expect(isSecretEnvVar('AUTH_TOKEN')).toBe(true);
|
||||
expect(isSecretEnvVar('PRIVATE_KEY')).toBe(true);
|
||||
expect(isSecretEnvVar('CREDENTIAL')).toBe(true);
|
||||
expect(isSecretEnvVar('password')).toBe(true); // Case insensitive
|
||||
|
||||
// Negative cases
|
||||
expect(isSecretEnvVar('USERNAME')).toBe(false);
|
||||
expect(isSecretEnvVar('PORT')).toBe(false);
|
||||
expect(isSecretEnvVar('DEBUG')).toBe(false);
|
||||
expect(isSecretEnvVar('NODE_ENV')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('wrapSecretEnvVars', () => {
|
||||
it('should wrap secret environment variables', () => {
|
||||
const env = {
|
||||
USERNAME: 'admin',
|
||||
PASSWORD: 'secret123',
|
||||
API_KEY: 'key123',
|
||||
PORT: '3000',
|
||||
};
|
||||
|
||||
const wrapped = wrapSecretEnvVars(env);
|
||||
|
||||
expect(wrapped.USERNAME).toBe('admin');
|
||||
expect(wrapped.PORT).toBe('3000');
|
||||
|
||||
expect(isSecret(wrapped.PASSWORD)).toBe(true);
|
||||
expect(isSecret(wrapped.API_KEY)).toBe(true);
|
||||
|
||||
const passwordSecret = wrapped.PASSWORD as SecretValue;
|
||||
expect(passwordSecret.reveal('test')).toBe('secret123');
|
||||
expect(passwordSecret.toString()).toBe('***PASSWORD***');
|
||||
});
|
||||
|
||||
it('should handle undefined values', () => {
|
||||
const env = {
|
||||
PASSWORD: undefined,
|
||||
USERNAME: 'admin',
|
||||
};
|
||||
|
||||
const wrapped = wrapSecretEnvVars(env);
|
||||
|
||||
expect(wrapped.PASSWORD).toBeUndefined();
|
||||
expect(wrapped.USERNAME).toBe('admin');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateConfig', () => {
|
||||
const schema = z.object({
|
||||
name: z.string(),
|
||||
port: z.number(),
|
||||
optional: z.string().optional(),
|
||||
});
|
||||
|
||||
it('should validate valid config', () => {
|
||||
const result = validateConfig({ name: 'app', port: 3000 }, schema);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return errors for invalid config', () => {
|
||||
const result = validateConfig({ name: 'app', port: 'invalid' }, schema);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toBeDefined();
|
||||
expect(result.errors![0].path).toBe('port');
|
||||
expect(result.errors![0].message).toContain('Expected number');
|
||||
});
|
||||
|
||||
it('should handle missing required fields', () => {
|
||||
const result = validateConfig({ port: 3000 }, schema);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toBeDefined();
|
||||
expect(result.errors![0].path).toBe('name');
|
||||
});
|
||||
|
||||
it('should rethrow non-Zod errors', () => {
|
||||
const badSchema = {
|
||||
parse: () => {
|
||||
throw new Error('Not a Zod error');
|
||||
},
|
||||
} as any;
|
||||
|
||||
expect(() => validateConfig({}, badSchema)).toThrow('Not a Zod error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkRequiredEnvVars', () => {
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
beforeEach(() => {
|
||||
// Clear environment
|
||||
for (const key in process.env) {
|
||||
delete process.env[key];
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore environment
|
||||
for (const key in process.env) {
|
||||
delete process.env[key];
|
||||
}
|
||||
Object.assign(process.env, originalEnv);
|
||||
});
|
||||
|
||||
it('should pass when all required vars are set', () => {
|
||||
process.env.API_KEY = 'key123';
|
||||
process.env.DATABASE_URL = 'postgres://...';
|
||||
|
||||
const result = checkRequiredEnvVars(['API_KEY', 'DATABASE_URL']);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should fail when required vars are missing', () => {
|
||||
process.env.API_KEY = 'key123';
|
||||
|
||||
const result = checkRequiredEnvVars(['API_KEY', 'DATABASE_URL', 'MISSING_VAR']);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toHaveLength(2);
|
||||
expect(result.errors![0].path).toBe('env.DATABASE_URL');
|
||||
expect(result.errors![1].path).toBe('env.MISSING_VAR');
|
||||
});
|
||||
|
||||
it('should handle empty required list', () => {
|
||||
const result = checkRequiredEnvVars([]);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateCompleteness', () => {
|
||||
it('should validate complete config', () => {
|
||||
const config = {
|
||||
database: {
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
credentials: {
|
||||
username: 'admin',
|
||||
password: 'secret',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = validateCompleteness(config, [
|
||||
'database.host',
|
||||
'database.port',
|
||||
'database.credentials.username',
|
||||
]);
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should detect missing values', () => {
|
||||
const config = {
|
||||
database: {
|
||||
host: 'localhost',
|
||||
credentials: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = validateCompleteness(config, [
|
||||
'database.host',
|
||||
'database.port',
|
||||
'database.credentials.username',
|
||||
]);
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toHaveLength(2);
|
||||
expect(result.errors![0].path).toBe('database.port');
|
||||
expect(result.errors![1].path).toBe('database.credentials.username');
|
||||
});
|
||||
|
||||
it('should handle null and undefined as missing', () => {
|
||||
const config = {
|
||||
a: null,
|
||||
b: undefined,
|
||||
c: 'value',
|
||||
};
|
||||
|
||||
const result = validateCompleteness(config, ['a', 'b', 'c']);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should handle non-existent paths', () => {
|
||||
const config = { a: 'value' };
|
||||
const result = validateCompleteness(config, ['b.c.d']);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors![0].path).toBe('b.c.d');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatValidationResult', () => {
|
||||
it('should format valid result', () => {
|
||||
const result: ValidationResult = { valid: true };
|
||||
const formatted = formatValidationResult(result);
|
||||
expect(formatted).toBe('✅ Configuration is valid');
|
||||
});
|
||||
|
||||
it('should format errors', () => {
|
||||
const result: ValidationResult = {
|
||||
valid: false,
|
||||
errors: [
|
||||
{ path: 'port', message: 'Expected number' },
|
||||
{
|
||||
path: 'database.host',
|
||||
message: 'Invalid value',
|
||||
expected: 'string',
|
||||
received: 'number',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const formatted = formatValidationResult(result);
|
||||
expect(formatted).toContain('❌ Configuration validation failed');
|
||||
expect(formatted).toContain('Errors:');
|
||||
expect(formatted).toContain('- port: Expected number');
|
||||
expect(formatted).toContain('- database.host: Invalid value');
|
||||
expect(formatted).toContain('Expected: string, Received: number');
|
||||
});
|
||||
|
||||
it('should format warnings', () => {
|
||||
const result: ValidationResult = {
|
||||
valid: true,
|
||||
warnings: [
|
||||
{ path: 'deprecated.feature', message: 'This feature is deprecated' },
|
||||
],
|
||||
};
|
||||
|
||||
const formatted = formatValidationResult(result);
|
||||
expect(formatted).toContain('✅ Configuration is valid');
|
||||
expect(formatted).toContain('Warnings:');
|
||||
expect(formatted).toContain('- deprecated.feature: This feature is deprecated');
|
||||
});
|
||||
});
|
||||
|
||||
describe('createStrictSchema', () => {
|
||||
it('should create strict schema', () => {
|
||||
const schema = createStrictSchema({
|
||||
name: z.string(),
|
||||
age: z.number(),
|
||||
});
|
||||
|
||||
expect(() => schema.parse({ name: 'John', age: 30 })).not.toThrow();
|
||||
expect(() => schema.parse({ name: 'John', age: 30, extra: 'field' })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeSchemas', () => {
|
||||
it('should merge two schemas', () => {
|
||||
const schema1 = z.object({ a: z.string() });
|
||||
const schema2 = z.object({ b: z.number() });
|
||||
|
||||
const merged = mergeSchemas(schema1, schema2);
|
||||
const result = merged.parse({ a: 'test', b: 123 });
|
||||
|
||||
expect(result).toEqual({ a: 'test', b: 123 });
|
||||
});
|
||||
|
||||
it('should merge multiple schemas', () => {
|
||||
const schema1 = z.object({ a: z.string() });
|
||||
const schema2 = z.object({ b: z.number() });
|
||||
const schema3 = z.object({ c: z.boolean() });
|
||||
|
||||
const merged = mergeSchemas(schema1, schema2, schema3);
|
||||
const result = merged.parse({ a: 'test', b: 123, c: true });
|
||||
|
||||
expect(result).toEqual({ a: 'test', b: 123, c: true });
|
||||
});
|
||||
|
||||
it('should throw with less than two schemas', () => {
|
||||
expect(() => mergeSchemas(z.object({}))).toThrow('At least two schemas required');
|
||||
expect(() => mergeSchemas()).toThrow('At least two schemas required');
|
||||
});
|
||||
|
||||
it('should handle overlapping fields', () => {
|
||||
const schema1 = z.object({ a: z.string(), shared: z.string() });
|
||||
const schema2 = z.object({ b: z.number(), shared: z.string() });
|
||||
|
||||
const merged = mergeSchemas(schema1, schema2);
|
||||
|
||||
// Both schemas require 'shared' to be a string
|
||||
expect(() => merged.parse({ a: 'test', b: 123, shared: 'value' })).not.toThrow();
|
||||
expect(() => merged.parse({ a: 'test', b: 123, shared: 123 })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('COMMON_SECRET_PATTERNS', () => {
|
||||
it('should be an array of RegExp', () => {
|
||||
expect(Array.isArray(COMMON_SECRET_PATTERNS)).toBe(true);
|
||||
expect(COMMON_SECRET_PATTERNS.length).toBeGreaterThan(0);
|
||||
|
||||
for (const pattern of COMMON_SECRET_PATTERNS) {
|
||||
expect(pattern).toBeInstanceOf(RegExp);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue