Files
broccolini-bot/tests/configSchema.test.js
indifferentketchup d89ac65823 audit week 3 [TEST-001]: bootstrap vitest + utils & configSchema smoke tests
Adds vitest@^4.1.5 as a devDependency, an `npm test` script (runs once,
non-watch), and tests/ with 87 smoke tests across two suites:

- tests/utils.test.js (42 tests) — pure functions in utils.js:
  stripEmailQuotes, stripMobileFooter, extractRawEmail, escapeHtml,
  sanitizeEmbedText, truncateEmbedDescription, replaceVariables,
  getPriorityEmoji, safeEqual, isStaff. Covers normal input, empty input,
  null/undefined, edge cases (CRLF normalization, oversize truncation,
  triple-backtick escape, code-block injection).

- tests/configSchema.test.js (45 tests) — getValidator type inference and
  per-validator validate() behavior for boolean / integer / hex_color /
  url / email / discord_id / discord_id_list / string fallback. Covers
  ALLOWED_CONFIG_KEYS membership, the ROLE_ID_TO_PING mid-key override,
  legacy "true"/"false"/numeric coercion in the string fallback, empty
  input as ok-with-empty, garbage rejection.

vitest.config.mjs sets `environment: 'node'`, `globals: false`, and
`include: ['tests/**/*.test.js']`. Foundation for the mongoose 6→8
upgrade — these tests don't touch the DB but confirm pure-function
behavior is preserved across dependency moves.
2026-05-08 20:38:41 +00:00

264 lines
7.7 KiB
JavaScript

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);
});
});