stock-bot/libs/core/config/test/utils.test.ts
2025-06-26 16:12:27 -04:00

517 lines
16 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import { z } from 'zod';
import {
checkRequiredEnvVars,
COMMON_SECRET_PATTERNS,
createStrictSchema,
formatValidationResult,
isSecret,
isSecretEnvVar,
mergeSchemas,
redactSecrets,
secret,
secretSchema,
secretStringSchema,
SecretValue,
validateCompleteness,
validateConfig,
wrapSecretEnvVars,
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);
}
});
});
});