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 wrote:\n> previous message'; expect(stripEmailQuotes(input)).toBe('My reply.'); }); it('strips "From: …" reply header block', () => { const input = 'New reply text.\nFrom: Bob \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 wrote:\r\n> quoted'; expect(stripEmailQuotes(input)).toBe('New reply.'); }); it('picks earliest cutoff when multiple markers match', () => { // Earlier in the body: "On X wrote:". Later: "_____" underline. // The earliest cutoff is the reply marker, not the underline. const input = 'My new reply.\nOn Mon Bob wrote:\n> quoted text\n_____\nsignature'; expect(stripEmailQuotes(input)).toBe('My 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 " form', () => { expect(extractRawEmail('Bob ')).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" ')).toBe('bob@example.com'); }); }); describe('escapeHtml', () => { it('escapes <, >, &, ", \'', () => { expect(escapeHtml('')) .toBe('<script>alert("xss")</script>'); expect(escapeHtml("a & b's ")).toBe('a & b's <foo>'); }); 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); }); });