/** * Phase 5b — transfer event recording tests. * * Follows the same injectable-parameter pattern as claimEvents.test.js: * _TicketModel — controls the DB layer (updateOne) * _recordAction — captures recording calls without any module mocking * * No vi.mock needed; all dependencies injected directly. * * Tests applyTransfer directly (same pattern as testing applyClaim directly). * * Covers: * (a) transfer to different member — one 'transfer' event with fromId/toId * (b) transfer to current claimer — fromId === toId → no event * (c) fromId/toId/staffId/guildId correctness */ import { describe, it, expect, vi } from 'vitest'; import { applyTransfer } from '../handlers/commands/index.js'; // --------------------------------------------------------------------------- // Shared factories // --------------------------------------------------------------------------- function makeTicket(overrides = {}) { return { gmailThreadId: 'discord-test-001', escalationTier: 0, escalated: false, claimerId: 'prev-claimer-001', claimedBy: 'PrevClaimerName', priority: 'normal', game: 'TestGame', senderEmail: 'user@example.com', creatorId: 'creator-001', ticketNumber: 42, ...overrides }; } function makeGuildMember(userId = 'new-claimer-002') { return { id: userId, displayName: 'New Claimer', user: { id: userId, username: 'newclaimer', tag: 'newclaimer#0002', toString: () => `<@${userId}>` } }; } function makeInteraction(userId = 'staff-001') { return { user: { id: userId, username: 'staffuser', tag: 'staffuser#0001', toString: () => `<@${userId}>` }, member: { displayName: 'Staff Member' }, guild: { id: 'guild-001', members: { fetch: vi.fn().mockRejectedValue(new Error('no member in test env')) } }, channel: { id: 'chan-001', name: 'ticket-chan-001', isThread: vi.fn().mockReturnValue(true), send: vi.fn().mockResolvedValue({ id: 'sent-msg-001' }) }, editReply: vi.fn().mockResolvedValue(undefined), client: { channels: { fetch: vi.fn().mockResolvedValue(null) } } }; } // --------------------------------------------------------------------------- // (a) Transfer to a different member — one event // --------------------------------------------------------------------------- describe('applyTransfer — different member emits one event', () => { it('emits exactly one "transfer" event', async () => { const ticket = makeTicket({ claimerId: 'prev-claimer-001' }); const guildMember = makeGuildMember('new-claimer-002'); const mockUpdateOne = vi.fn().mockResolvedValue({ modifiedCount: 1 }); const mockRecord = vi.fn(); const interaction = makeInteraction('staff-001'); await applyTransfer(interaction, ticket, guildMember, 'reason', { updateOne: mockUpdateOne }, mockRecord); expect(mockRecord).toHaveBeenCalledTimes(1); const [staffId, type] = mockRecord.mock.calls[0]; expect(staffId).toBe('staff-001'); expect(type).toBe('transfer'); }); it('passes correct fromId, toId, guildId, and ticket', async () => { const ticket = makeTicket({ claimerId: 'prev-claimer-001' }); const guildMember = makeGuildMember('new-claimer-002'); const mockUpdateOne = vi.fn().mockResolvedValue({ modifiedCount: 1 }); const mockRecord = vi.fn(); const interaction = makeInteraction('staff-001'); await applyTransfer(interaction, ticket, guildMember, 'reason', { updateOne: mockUpdateOne }, mockRecord); const [, , payload] = mockRecord.mock.calls[0]; expect(payload.fromId).toBe('prev-claimer-001'); expect(payload.toId).toBe('new-claimer-002'); expect(payload.guildId).toBe('guild-001'); expect(payload.ticket).toBe(ticket); }); it('fromId is the pre-write claimerId (captured before the DB write)', async () => { const ticket = makeTicket({ claimerId: 'original-claimer' }); const guildMember = makeGuildMember('new-staff-003'); const mockUpdateOne = vi.fn().mockResolvedValue({ modifiedCount: 1 }); const mockRecord = vi.fn(); const interaction = makeInteraction('staff-001'); await applyTransfer(interaction, ticket, guildMember, 'reason', { updateOne: mockUpdateOne }, mockRecord); const [, , payload] = mockRecord.mock.calls[0]; expect(payload.fromId).toBe('original-claimer'); expect(payload.toId).toBe('new-staff-003'); }); }); // --------------------------------------------------------------------------- // (b) Transfer to the current claimer — no event // --------------------------------------------------------------------------- describe('applyTransfer — transfer to current claimer emits no event', () => { it('emits no event when fromId === toId (transferring to existing claimer)', async () => { const ticket = makeTicket({ claimerId: 'same-claimer-001' }); const guildMember = makeGuildMember('same-claimer-001'); const mockUpdateOne = vi.fn().mockResolvedValue({ modifiedCount: 1 }); const mockRecord = vi.fn(); const interaction = makeInteraction('staff-001'); await applyTransfer(interaction, ticket, guildMember, 'reason', { updateOne: mockUpdateOne }, mockRecord); expect(mockRecord).not.toHaveBeenCalled(); }); it('emits one event when the ticket is unclaimed (fromId null) and toId is non-null — null !== toId satisfies the fromId !== toId gate', async () => { // null !== 'new-claimer' so this IS a real transfer — event IS emitted const ticket = makeTicket({ claimerId: null }); const guildMember = makeGuildMember('new-claimer-002'); const mockUpdateOne = vi.fn().mockResolvedValue({ modifiedCount: 1 }); const mockRecord = vi.fn(); const interaction = makeInteraction('staff-001'); await applyTransfer(interaction, ticket, guildMember, 'reason', { updateOne: mockUpdateOne }, mockRecord); // null !== 'new-claimer-002' → event IS emitted expect(mockRecord).toHaveBeenCalledTimes(1); const [, , payload] = mockRecord.mock.calls[0]; expect(payload.fromId).toBeNull(); expect(payload.toId).toBe('new-claimer-002'); }); });