support-tickets.test.ts99 lines · main
1/**
2 * Pure-unit tests for the support-ticket helpers that don't need a DB:
3 * - the routing-tag → topic-code parser
4 * - the ticket-number FORMAT (counter mocked / passed in)
5 *
6 * The DB-dependent paths — generateTicketNumber's atomic counter, the
7 * create-path transaction, and every endpoint — need a live local DB and
8 * are NOT covered here (integration-tested separately).
9 */
10
11import { describe, expect, test } from 'bun:test';
12
13import {
14 formatTicketNumber,
15 parseRoutingTags,
16 primaryTopicCode,
17 renderTicketNumber,
18 ticketDayKey,
19} from './support-tickets.js';
20
21describe('parseRoutingTags', () => {
22 test('extracts routing tags in subject order', () => {
23 expect(parseRoutingTags('#support #billing #technical #self-hosting')).toEqual([
24 'support',
25 'billing',
26 'technical',
27 'self-hosting',
28 ]);
29 });
30
31 test('ignores non-routing chips and de-dupes', () => {
32 expect(parseRoutingTags('#sales #support #support #urgent')).toEqual(['support']);
33 });
34
35 test('returns empty for no/blank/untagged subject', () => {
36 expect(parseRoutingTags(null)).toEqual([]);
37 expect(parseRoutingTags(undefined)).toEqual([]);
38 expect(parseRoutingTags('')).toEqual([]);
39 expect(parseRoutingTags('just a plain subject line')).toEqual([]);
40 });
41
42 test('is case-insensitive on the tag text', () => {
43 expect(parseRoutingTags('#Support #BILLING')).toEqual(['support', 'billing']);
44 });
45});
46
47describe('primaryTopicCode', () => {
48 test('maps each routing tag to its code', () => {
49 expect(primaryTopicCode('#support')).toBe('SUP');
50 expect(primaryTopicCode('#billing')).toBe('BIL');
51 expect(primaryTopicCode('#technical')).toBe('TEC');
52 expect(primaryTopicCode('#self-hosting')).toBe('SLF');
53 });
54
55 test('uses the FIRST routing tag in the subject', () => {
56 expect(primaryTopicCode('#billing #support')).toBe('BIL');
57 expect(primaryTopicCode('#technical #billing #support')).toBe('TEC');
58 });
59
60 test('returns null when no routing tag is present', () => {
61 expect(primaryTopicCode('#sales #partnerships')).toBeNull();
62 expect(primaryTopicCode('hello there')).toBeNull();
63 expect(primaryTopicCode(null)).toBeNull();
64 });
65});
66
67describe('formatTicketNumber + renderTicketNumber', () => {
68 const day = new Date('2026-06-29T12:00:00Z');
69
70 test('formats <CODE><YYMMDD>-<6-digit counter> (no # in storage)', () => {
71 expect(formatTicketNumber('SUP', day, 1)).toBe('SUP260629-000001');
72 });
73
74 test('renders the stored value with a leading # for API responses', () => {
75 expect(renderTicketNumber(formatTicketNumber('SUP', day, 1))).toBe('#SUP260629-000001');
76 });
77
78 test('zero-pads the counter to 6 digits and grows past it', () => {
79 expect(formatTicketNumber('BIL', day, 42)).toBe('BIL260629-000042');
80 expect(formatTicketNumber('TEC', day, 123456)).toBe('TEC260629-123456');
81 });
82
83 test('uses UTC for the YYMMDD stamp', () => {
84 // 23:30 UTC on the 29th stays the 29th regardless of host timezone.
85 const lateUtc = new Date('2026-06-29T23:30:00Z');
86 expect(formatTicketNumber('SLF', lateUtc, 7)).toBe('SLF260629-000007');
87 });
88
89 test('renderTicketNumber passes through null', () => {
90 expect(renderTicketNumber(null)).toBeNull();
91 });
92});
93
94describe('ticketDayKey', () => {
95 test('is the UTC YYYY-MM-DD the counter resets on', () => {
96 expect(ticketDayKey(new Date('2026-06-29T12:00:00Z'))).toBe('2026-06-29');
97 expect(ticketDayKey(new Date('2026-01-05T00:00:00Z'))).toBe('2026-01-05');
98 });
99});