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.
This commit is contained in:
2026-05-08 20:38:41 +00:00
parent adcd9dd9c9
commit d89ac65823
5 changed files with 1750 additions and 2 deletions

263
tests/configSchema.test.js Normal file
View File

@@ -0,0 +1,263 @@
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);
});
});

234
tests/utils.test.js Normal file
View File

@@ -0,0 +1,234 @@
import { describe, it, expect } from 'vitest';
import {
stripEmailQuotes,
stripMobileFooter,
extractRawEmail,
escapeHtml,
sanitizeEmbedText,
truncateEmbedDescription,
replaceVariables,
getPriorityEmoji,
safeEqual,
isStaff
} from '../utils.js';
describe('stripEmailQuotes', () => {
it('strips "On X wrote:" reply quote', () => {
const input = 'My reply.\nOn Mon, May 5, 2025 at 1:00 PM Bob <bob@x.com> wrote:\n> previous message';
expect(stripEmailQuotes(input)).toBe('My reply.');
});
it('strips "From: …" reply header block', () => {
const input = 'New reply text.\nFrom: Bob <bob@x.com>\nSent: Monday\nSubject: Re: foo';
expect(stripEmailQuotes(input)).toBe('New reply text.');
});
it('strips "_____" signature underline', () => {
const input = 'My message.\n_____\nold thread content';
expect(stripEmailQuotes(input)).toBe('My message.');
});
it('returns empty string for empty input', () => {
expect(stripEmailQuotes('')).toBe('');
});
it('trims whitespace when no marker is found', () => {
expect(stripEmailQuotes(' hello ')).toBe('hello');
});
it('keeps body intact when "On" appears mid-text without "wrote:"', () => {
expect(stripEmailQuotes('I clicked On the button.')).toBe('I clicked On the button.');
});
it('normalizes CRLF before scanning', () => {
const input = 'New reply.\r\nOn Monday Bob <b@x.com> wrote:\r\n> quoted';
expect(stripEmailQuotes(input)).toBe('New reply.');
});
});
describe('stripMobileFooter', () => {
it('removes "Sent from my iPhone"', () => {
expect(stripMobileFooter('Hi\nSent from my iPhone').trim()).toBe('Hi');
});
it('removes "Sent from my Android"', () => {
expect(stripMobileFooter('Hi\nSent from my Android').trim()).toBe('Hi');
});
it('removes "Sent from my Galaxy"', () => {
expect(stripMobileFooter('Hi\nSent from my Galaxy').trim()).toBe('Hi');
});
it('removes "Get Outlook for iOS"', () => {
expect(stripMobileFooter('Hi\nGet Outlook for iOS').trim()).toBe('Hi');
});
it('returns input unchanged when no footer present', () => {
expect(stripMobileFooter('Just a normal message')).toBe('Just a normal message');
});
it('returns null/undefined unchanged', () => {
expect(stripMobileFooter(null)).toBe(null);
expect(stripMobileFooter(undefined)).toBe(undefined);
});
it('returns empty string unchanged', () => {
expect(stripMobileFooter('')).toBe('');
});
});
describe('extractRawEmail', () => {
it('extracts address from "Name <email>" form', () => {
expect(extractRawEmail('Bob <bob@example.com>')).toBe('bob@example.com');
});
it('returns trimmed input when angle brackets absent', () => {
expect(extractRawEmail(' bob@example.com ')).toBe('bob@example.com');
});
it('handles quoted name', () => {
expect(extractRawEmail('"Bob, the Developer" <bob@example.com>')).toBe('bob@example.com');
});
});
describe('escapeHtml', () => {
it('escapes <, >, &, ", \'', () => {
expect(escapeHtml('<script>alert("xss")</script>'))
.toBe('&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;');
expect(escapeHtml("a & b's <foo>")).toBe('a &amp; b&#39;s &lt;foo&gt;');
});
it('returns empty string for null/undefined', () => {
expect(escapeHtml(null)).toBe('');
expect(escapeHtml(undefined)).toBe('');
});
it('passes through plain text unchanged', () => {
expect(escapeHtml('plain text')).toBe('plain text');
});
});
describe('sanitizeEmbedText', () => {
it('replaces triple-backticks to prevent code-block escape', () => {
expect(sanitizeEmbedText('```injected```')).toBe("'''injected'''");
});
it('trims whitespace', () => {
expect(sanitizeEmbedText(' hello ')).toBe('hello');
});
it('returns empty string for null/undefined', () => {
expect(sanitizeEmbedText(null)).toBe('');
expect(sanitizeEmbedText(undefined)).toBe('');
});
});
describe('truncateEmbedDescription', () => {
it('returns short strings unchanged', () => {
expect(truncateEmbedDescription('hi')).toBe('hi');
});
it('truncates at default 4096 with ellipsis', () => {
const big = 'a'.repeat(5000);
const out = truncateEmbedDescription(big);
expect(out.length).toBe(4096);
expect(out.endsWith('...')).toBe(true);
});
it('respects custom max', () => {
expect(truncateEmbedDescription('abcdef', 5)).toBe('ab...');
});
it('returns empty string for null/undefined', () => {
expect(truncateEmbedDescription(null)).toBe('');
expect(truncateEmbedDescription(undefined)).toBe('');
});
});
describe('replaceVariables', () => {
it('substitutes ticket fields', () => {
const ctx = {
ticket: {
sender_name: 'Alice',
senderEmail: 'alice@x.com',
ticketNumber: 42,
subject: 'Help'
}
};
const out = replaceVariables('User {ticket.user} ({ticket.email}) #{ticket.number} - {ticket.subject}', ctx);
expect(out).toBe('User Alice (alice@x.com) #42 - Help');
});
it('falls back when fields missing', () => {
const out = replaceVariables('{ticket.user} {ticket.email} {ticket.subject}', { ticket: {} });
expect(out).toBe('Unknown No subject');
});
it('substitutes staff fields', () => {
const ctx = {
staff: { username: 'bob', displayName: 'Bob the Builder', mention: '<@123>' }
};
expect(replaceVariables('{staff.user} / {staff.name} / {staff.mention}', ctx))
.toBe('bob / Bob the Builder / <@123>');
});
it('returns empty string for empty template', () => {
expect(replaceVariables('')).toBe('');
expect(replaceVariables(null)).toBe('');
});
it('substitutes hours when provided', () => {
expect(replaceVariables('after {hours} hours', { hours: 24 })).toBe('after 24 hours');
});
it('substitutes {date} and {time} from current time', () => {
const out = replaceVariables('on {date}', {});
expect(out).toMatch(/^on \S+/);
expect(out).not.toContain('{date}');
});
});
describe('getPriorityEmoji', () => {
it('maps high/medium/low/normal to CONFIG values', () => {
expect(typeof getPriorityEmoji('high')).toBe('string');
expect(typeof getPriorityEmoji('low')).toBe('string');
expect(typeof getPriorityEmoji('medium')).toBe('string');
expect(typeof getPriorityEmoji('normal')).toBe('string');
});
it('falls back for unknown priority', () => {
expect(typeof getPriorityEmoji('weird')).toBe('string');
});
});
describe('safeEqual', () => {
it('returns true for matching strings', () => {
expect(safeEqual('hello', 'hello')).toBe(true);
});
it('returns false for mismatched strings', () => {
expect(safeEqual('hello', 'world')).toBe(false);
});
it('returns false for length mismatch (no throw)', () => {
expect(safeEqual('a', 'abc')).toBe(false);
});
it('returns false for null/undefined inputs', () => {
expect(safeEqual(null, 'abc')).toBe(false);
expect(safeEqual(undefined, undefined)).toBe(true);
expect(safeEqual('', '')).toBe(true);
});
});
describe('isStaff', () => {
it('returns false for null/undefined member', () => {
expect(isStaff(null)).toBe(false);
expect(isStaff(undefined)).toBe(false);
expect(isStaff({})).toBe(false);
});
it('returns false for member with no roles cache', () => {
expect(isStaff({ roles: null })).toBe(false);
});
});