/** * Phase 6 — staff response event recording tests. * * Follows the same injectable-parameter pattern as claimEvents.test.js: * _TicketModel — controls Ticket DB layer (findOne, updateOne) * _TagModel — controls Tag DB layer (findOne, updateOne) for /response send * _recordAction — captures recording calls without any module mocking * _isStaff — controls staff check result (messages.js path only) * * No vi.mock needed; all dependencies injected directly. * * Covers: * (a) handleDiscordReply — staff message in a discord ticket → one 'response' event * (b) handleDiscordReply — staff message in an email ticket → one 'response' event * (c) handleDiscordReply — bot message → no event * (d) handleDiscordReply — non-staff message → no event * (e) handleResponseSend — /response send in a ticket → one 'response' event * (f) handleResponseSend — no ticket found → no event * (g) handleResponseSend — tag not found → no event */ import { describe, it, expect, vi } from 'vitest'; import { handleDiscordReply } from '../handlers/messages.js'; import { handleResponseSend } from '../handlers/commands/response.js'; // --------------------------------------------------------------------------- // Shared factories — handleDiscordReply // --------------------------------------------------------------------------- function makeMessage(overrides = {}) { return { author: { bot: false, id: 'staff-001' }, interaction: null, channel: { id: 'chan-001', name: 'ticket-chan-001' }, guild: { id: 'guild-001', members: { cache: { get: vi.fn().mockReturnValue(null) }, fetch: vi.fn().mockRejectedValue(new Error('no members in test env')) } }, content: 'Hello customer', id: 'msg-001', ...overrides }; } function makeTicket(overrides = {}) { return { gmailThreadId: 'discord-test-001', escalationTier: 0, claimerId: null, priority: 'normal', game: 'TestGame', senderEmail: 'user@example.com', creatorId: 'creator-001', ...overrides }; } function makeMessageTicketModel(ticket) { return { findOne: vi.fn().mockReturnValue({ lean: vi.fn().mockResolvedValue(ticket) }), updateOne: vi.fn().mockResolvedValue({ modifiedCount: 0 }) }; } // --------------------------------------------------------------------------- // (a) + (b) Staff message records one 'response' — discord + email tickets // --------------------------------------------------------------------------- describe('handleDiscordReply — staff message records response', () => { it('records one "response" event for a discord ticket', async () => { const ticket = makeTicket({ gmailThreadId: 'discord-test-001' }); const mockModel = makeMessageTicketModel(ticket); const mockRecord = vi.fn(); const stubStaff = vi.fn().mockReturnValue(true); await handleDiscordReply(makeMessage(), mockModel, mockRecord, stubStaff); expect(mockRecord).toHaveBeenCalledTimes(1); const [staffId, type, payload] = mockRecord.mock.calls[0]; expect(staffId).toBe('staff-001'); expect(type).toBe('response'); expect(payload.ticket).toBe(ticket); expect(payload.guildId).toBe('guild-001'); }); it('records one "response" event for an email ticket (before the discord early-return)', async () => { const ticket = makeTicket({ gmailThreadId: '18f3a2b1c0d4e5f6' }); const mockModel = makeMessageTicketModel(ticket); const mockRecord = vi.fn(); const stubStaff = vi.fn().mockReturnValue(true); // Gmail relay will fail but that's caught internally — record already fired. await handleDiscordReply(makeMessage(), mockModel, mockRecord, stubStaff); expect(mockRecord).toHaveBeenCalledTimes(1); const [staffId, type, payload] = mockRecord.mock.calls[0]; expect(staffId).toBe('staff-001'); expect(type).toBe('response'); expect(payload.ticket).toBe(ticket); expect(payload.guildId).toBe('guild-001'); }); }); // --------------------------------------------------------------------------- // (c) Bot message → no event // --------------------------------------------------------------------------- describe('handleDiscordReply — bot message records nothing', () => { it('records nothing when author.bot is true', async () => { const mockModel = makeMessageTicketModel(makeTicket()); const mockRecord = vi.fn(); const stubStaff = vi.fn().mockReturnValue(true); const m = makeMessage({ author: { bot: true, id: 'bot-001' } }); await handleDiscordReply(m, mockModel, mockRecord, stubStaff); expect(mockRecord).not.toHaveBeenCalled(); }); }); // --------------------------------------------------------------------------- // (d) Non-staff message → no event // --------------------------------------------------------------------------- describe('handleDiscordReply — non-staff message records nothing', () => { it('records nothing when isStaff returns false', async () => { const ticket = makeTicket({ gmailThreadId: 'discord-test-001' }); const mockModel = makeMessageTicketModel(ticket); const mockRecord = vi.fn(); const stubStaff = vi.fn().mockReturnValue(false); await handleDiscordReply(makeMessage(), mockModel, mockRecord, stubStaff); expect(mockRecord).not.toHaveBeenCalled(); }); it('records nothing when the message is not in a ticket channel', async () => { const mockModel = makeMessageTicketModel(null); // no ticket const mockRecord = vi.fn(); const stubStaff = vi.fn().mockReturnValue(true); await handleDiscordReply(makeMessage(), mockModel, mockRecord, stubStaff); expect(mockRecord).not.toHaveBeenCalled(); }); }); // --------------------------------------------------------------------------- // Shared factories — handleResponseSend // --------------------------------------------------------------------------- function makeInteraction(overrides = {}) { return { user: { id: 'staff-001', username: 'staffuser', toString: () => '<@staff-001>' }, member: { displayName: 'Staff Member' }, guild: { id: 'guild-001', name: 'Test Guild', memberCount: 10 }, channel: { id: 'chan-001' }, options: { getString: vi.fn().mockReturnValue('my-tag') }, reply: vi.fn().mockResolvedValue(undefined), ...overrides }; } function makeTagModel(tag) { return { findOne: vi.fn().mockReturnValue({ lean: vi.fn().mockResolvedValue(tag) }), updateOne: vi.fn().mockResolvedValue({ modifiedCount: 1 }) }; } function makeResponseTicketModel(ticket) { return { findOne: vi.fn().mockReturnValue({ lean: vi.fn().mockResolvedValue(ticket) }) }; } // --------------------------------------------------------------------------- // (e) /response send in a ticket channel → one event // --------------------------------------------------------------------------- describe('handleResponseSend — records one "response" event', () => { it('records staffId, guildId, and ticket when ticket is found', async () => { const ticket = makeTicket(); const tag = { name: 'my-tag', content: 'Hello {ticket.user}', useCount: 0 }; const mockRecord = vi.fn(); await handleResponseSend( makeInteraction(), makeTagModel(tag), makeResponseTicketModel(ticket), mockRecord ); expect(mockRecord).toHaveBeenCalledTimes(1); const [staffId, type, payload] = mockRecord.mock.calls[0]; expect(staffId).toBe('staff-001'); expect(type).toBe('response'); expect(payload.ticket).toBe(ticket); expect(payload.guildId).toBe('guild-001'); }); }); // --------------------------------------------------------------------------- // (f) No ticket found → no event // --------------------------------------------------------------------------- describe('handleResponseSend — no ticket records nothing', () => { it('records nothing when no ticket exists for the channel', async () => { const tag = { name: 'my-tag', content: 'Hello', useCount: 0 }; const mockRecord = vi.fn(); await handleResponseSend( makeInteraction(), makeTagModel(tag), makeResponseTicketModel(null), mockRecord ); expect(mockRecord).not.toHaveBeenCalled(); }); }); // --------------------------------------------------------------------------- // (g) Tag not found → no event // --------------------------------------------------------------------------- describe('handleResponseSend — tag not found records nothing', () => { it('records nothing when the tag does not exist', async () => { const mockRecord = vi.fn(); await handleResponseSend( makeInteraction(), makeTagModel(null), makeResponseTicketModel(makeTicket()), mockRecord ); expect(mockRecord).not.toHaveBeenCalled(); }); });