import { describe, it, expect, vi, beforeEach } from 'vitest' const { mockFind, mockSelect, mockLean } = vi.hoisted(() => ({ mockFind: vi.fn(), mockSelect: vi.fn(), mockLean: vi.fn() })) vi.mock('../../../server/models/member.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/board/suggestions.get.js' import { createMockEvent } from '../helpers/createMockEvent.js' function setupChain(result = []) { mockLean.mockResolvedValue(result) mockSelect.mockReturnValue({ lean: mockLean }) mockFind.mockReturnValue({ select: mockSelect }) } function makeMember(overrides = {}) { return { _id: 'member-1', board: { topics: [] }, ...overrides } } function makeCandidate(overrides = {}) { return { _id: 'candidate-1', name: 'Test Candidate', circle: 'community', avatar: '/avatar.jpg', craftTags: ['game-design'], board: { topics: [ { tagSlug: 'revenue-sharing', state: 'interested' } ], offerPeerSupport: false, slackHandle: '' }, privacy: {}, ...overrides } } describe('GET /api/board/suggestions', () => { beforeEach(() => { vi.clearAllMocks() }) it('returns empty suggestions when member has no topics', async () => { const member = makeMember({ board: { topics: [] } }) requireAuth.mockResolvedValue(member) const event = createMockEvent({ method: 'GET', path: '/api/board/suggestions' }) const result = await handler(event) expect(result).toEqual({ suggestions: [] }) expect(mockFind).not.toHaveBeenCalled() }) it('returns matching members with shared topics and correct state comparison', async () => { const member = makeMember({ board: { topics: [ { tagSlug: 'revenue-sharing', state: 'help' }, { tagSlug: 'co-op-governance', state: 'seeking' } ] } }) requireAuth.mockResolvedValue(member) const candidate = makeCandidate({ board: { topics: [ { tagSlug: 'revenue-sharing', state: 'interested' } ], offerPeerSupport: false } }) setupChain([candidate]) const event = createMockEvent({ method: 'GET', path: '/api/board/suggestions' }) const result = await handler(event) expect(result.suggestions).toHaveLength(1) expect(result.suggestions[0].matchingTags).toEqual([ { tagSlug: 'revenue-sharing', yourState: 'help', theirState: 'interested' } ]) expect(result.suggestions[0].matchCount).toBe(1) }) it('excludes the requesting member from results', async () => { const member = makeMember({ _id: 'member-1', board: { topics: [{ tagSlug: 'revenue-sharing', state: 'help' }] } }) requireAuth.mockResolvedValue(member) setupChain([]) const event = createMockEvent({ method: 'GET', path: '/api/board/suggestions' }) await handler(event) expect(mockFind).toHaveBeenCalledWith( expect.objectContaining({ _id: { $ne: 'member-1' } }) ) }) it('respects avatar privacy settings', async () => { const member = makeMember({ board: { topics: [{ tagSlug: 'revenue-sharing', state: 'help' }] } }) requireAuth.mockResolvedValue(member) const candidate = makeCandidate({ privacy: { avatar: 'private' }, avatar: '/secret-avatar.jpg', board: { topics: [{ tagSlug: 'revenue-sharing', state: 'interested' }], offerPeerSupport: false } }) setupChain([candidate]) const event = createMockEvent({ method: 'GET', path: '/api/board/suggestions' }) const result = await handler(event) expect(result.suggestions[0].member.avatar).toBeUndefined() }) it('respects craftTags privacy settings', async () => { const member = makeMember({ board: { topics: [{ tagSlug: 'revenue-sharing', state: 'help' }] } }) requireAuth.mockResolvedValue(member) const candidate = makeCandidate({ privacy: { craftTags: 'private' }, craftTags: ['game-design'], board: { topics: [{ tagSlug: 'revenue-sharing', state: 'interested' }], offerPeerSupport: false } }) setupChain([candidate]) const event = createMockEvent({ method: 'GET', path: '/api/board/suggestions' }) const result = await handler(event) expect(result.suggestions[0].member.craftTags).toBeUndefined() }) it('exposes avatar when privacy is public', async () => { const member = makeMember({ board: { topics: [{ tagSlug: 'revenue-sharing', state: 'help' }] } }) requireAuth.mockResolvedValue(member) const candidate = makeCandidate({ privacy: { avatar: 'public' }, avatar: '/public-avatar.jpg', board: { topics: [{ tagSlug: 'revenue-sharing', state: 'interested' }], offerPeerSupport: false } }) setupChain([candidate]) const event = createMockEvent({ method: 'GET', path: '/api/board/suggestions' }) const result = await handler(event) expect(result.suggestions[0].member.avatar).toBe('/public-avatar.jpg') }) it('only exposes slackHandle when offerPeerSupport is true AND slackHandle is set', async () => { const member = makeMember({ board: { topics: [{ tagSlug: 'revenue-sharing', state: 'help' }] } }) requireAuth.mockResolvedValue(member) // Case 1: offerPeerSupport false — no slackHandle const noSupport = makeCandidate({ _id: 'c1', board: { topics: [{ tagSlug: 'revenue-sharing', state: 'interested' }], offerPeerSupport: false, slackHandle: 'someone' } }) // Case 2: offerPeerSupport true but no slackHandle const supportNoHandle = makeCandidate({ _id: 'c2', board: { topics: [{ tagSlug: 'revenue-sharing', state: 'interested' }], offerPeerSupport: true, slackHandle: '' } }) // Case 3: offerPeerSupport true AND slackHandle set const supportWithHandle = makeCandidate({ _id: 'c3', board: { topics: [{ tagSlug: 'revenue-sharing', state: 'interested' }], offerPeerSupport: true, slackHandle: 'helpfulperson' } }) setupChain([noSupport, supportNoHandle, supportWithHandle]) const event = createMockEvent({ method: 'GET', path: '/api/board/suggestions' }) const result = await handler(event) expect(result.suggestions[0].member.slackHandle).toBeUndefined() expect(result.suggestions[1].member.slackHandle).toBeUndefined() expect(result.suggestions[2].member.slackHandle).toBe('helpfulperson') }) it('filters by tag query param', async () => { const member = makeMember({ board: { topics: [ { tagSlug: 'revenue-sharing', state: 'help' }, { tagSlug: 'co-op-governance', state: 'seeking' } ] } }) requireAuth.mockResolvedValue(member) setupChain([]) const event = createMockEvent({ method: 'GET', path: '/api/board/suggestions?tag=revenue-sharing' }) await handler(event) // Should only query for the filtered tag expect(mockFind).toHaveBeenCalledWith( expect.objectContaining({ 'board.topics.tagSlug': { $in: ['revenue-sharing'] } }) ) }) it('sorts by matchCount descending', async () => { const member = makeMember({ board: { topics: [ { tagSlug: 'revenue-sharing', state: 'help' }, { tagSlug: 'co-op-governance', state: 'seeking' }, { tagSlug: 'profit-sharing', state: 'interested' } ] } }) requireAuth.mockResolvedValue(member) const oneMatch = makeCandidate({ _id: 'c1', name: 'One Match', board: { topics: [{ tagSlug: 'revenue-sharing', state: 'interested' }], offerPeerSupport: false } }) const twoMatches = makeCandidate({ _id: 'c2', name: 'Two Matches', board: { topics: [ { tagSlug: 'revenue-sharing', state: 'help' }, { tagSlug: 'co-op-governance', state: 'interested' } ], offerPeerSupport: false } }) setupChain([oneMatch, twoMatches]) const event = createMockEvent({ method: 'GET', path: '/api/board/suggestions' }) const result = await handler(event) expect(result.suggestions[0].matchCount).toBe(2) expect(result.suggestions[0].member.name).toBe('Two Matches') expect(result.suggestions[1].matchCount).toBe(1) expect(result.suggestions[1].member.name).toBe('One Match') }) it('requires auth (401)', async () => { requireAuth.mockRejectedValue( createError({ statusCode: 401, statusMessage: 'Unauthorized' }) ) const event = createMockEvent({ method: 'GET', path: '/api/board/suggestions' }) await expect(handler(event)).rejects.toMatchObject({ statusCode: 401 }) }) })