import { describe, it, expect, vi, beforeEach } from 'vitest' const { mockFind, mockSort, mockLimit, mockSelect, mockLean } = vi.hoisted(() => ({ mockFind: vi.fn(), mockSort: vi.fn(), mockLimit: vi.fn(), mockSelect: vi.fn(), mockLean: vi.fn() })) vi.mock('../../../server/models/event.js', () => ({ default: { find: mockFind } })) vi.mock('../../../server/utils/mongoose.js', () => ({ connectDB: vi.fn() })) vi.mock('../../../server/utils/auth.js', () => ({ requireAuth: vi.fn() })) import { requireAuth } from '../../../server/utils/auth.js' import handler from '../../../server/api/events/recommended.get.js' import { createMockEvent } from '../helpers/createMockEvent.js' // Wire up the chained query builder function setupChain(result = []) { mockLean.mockResolvedValue(result) mockSelect.mockReturnValue({ lean: mockLean }) mockLimit.mockReturnValue({ select: mockSelect }) mockSort.mockReturnValue({ limit: mockLimit }) mockFind.mockReturnValue({ sort: mockSort }) } const futureDate = new Date(Date.now() + 86400000) function makeMember(overrides = {}) { return { _id: 'member-1', craftTags: [], ...overrides } } function makeEvent(overrides = {}) { return { _id: 'event-1', title: 'Test Event', slug: '2026-05-01-test-event', startDate: futureDate, endDate: futureDate, tagline: 'A test event', tags: ['game-design'], eventType: 'workshop', isOnline: true, ...overrides } } describe('GET /api/events/recommended', () => { beforeEach(() => { vi.clearAllMocks() }) it('returns events matching member craft tags', async () => { const member = makeMember({ craftTags: ['game-design', 'narrative'] }) requireAuth.mockResolvedValue(member) const events = [makeEvent({ tags: ['game-design'] })] setupChain(events) const event = createMockEvent({ method: 'GET', path: '/api/events/recommended' }) const result = await handler(event) expect(result).toEqual(events) expect(mockFind).toHaveBeenCalledWith( expect.objectContaining({ tags: { $in: expect.arrayContaining(['game-design', 'narrative']) } }) ) }) it('returns empty array when no tag overlap', async () => { const member = makeMember({ craftTags: ['audio'] }) requireAuth.mockResolvedValue(member) setupChain([]) const event = createMockEvent({ method: 'GET', path: '/api/events/recommended' }) const result = await handler(event) expect(result).toEqual([]) }) it('excludes past events via startDate $gt filter', async () => { const member = makeMember({ craftTags: ['game-design'] }) requireAuth.mockResolvedValue(member) setupChain([]) const event = createMockEvent({ method: 'GET', path: '/api/events/recommended' }) await handler(event) const filter = mockFind.mock.calls[0][0] expect(filter.startDate).toEqual({ $gt: expect.any(Date) }) // The filter date should be approximately now (within 5 seconds) const filterDate = filter.startDate.$gt expect(filterDate.getTime()).toBeGreaterThan(Date.now() - 5000) expect(filterDate.getTime()).toBeLessThanOrEqual(Date.now()) }) it('uses default limit of 10', async () => { const member = makeMember({ craftTags: ['game-design'] }) requireAuth.mockResolvedValue(member) setupChain([]) const event = createMockEvent({ method: 'GET', path: '/api/events/recommended' }) await handler(event) expect(mockLimit).toHaveBeenCalledWith(10) }) it('accepts limit query param', async () => { const member = makeMember({ craftTags: ['game-design'] }) requireAuth.mockResolvedValue(member) setupChain([]) const event = createMockEvent({ method: 'GET', path: '/api/events/recommended?limit=5' }) await handler(event) expect(mockLimit).toHaveBeenCalledWith(5) }) it('caps limit at 25', async () => { const member = makeMember({ craftTags: ['game-design'] }) requireAuth.mockResolvedValue(member) setupChain([]) const event = createMockEvent({ method: 'GET', path: '/api/events/recommended?limit=100' }) await handler(event) expect(mockLimit).toHaveBeenCalledWith(25) }) it('returns empty array when member has no tags', async () => { const member = makeMember() requireAuth.mockResolvedValue(member) const event = createMockEvent({ method: 'GET', path: '/api/events/recommended' }) const result = await handler(event) expect(result).toEqual([]) // Should not query the database at all expect(mockFind).not.toHaveBeenCalled() }) it('requires auth (401)', async () => { requireAuth.mockRejectedValue( createError({ statusCode: 401, statusMessage: 'Unauthorized' }) ) const event = createMockEvent({ method: 'GET', path: '/api/events/recommended' }) await expect(handler(event)).rejects.toMatchObject({ statusCode: 401 }) }) })