import { describe, it, expect } from 'vitest'; import { ALLOWED_CONFIG_KEYS, getValidator } from '../services/configSchema.js'; describe('ALLOWED_CONFIG_KEYS', () => { it('is a non-empty Set', () => { expect(ALLOWED_CONFIG_KEYS).toBeInstanceOf(Set); expect(ALLOWED_CONFIG_KEYS.size).toBeGreaterThan(0); }); it('includes well-known runtime config keys', () => { for (const k of [ 'TICKET_CATEGORY_ID', 'AUTO_CLOSE_ENABLED', 'GMAIL_POLL_INTERVAL_SECONDS', 'EMBED_COLOR_OPEN', 'GAME_LIST' ]) { expect(ALLOWED_CONFIG_KEYS.has(k)).toBe(true); } }); it('does not contain stale removed keys', () => { for (const k of ['BOSSCORD_API_KEY', 'SURGE_ENABLED']) { expect(ALLOWED_CONFIG_KEYS.has(k)).toBe(false); } }); }); describe('getValidator: type inference', () => { it('treats *_ENABLED as boolean', () => { const v = getValidator('AUTO_CLOSE_ENABLED'); expect(v.type).toBe('boolean'); }); it('treats *_ID as discord_id', () => { expect(getValidator('TICKET_CATEGORY_ID').type).toBe('discord_id'); }); it('overrides ROLE_ID_TO_PING (mid-key _ID) as discord_id', () => { expect(getValidator('ROLE_ID_TO_PING').type).toBe('discord_id'); }); it('treats *_HOURS / *_MINUTES / *_SECONDS as integer', () => { expect(getValidator('AUTO_CLOSE_AFTER_HOURS').type).toBe('integer'); expect(getValidator('RATE_LIMIT_WINDOW_MINUTES').type).toBe('integer'); expect(getValidator('GMAIL_POLL_INTERVAL_SECONDS').type).toBe('integer'); }); it('treats *_COLOR as hex_color', () => { expect(getValidator('EMBED_COLOR_OPEN').type).toBe('hex_color'); }); it('treats LOGO_URL as url', () => { expect(getValidator('LOGO_URL').type).toBe('url'); }); it('treats *_EMAIL as email', () => { expect(getValidator('SUPPORT_EMAIL').type).toBe('email'); }); it('falls back to string for unknown shapes', () => { expect(getValidator('TICKET_CATEGORY_NAME').type).toBe('string'); }); }); describe('boolean validator', () => { const v = getValidator('AUTO_CLOSE_ENABLED'); it('accepts the literal true/false', () => { expect(v.validate(true)).toEqual({ ok: true, coerced: true }); expect(v.validate(false)).toEqual({ ok: true, coerced: false }); }); it('accepts string "true"/"false"', () => { expect(v.validate('true')).toEqual({ ok: true, coerced: true }); expect(v.validate('false')).toEqual({ ok: true, coerced: false }); }); it('rejects garbage', () => { const res = v.validate('maybe'); expect(res.ok).toBe(false); expect(res.error).toMatch(/true or false/); }); }); describe('integer validator', () => { const v = getValidator('AUTO_CLOSE_AFTER_HOURS'); it('coerces a numeric string to a number', () => { expect(v.validate('72')).toEqual({ ok: true, coerced: 72 }); }); it('accepts zero', () => { expect(v.validate('0')).toEqual({ ok: true, coerced: 0 }); }); it('rejects non-numeric strings', () => { const res = v.validate('abc'); expect(res.ok).toBe(false); expect(res.error).toMatch(/whole number/); }); it('rejects floats', () => { expect(v.validate('1.5').ok).toBe(false); }); it('rejects negative integers', () => { expect(v.validate('-5').ok).toBe(false); }); it('treats empty input as ok with empty coerced value', () => { expect(v.validate('')).toEqual({ ok: true, coerced: '' }); expect(v.validate(null)).toEqual({ ok: true, coerced: '' }); expect(v.validate(undefined)).toEqual({ ok: true, coerced: '' }); }); }); describe('hex_color validator', () => { const v = getValidator('EMBED_COLOR_OPEN'); it('accepts 0xRRGGBB form', () => { expect(v.validate('0xFF00AA')).toEqual({ ok: true, coerced: '0xFF00AA' }); }); it('accepts #RRGGBB form and normalizes to 0xRRGGBB', () => { expect(v.validate('#ff00aa')).toEqual({ ok: true, coerced: '0xFF00AA' }); }); it('accepts bare RRGGBB and normalizes', () => { expect(v.validate('00ff00')).toEqual({ ok: true, coerced: '0x00FF00' }); }); it('rejects 3-digit shorthand', () => { expect(v.validate('#abc').ok).toBe(false); }); it('rejects garbage', () => { expect(v.validate('purple').ok).toBe(false); }); it('treats empty as ok', () => { expect(v.validate('')).toEqual({ ok: true, coerced: '' }); }); }); describe('url validator (LOGO_URL)', () => { const v = getValidator('LOGO_URL'); it('accepts a full URL', () => { expect(v.validate('https://example.com/logo.png')).toEqual({ ok: true, coerced: 'https://example.com/logo.png' }); }); it('rejects bare hostnames', () => { expect(v.validate('example.com').ok).toBe(false); }); it('treats empty as ok', () => { expect(v.validate('')).toEqual({ ok: true, coerced: '' }); }); }); describe('discord_id validator', () => { const v = getValidator('TICKET_CATEGORY_ID'); it('accepts an 18-digit snowflake', () => { expect(v.validate('123456789012345678')).toEqual({ ok: true, coerced: '123456789012345678' }); }); it('accepts a 20-digit snowflake', () => { const id = '12345678901234567890'; expect(v.validate(id)).toEqual({ ok: true, coerced: id }); }); it('rejects too-short IDs', () => { expect(v.validate('12345').ok).toBe(false); }); it('rejects non-numeric strings', () => { expect(v.validate('not-an-id').ok).toBe(false); }); it('treats empty as ok', () => { expect(v.validate('')).toEqual({ ok: true, coerced: '' }); }); }); describe('discord_id_list validator', () => { // ADDITIONAL_STAFF_ROLES (a real comma-list) ends in _ROLES, not _IDS, so it // hits the string fallback. discord_id_list only fires for `*_IDS` keys, so // exercise it with a hypothetical name. const v = getValidator('STAFF_USER_IDS'); it('infers type discord_id_list for *_IDS keys', () => { expect(v.type).toBe('discord_id_list'); }); it('accepts a single ID', () => { expect(v.validate('123456789012345678')) .toEqual({ ok: true, coerced: '123456789012345678' }); }); it('accepts a comma-separated list and trims spaces', () => { expect(v.validate('123456789012345678, 987654321098765432')) .toEqual({ ok: true, coerced: '123456789012345678,987654321098765432' }); }); it('rejects if any segment is not a snowflake', () => { const res = v.validate('123456789012345678,nope'); expect(res.ok).toBe(false); expect(res.error).toMatch(/not a Discord ID/); }); it('treats empty as ok', () => { expect(v.validate('')).toEqual({ ok: true, coerced: '' }); }); }); describe('string validator (fallback)', () => { const v = getValidator('TICKET_CATEGORY_NAME'); it('coerces "true"/"false" to booleans (legacy)', () => { expect(v.validate('true')).toEqual({ ok: true, coerced: true }); expect(v.validate('false')).toEqual({ ok: true, coerced: false }); }); it('coerces numeric-looking strings to numbers (legacy)', () => { expect(v.validate('42')).toEqual({ ok: true, coerced: 42 }); expect(v.validate('3.14')).toEqual({ ok: true, coerced: 3.14 }); }); it('passes plain strings through', () => { expect(v.validate('Open Tickets')).toEqual({ ok: true, coerced: 'Open Tickets' }); }); it('passes empty string through unchanged', () => { expect(v.validate('')).toEqual({ ok: true, coerced: '' }); }); it('rejects null', () => { expect(v.validate(null).ok).toBe(false); }); }); describe('email validator', () => { const v = getValidator('SUPPORT_EMAIL'); it('accepts valid email', () => { expect(v.validate('support@example.com')) .toEqual({ ok: true, coerced: 'support@example.com' }); }); it('rejects malformed strings', () => { expect(v.validate('not-an-email').ok).toBe(false); expect(v.validate('a@').ok).toBe(false); expect(v.validate('@b').ok).toBe(false); }); });