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

912 lines
24 KiB
TypeScript

import { describe, expect, it } from 'bun:test';
import { z } from 'zod';
import {
baseConfigSchema,
baseProviderConfigSchema,
browserConfigSchema,
databaseConfigSchema,
dragonflyConfigSchema,
environmentSchema,
eodProviderConfigSchema,
httpConfigSchema,
ibProviderConfigSchema,
loggingConfigSchema,
mongodbConfigSchema,
postgresConfigSchema,
providerConfigSchema,
proxyConfigSchema,
qmProviderConfigSchema,
questdbConfigSchema,
queueConfigSchema,
serviceConfigSchema,
webshareConfigSchema,
webshareProviderConfigSchema,
yahooProviderConfigSchema,
} from '../src/schemas';
describe('Config Schemas', () => {
describe('environmentSchema', () => {
it('should accept valid environments', () => {
expect(environmentSchema.parse('development')).toBe('development');
expect(environmentSchema.parse('test')).toBe('test');
expect(environmentSchema.parse('production')).toBe('production');
});
it('should reject invalid environments', () => {
expect(() => environmentSchema.parse('staging')).toThrow();
expect(() => environmentSchema.parse('dev')).toThrow();
expect(() => environmentSchema.parse('')).toThrow();
});
});
describe('baseConfigSchema', () => {
it('should accept minimal valid config', () => {
const config = baseConfigSchema.parse({});
expect(config).toEqual({
debug: false,
});
});
it('should accept full valid config', () => {
const input = {
environment: 'production',
name: 'test-app',
version: '1.0.0',
debug: true,
};
const config = baseConfigSchema.parse(input);
expect(config).toEqual(input);
});
it('should apply default values', () => {
const config = baseConfigSchema.parse({ name: 'app' });
expect(config.debug).toBe(false);
});
it('should reject invalid environment in base config', () => {
expect(() => baseConfigSchema.parse({ environment: 'invalid' })).toThrow();
});
});
describe('serviceConfigSchema', () => {
it('should require name and port', () => {
expect(() => serviceConfigSchema.parse({})).toThrow();
expect(() => serviceConfigSchema.parse({ name: 'test' })).toThrow();
expect(() => serviceConfigSchema.parse({ port: 3000 })).toThrow();
});
it('should accept minimal valid config', () => {
const config = serviceConfigSchema.parse({
name: 'test-service',
port: 3000,
});
expect(config).toEqual({
name: 'test-service',
port: 3000,
host: '0.0.0.0',
healthCheckPath: '/health',
metricsPath: '/metrics',
shutdownTimeout: 30000,
cors: {
enabled: true,
origin: '*',
credentials: true,
},
});
});
it('should accept full config', () => {
const input = {
name: 'test-service',
serviceName: 'test-service',
port: 8080,
host: 'localhost',
healthCheckPath: '/api/health',
metricsPath: '/api/metrics',
shutdownTimeout: 60000,
cors: {
enabled: false,
origin: ['http://localhost:3000', 'https://example.com'],
credentials: false,
},
};
const config = serviceConfigSchema.parse(input);
expect(config).toEqual(input);
});
it('should validate port range', () => {
expect(() => serviceConfigSchema.parse({ name: 'test', port: 0 })).toThrow();
expect(() => serviceConfigSchema.parse({ name: 'test', port: 65536 })).toThrow();
expect(() => serviceConfigSchema.parse({ name: 'test', port: -1 })).toThrow();
// Valid ports
expect(serviceConfigSchema.parse({ name: 'test', port: 1 }).port).toBe(1);
expect(serviceConfigSchema.parse({ name: 'test', port: 65535 }).port).toBe(65535);
});
it('should handle CORS origin as string or array', () => {
const stringOrigin = serviceConfigSchema.parse({
name: 'test',
port: 3000,
cors: { origin: 'http://localhost:3000' },
});
expect(stringOrigin.cors.origin).toBe('http://localhost:3000');
const arrayOrigin = serviceConfigSchema.parse({
name: 'test',
port: 3000,
cors: { origin: ['http://localhost:3000', 'https://example.com'] },
});
expect(arrayOrigin.cors.origin).toEqual(['http://localhost:3000', 'https://example.com']);
});
});
describe('loggingConfigSchema', () => {
it('should accept minimal config with defaults', () => {
const config = loggingConfigSchema.parse({});
expect(config).toEqual({
level: 'info',
format: 'json',
hideObject: false,
});
});
it('should accept all log levels', () => {
const levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'];
for (const level of levels) {
const config = loggingConfigSchema.parse({ level });
expect(config.level).toBe(level);
}
});
it('should reject invalid log levels', () => {
expect(() => loggingConfigSchema.parse({ level: 'verbose' })).toThrow();
expect(() => loggingConfigSchema.parse({ level: 'warning' })).toThrow();
});
it('should accept loki configuration', () => {
const config = loggingConfigSchema.parse({
loki: {
enabled: true,
host: 'loki.example.com',
port: 3100,
labels: { app: 'test', env: 'prod' },
},
});
expect(config.loki).toEqual({
enabled: true,
host: 'loki.example.com',
port: 3100,
labels: { app: 'test', env: 'prod' },
});
});
it('should apply loki defaults', () => {
const config = loggingConfigSchema.parse({
loki: { enabled: true },
});
expect(config.loki).toEqual({
enabled: true,
host: 'localhost',
port: 3100,
labels: {},
});
});
});
describe('queueConfigSchema', () => {
it('should accept minimal config with defaults', () => {
const config = queueConfigSchema.parse({
redis: {}, // redis is required, but its properties have defaults
});
expect(config).toEqual({
enabled: true,
redis: {
host: 'localhost',
port: 6379,
db: 1,
},
workers: 1,
concurrency: 1,
enableScheduledJobs: true,
defaultJobOptions: {
attempts: 3,
backoff: {
type: 'exponential',
delay: 1000,
},
removeOnComplete: 100,
removeOnFail: 100,
},
});
});
it('should accept full config', () => {
const input = {
enabled: false,
redis: {
host: 'redis.example.com',
port: 6380,
password: 'secret',
db: 2,
},
workers: 4,
concurrency: 10,
enableScheduledJobs: false,
defaultJobOptions: {
attempts: 5,
backoff: {
type: 'fixed' as const,
delay: 2000,
},
removeOnComplete: 50,
removeOnFail: 200,
timeout: 60000,
},
};
const config = queueConfigSchema.parse(input);
expect(config).toEqual(input);
});
it('should validate backoff type', () => {
const exponential = queueConfigSchema.parse({
redis: {},
defaultJobOptions: { backoff: { type: 'exponential' } },
});
expect(exponential.defaultJobOptions.backoff.type).toBe('exponential');
const fixed = queueConfigSchema.parse({
redis: {},
defaultJobOptions: { backoff: { type: 'fixed' } },
});
expect(fixed.defaultJobOptions.backoff.type).toBe('fixed');
expect(() =>
queueConfigSchema.parse({
redis: {},
defaultJobOptions: { backoff: { type: 'linear' } },
})
).toThrow();
});
});
describe('httpConfigSchema', () => {
it('should accept minimal config with defaults', () => {
const config = httpConfigSchema.parse({});
expect(config).toEqual({
timeout: 30000,
retries: 3,
retryDelay: 1000,
});
});
it('should accept full config', () => {
const input = {
timeout: 60000,
retries: 5,
retryDelay: 2000,
userAgent: 'MyApp/1.0',
proxy: {
enabled: true,
url: 'http://proxy.example.com:8080',
auth: {
username: 'user',
password: 'pass',
},
},
};
const config = httpConfigSchema.parse(input);
expect(config).toEqual(input);
});
it('should validate proxy URL', () => {
expect(() =>
httpConfigSchema.parse({
proxy: { url: 'not-a-url' },
})
).toThrow();
const validProxy = httpConfigSchema.parse({
proxy: { url: 'http://proxy.example.com' },
});
expect(validProxy.proxy?.url).toBe('http://proxy.example.com');
});
});
describe('webshareConfigSchema', () => {
it('should accept minimal config with defaults', () => {
const config = webshareConfigSchema.parse({});
expect(config).toEqual({
apiUrl: 'https://proxy.webshare.io/api/v2/',
enabled: true,
});
});
it('should accept full config', () => {
const input = {
apiKey: 'test-api-key',
apiUrl: 'https://custom.webshare.io/api/v3/',
enabled: false,
};
const config = webshareConfigSchema.parse(input);
expect(config).toEqual(input);
});
});
describe('browserConfigSchema', () => {
it('should accept minimal config with defaults', () => {
const config = browserConfigSchema.parse({});
expect(config).toEqual({
headless: true,
timeout: 30000,
});
});
it('should accept custom values', () => {
const config = browserConfigSchema.parse({
headless: false,
timeout: 60000,
});
expect(config).toEqual({
headless: false,
timeout: 60000,
});
});
});
describe('proxyConfigSchema', () => {
it('should accept minimal config with defaults', () => {
const config = proxyConfigSchema.parse({});
expect(config).toEqual({
enabled: false,
cachePrefix: 'proxy:',
ttl: 3600,
});
});
it('should accept full config', () => {
const input = {
enabled: true,
cachePrefix: 'custom:proxy:',
ttl: 7200,
webshare: {
apiKey: 'test-key',
apiUrl: 'https://api.webshare.io/v2/',
},
};
const config = proxyConfigSchema.parse(input);
expect(config).toEqual(input);
});
});
describe('Schema Composition', () => {
it('should be able to compose schemas', () => {
const appConfigSchema = z.object({
base: baseConfigSchema,
service: serviceConfigSchema,
logging: loggingConfigSchema,
});
const config = appConfigSchema.parse({
base: {
name: 'test-app',
version: '1.0.0',
},
service: {
name: 'test-service',
port: 3000,
},
logging: {
level: 'debug',
},
});
expect(config.base.debug).toBe(false);
expect(config.service.host).toBe('0.0.0.0');
expect(config.logging.format).toBe('json');
});
});
describe('Edge Cases', () => {
it('should handle empty strings appropriately', () => {
// Empty strings are allowed by z.string() unless .min(1) is specified
const serviceConfig = serviceConfigSchema.parse({ name: '', port: 3000 });
expect(serviceConfig.name).toBe('');
const baseConfig = baseConfigSchema.parse({ name: '' });
expect(baseConfig.name).toBe('');
});
it('should handle null values', () => {
expect(() => serviceConfigSchema.parse({ name: null, port: 3000 })).toThrow();
expect(() => queueConfigSchema.parse({ redis: {}, workers: null })).toThrow();
});
it('should handle undefined values for optional fields', () => {
const config = serviceConfigSchema.parse({
name: 'test',
port: 3000,
serviceName: undefined,
});
expect(config.serviceName).toBeUndefined();
});
it('should handle numeric strings for number fields', () => {
expect(() => serviceConfigSchema.parse({ name: 'test', port: '3000' })).toThrow();
expect(() => queueConfigSchema.parse({ redis: {}, workers: '4' })).toThrow();
});
it('should strip unknown properties', () => {
const config = baseConfigSchema.parse({
name: 'test',
unknownProp: 'should be removed',
});
expect('unknownProp' in config).toBe(false);
});
});
describe('postgresConfigSchema', () => {
it('should accept minimal config with defaults', () => {
const config = postgresConfigSchema.parse({
database: 'testdb',
user: 'testuser',
password: 'testpass',
});
expect(config).toEqual({
enabled: true,
host: 'localhost',
port: 5432,
database: 'testdb',
user: 'testuser',
password: 'testpass',
ssl: false,
poolSize: 10,
connectionTimeout: 30000,
idleTimeout: 10000,
});
});
it('should accept full config', () => {
const input = {
enabled: false,
host: 'db.example.com',
port: 5433,
database: 'proddb',
user: 'admin',
password: 'secret',
ssl: true,
poolSize: 20,
connectionTimeout: 60000,
idleTimeout: 30000,
};
const config = postgresConfigSchema.parse(input);
expect(config).toEqual(input);
});
it('should validate poolSize range', () => {
expect(() =>
postgresConfigSchema.parse({
database: 'testdb',
user: 'testuser',
password: 'testpass',
poolSize: 0,
})
).toThrow();
expect(() =>
postgresConfigSchema.parse({
database: 'testdb',
user: 'testuser',
password: 'testpass',
poolSize: 101,
})
).toThrow();
});
});
describe('questdbConfigSchema', () => {
it('should accept minimal config with defaults', () => {
const config = questdbConfigSchema.parse({});
expect(config).toEqual({
enabled: true,
host: 'localhost',
ilpPort: 9009,
httpPort: 9000,
pgPort: 8812,
database: 'questdb',
bufferSize: 65536,
flushInterval: 1000,
});
});
it('should accept full config', () => {
const input = {
enabled: false,
host: 'questdb.example.com',
ilpPort: 9010,
httpPort: 9001,
pgPort: 8813,
database: 'metrics',
user: 'admin',
password: 'secret',
bufferSize: 131072,
flushInterval: 2000,
};
const config = questdbConfigSchema.parse(input);
expect(config).toEqual(input);
});
});
describe('mongodbConfigSchema', () => {
it('should accept minimal config', () => {
const config = mongodbConfigSchema.parse({
uri: 'mongodb://localhost:27017',
database: 'testdb',
});
expect(config).toEqual({
enabled: true,
uri: 'mongodb://localhost:27017',
database: 'testdb',
poolSize: 10,
});
});
it('should accept full config', () => {
const input = {
enabled: false,
uri: 'mongodb://user:pass@cluster.mongodb.net',
database: 'proddb',
poolSize: 50,
host: 'cluster.mongodb.net',
port: 27017,
user: 'admin',
password: 'secret',
authSource: 'admin',
replicaSet: 'rs0',
};
const config = mongodbConfigSchema.parse(input);
expect(config).toEqual(input);
});
it('should validate URI format', () => {
expect(() =>
mongodbConfigSchema.parse({
uri: 'invalid-uri',
database: 'testdb',
})
).toThrow();
});
it('should validate poolSize range', () => {
expect(() =>
mongodbConfigSchema.parse({
uri: 'mongodb://localhost',
database: 'testdb',
poolSize: 0,
})
).toThrow();
expect(() =>
mongodbConfigSchema.parse({
uri: 'mongodb://localhost',
database: 'testdb',
poolSize: 101,
})
).toThrow();
});
});
describe('dragonflyConfigSchema', () => {
it('should accept minimal config with defaults', () => {
const config = dragonflyConfigSchema.parse({});
expect(config).toEqual({
enabled: true,
host: 'localhost',
port: 6379,
db: 0,
maxRetries: 3,
retryDelay: 100,
});
});
it('should accept full config', () => {
const input = {
enabled: false,
host: 'cache.example.com',
port: 6380,
password: 'secret',
db: 5,
keyPrefix: 'app:',
ttl: 3600,
maxRetries: 5,
retryDelay: 200,
};
const config = dragonflyConfigSchema.parse(input);
expect(config).toEqual(input);
});
it('should validate db range', () => {
expect(() => dragonflyConfigSchema.parse({ db: -1 })).toThrow();
expect(() => dragonflyConfigSchema.parse({ db: 16 })).toThrow();
});
});
describe('databaseConfigSchema', () => {
it('should accept complete database configuration', () => {
const config = databaseConfigSchema.parse({
postgres: {
database: 'testdb',
user: 'testuser',
password: 'testpass',
},
questdb: {},
mongodb: {
uri: 'mongodb://localhost',
database: 'testdb',
},
dragonfly: {},
});
expect(config.postgres.host).toBe('localhost');
expect(config.questdb.enabled).toBe(true);
expect(config.mongodb.poolSize).toBe(10);
expect(config.dragonfly.port).toBe(6379);
});
});
describe('baseProviderConfigSchema', () => {
it('should accept minimal config with defaults', () => {
const config = baseProviderConfigSchema.parse({
name: 'test-provider',
});
expect(config).toEqual({
name: 'test-provider',
enabled: true,
priority: 0,
timeout: 30000,
retries: 3,
});
});
it('should accept full config', () => {
const input = {
name: 'test-provider',
enabled: false,
priority: 10,
rateLimit: {
maxRequests: 50,
windowMs: 30000,
},
timeout: 60000,
retries: 5,
};
const config = baseProviderConfigSchema.parse(input);
expect(config).toEqual(input);
});
});
describe('eodProviderConfigSchema', () => {
it('should accept minimal config', () => {
const config = eodProviderConfigSchema.parse({
name: 'eod',
apiKey: 'test-key',
});
expect(config).toEqual({
name: 'eod',
apiKey: 'test-key',
enabled: true,
priority: 0,
timeout: 30000,
retries: 3,
baseUrl: 'https://eodhistoricaldata.com/api',
tier: 'free',
});
});
it('should validate tier values', () => {
expect(() =>
eodProviderConfigSchema.parse({
name: 'eod',
apiKey: 'test-key',
tier: 'premium',
})
).toThrow();
const validTiers = ['free', 'fundamentals', 'all-in-one'];
for (const tier of validTiers) {
const config = eodProviderConfigSchema.parse({
name: 'eod',
apiKey: 'test-key',
tier,
});
expect(config.tier).toBe(tier);
}
});
});
describe('ibProviderConfigSchema', () => {
it('should accept minimal config', () => {
const config = ibProviderConfigSchema.parse({
name: 'ib',
});
expect(config).toEqual({
name: 'ib',
enabled: true,
priority: 0,
timeout: 30000,
retries: 3,
gateway: {
host: 'localhost',
port: 5000,
clientId: 1,
},
marketDataType: 'delayed',
});
});
it('should accept full config', () => {
const input = {
name: 'ib',
enabled: false,
priority: 5,
gateway: {
host: 'gateway.example.com',
port: 7497,
clientId: 99,
},
account: 'DU123456',
marketDataType: 'live' as const,
};
const config = ibProviderConfigSchema.parse(input);
expect(config).toEqual(expect.objectContaining(input));
});
it('should validate marketDataType', () => {
expect(() =>
ibProviderConfigSchema.parse({
name: 'ib',
marketDataType: 'realtime',
})
).toThrow();
const validTypes = ['live', 'delayed', 'frozen'];
for (const type of validTypes) {
const config = ibProviderConfigSchema.parse({
name: 'ib',
marketDataType: type,
});
expect(config.marketDataType).toBe(type);
}
});
});
describe('qmProviderConfigSchema', () => {
it('should require all credentials', () => {
expect(() =>
qmProviderConfigSchema.parse({
name: 'qm',
})
).toThrow();
const config = qmProviderConfigSchema.parse({
name: 'qm',
username: 'testuser',
password: 'testpass',
webmasterId: '12345',
});
expect(config.baseUrl).toBe('https://app.quotemedia.com/quotetools');
});
});
describe('yahooProviderConfigSchema', () => {
it('should accept minimal config', () => {
const config = yahooProviderConfigSchema.parse({
name: 'yahoo',
});
expect(config).toEqual({
name: 'yahoo',
enabled: true,
priority: 0,
timeout: 30000,
retries: 3,
baseUrl: 'https://query1.finance.yahoo.com',
cookieJar: true,
});
});
it('should accept crumb parameter', () => {
const config = yahooProviderConfigSchema.parse({
name: 'yahoo',
crumb: 'abc123xyz',
});
expect(config.crumb).toBe('abc123xyz');
});
});
describe('webshareProviderConfigSchema', () => {
it('should not require name like other providers', () => {
const config = webshareProviderConfigSchema.parse({});
expect(config).toEqual({
apiUrl: 'https://proxy.webshare.io/api/v2/',
enabled: true,
});
});
it('should accept apiKey', () => {
const config = webshareProviderConfigSchema.parse({
apiKey: 'test-key',
enabled: false,
});
expect(config.apiKey).toBe('test-key');
expect(config.enabled).toBe(false);
});
});
describe('providerConfigSchema', () => {
it('should accept empty config', () => {
const config = providerConfigSchema.parse({});
expect(config).toEqual({});
});
it('should accept partial provider config', () => {
const config = providerConfigSchema.parse({
eod: {
name: 'eod',
apiKey: 'test-key',
},
yahoo: {
name: 'yahoo',
},
});
expect(config.eod?.apiKey).toBe('test-key');
expect(config.yahoo?.baseUrl).toBe('https://query1.finance.yahoo.com');
expect(config.ib).toBeUndefined();
});
it('should accept full provider config', () => {
const config = providerConfigSchema.parse({
eod: {
name: 'eod',
apiKey: 'eod-key',
tier: 'all-in-one',
},
ib: {
name: 'ib',
gateway: {
host: 'gateway.ib.com',
port: 7497,
clientId: 2,
},
},
qm: {
name: 'qm',
username: 'user',
password: 'pass',
webmasterId: '123',
},
yahoo: {
name: 'yahoo',
crumb: 'xyz',
},
webshare: {
apiKey: 'ws-key',
},
});
expect(config.eod?.tier).toBe('all-in-one');
expect(config.ib?.gateway.port).toBe(7497);
expect(config.qm?.username).toBe('user');
expect(config.yahoo?.crumb).toBe('xyz');
expect(config.webshare?.apiKey).toBe('ws-key');
});
});
});