diff --git a/app/composables/useOnboarding.js b/app/composables/useOnboarding.js index f0e0dcc..e457f28 100644 --- a/app/composables/useOnboarding.js +++ b/app/composables/useOnboarding.js @@ -14,7 +14,6 @@ export function useOnboarding(options = {}) { const loading = useState('onboarding.loading', () => false) const recommendations = useState('onboarding.recommendations', () => ({ events: [], - board: [], wiki: [], })) @@ -72,7 +71,7 @@ export function useOnboarding(options = {}) { } // Graduated — suggestion mode - const cats = ['events', 'board', 'wiki'].filter( + const cats = ['events', 'wiki'].filter( (c) => recommendations.value[c]?.length > 0 ) @@ -99,14 +98,6 @@ export function useOnboarding(options = {}) { actionText: 'View event', } } - if (category === 'board') { - return { - key: 'board', - text: `Connect with ${item.name || 'a member'} on the board`, - action: '/board', - actionText: 'Explore board', - } - } if (category === 'wiki') { return { key: 'wiki', @@ -144,14 +135,12 @@ export function useOnboarding(options = {}) { } async function fetchRecommendations() { - const [events, board, wiki] = await Promise.allSettled([ + const [events, wiki] = await Promise.allSettled([ $fetch('/api/events/recommended'), - $fetch('/api/board/suggestions'), $fetch('/api/wiki/recommended'), ]) recommendations.value = { events: events.status === 'fulfilled' ? (events.value || []) : [], - board: board.status === 'fulfilled' ? (board.value?.suggestions || []) : [], wiki: wiki.status === 'fulfilled' ? (wiki.value || []) : [], } } diff --git a/scripts/seed-members.js b/scripts/seed-members.js index 89c3548..2acdb35 100644 --- a/scripts/seed-members.js +++ b/scripts/seed-members.js @@ -5,33 +5,6 @@ import dotenv from 'dotenv' dotenv.config() -const COOPERATIVE_SLUGS = [ - 'governance', 'finance-and-budgeting', 'legal-structures', 'conflict-resolution', - 'consensus-decision-making', 'revenue-sharing', 'cooperative-bylaws', 'member-onboarding', - 'democratic-management', 'worker-ownership', 'platform-cooperativism', 'cooperative-marketing', - 'shared-resources', 'cooperative-funding', 'community-building', 'equity-and-inclusion', - 'cooperative-tech', 'sustainability', 'collective-bargaining', 'inter-coop-collaboration', -] - -const CRAFT_SLUGS = [ - 'game-design', 'programming', 'narrative-design', 'art-and-animation', - 'audio-and-music', 'production-management', 'qa-and-testing', 'community-management', - 'marketing-and-comms', 'ux-and-ui-design', 'business-development', 'devops-and-tools', - 'localization', 'accessibility', 'analytics-and-data', 'education-and-mentoring', -] - -const AVATARS = ['disbelieving', 'double-take', 'exasperated', 'mild', 'sweet', 'wtf'] -const STATES = ['help', 'interested', 'seeking'] - -function pick(arr, n) { - const shuffled = [...arr].sort(() => Math.random() - 0.5) - return shuffled.slice(0, n) -} - -function randomState() { - return STATES[Math.floor(Math.random() * STATES.length)] -} - const sampleMembers = [ { email: 'alex.rivera@pixelcollective.coop', @@ -42,16 +15,7 @@ const sampleMembers = [ avatar: 'sweet', slackInvited: true, craftTags: ['game-design', 'production-management', 'business-development'], - board: { - topics: [ - { tagSlug: 'governance', state: 'help' }, - { tagSlug: 'revenue-sharing', state: 'help' }, - { tagSlug: 'worker-ownership', state: 'interested' }, - { tagSlug: 'cooperative-bylaws', state: 'help' }, - ], - offerPeerSupport: true, - slackHandle: 'alex.rivera', - }, + board: { slackHandle: 'alex.rivera' }, createdAt: new Date('2024-01-15'), lastLogin: new Date('2026-04-10'), }, @@ -64,17 +28,7 @@ const sampleMembers = [ avatar: 'mild', slackInvited: true, craftTags: ['business-development', 'marketing-and-comms'], - board: { - topics: [ - { tagSlug: 'legal-structures', state: 'help' }, - { tagSlug: 'cooperative-bylaws', state: 'help' }, - { tagSlug: 'governance', state: 'interested' }, - { tagSlug: 'conflict-resolution', state: 'help' }, - { tagSlug: 'equity-and-inclusion', state: 'interested' }, - ], - offerPeerSupport: true, - slackHandle: 'sam.chen', - }, + board: { slackHandle: 'sam.chen' }, createdAt: new Date('2024-02-03'), lastLogin: new Date('2026-04-08'), }, @@ -89,16 +43,7 @@ const sampleMembers = [ helcimSubscriptionId: 'sub_67890', slackInvited: true, craftTags: ['programming', 'devops-and-tools', 'game-design', 'qa-and-testing'], - board: { - topics: [ - { tagSlug: 'cooperative-tech', state: 'help' }, - { tagSlug: 'platform-cooperativism', state: 'interested' }, - { tagSlug: 'shared-resources', state: 'help' }, - { tagSlug: 'democratic-management', state: 'seeking' }, - ], - offerPeerSupport: true, - slackHandle: 'maria.g', - }, + board: { slackHandle: 'maria.g' }, createdAt: new Date('2024-03-10'), lastLogin: new Date('2026-04-12'), }, @@ -111,16 +56,7 @@ const sampleMembers = [ avatar: 'exasperated', slackInvited: true, craftTags: ['business-development', 'analytics-and-data'], - board: { - topics: [ - { tagSlug: 'cooperative-funding', state: 'help' }, - { tagSlug: 'finance-and-budgeting', state: 'help' }, - { tagSlug: 'sustainability', state: 'interested' }, - { tagSlug: 'revenue-sharing', state: 'interested' }, - ], - offerPeerSupport: true, - slackHandle: 'david.park', - }, + board: { slackHandle: 'david.park' }, createdAt: new Date('2024-04-12'), lastLogin: new Date('2026-04-09'), }, @@ -133,15 +69,7 @@ const sampleMembers = [ avatar: 'disbelieving', slackInvited: true, craftTags: ['education-and-mentoring', 'community-management'], - board: { - topics: [ - { tagSlug: 'cooperative-funding', state: 'help' }, - { tagSlug: 'community-building', state: 'help' }, - { tagSlug: 'member-onboarding', state: 'interested' }, - { tagSlug: 'equity-and-inclusion', state: 'help' }, - ], - offerPeerSupport: false, - }, + board: {}, createdAt: new Date('2024-05-08'), lastLogin: new Date('2026-04-05'), }, @@ -154,15 +82,7 @@ const sampleMembers = [ avatar: 'wtf', slackInvited: true, craftTags: ['programming', 'game-design', 'audio-and-music'], - board: { - topics: [ - { tagSlug: 'worker-ownership', state: 'seeking' }, - { tagSlug: 'governance', state: 'seeking' }, - { tagSlug: 'cooperative-tech', state: 'interested' }, - ], - offerPeerSupport: true, - slackHandle: 'jordan.lee', - }, + board: { slackHandle: 'jordan.lee' }, createdAt: new Date('2024-06-20'), lastLogin: new Date('2026-04-07'), }, @@ -175,14 +95,7 @@ const sampleMembers = [ avatar: 'sweet', slackInvited: true, craftTags: ['art-and-animation', 'ux-and-ui-design', 'accessibility'], - board: { - topics: [ - { tagSlug: 'equity-and-inclusion', state: 'interested' }, - { tagSlug: 'community-building', state: 'seeking' }, - { tagSlug: 'consensus-decision-making', state: 'seeking' }, - ], - offerPeerSupport: false, - }, + board: {}, createdAt: new Date('2024-07-15'), lastLogin: new Date('2026-04-01'), }, @@ -196,17 +109,7 @@ const sampleMembers = [ helcimCustomerId: 'cust_54321', slackInvited: true, craftTags: ['programming', 'devops-and-tools', 'production-management'], - board: { - topics: [ - { tagSlug: 'cooperative-tech', state: 'help' }, - { tagSlug: 'shared-resources', state: 'help' }, - { tagSlug: 'platform-cooperativism', state: 'help' }, - { tagSlug: 'democratic-management', state: 'interested' }, - { tagSlug: 'inter-coop-collaboration', state: 'interested' }, - ], - offerPeerSupport: true, - slackHandle: 'casey.w', - }, + board: { slackHandle: 'casey.w' }, createdAt: new Date('2024-08-01'), lastLogin: new Date('2026-04-11'), }, @@ -219,14 +122,7 @@ const sampleMembers = [ avatar: 'double-take', slackInvited: false, craftTags: ['narrative-design', 'localization'], - board: { - topics: [ - { tagSlug: 'community-building', state: 'interested' }, - { tagSlug: 'consensus-decision-making', state: 'seeking' }, - { tagSlug: 'member-onboarding', state: 'seeking' }, - ], - offerPeerSupport: false, - }, + board: {}, createdAt: new Date('2024-08-15'), lastLogin: new Date('2026-03-28'), }, @@ -241,18 +137,7 @@ const sampleMembers = [ helcimSubscriptionId: 'sub_13579', slackInvited: true, craftTags: ['game-design', 'production-management', 'marketing-and-comms', 'business-development'], - board: { - topics: [ - { tagSlug: 'governance', state: 'help' }, - { tagSlug: 'cooperative-bylaws', state: 'help' }, - { tagSlug: 'revenue-sharing', state: 'help' }, - { tagSlug: 'worker-ownership', state: 'help' }, - { tagSlug: 'collective-bargaining', state: 'interested' }, - { tagSlug: 'inter-coop-collaboration', state: 'interested' }, - ], - offerPeerSupport: true, - slackHandle: 'morgan.d', - }, + board: { slackHandle: 'morgan.d' }, createdAt: new Date('2024-09-01'), lastLogin: new Date('2026-04-13'), }, @@ -265,14 +150,7 @@ const sampleMembers = [ avatar: 'disbelieving', slackInvited: false, craftTags: ['programming', 'qa-and-testing'], - board: { - topics: [ - { tagSlug: 'cooperative-tech', state: 'seeking' }, - { tagSlug: 'worker-ownership', state: 'seeking' }, - { tagSlug: 'sustainability', state: 'interested' }, - ], - offerPeerSupport: false, - }, + board: {}, createdAt: new Date('2024-10-10'), lastLogin: new Date('2026-03-20'), }, @@ -285,17 +163,7 @@ const sampleMembers = [ avatar: 'wtf', slackInvited: true, craftTags: ['community-management', 'education-and-mentoring', 'marketing-and-comms'], - board: { - topics: [ - { tagSlug: 'cooperative-marketing', state: 'help' }, - { tagSlug: 'community-building', state: 'help' }, - { tagSlug: 'equity-and-inclusion', state: 'help' }, - { tagSlug: 'member-onboarding', state: 'help' }, - { tagSlug: 'conflict-resolution', state: 'interested' }, - ], - offerPeerSupport: true, - slackHandle: 'phoenix.m', - }, + board: { slackHandle: 'phoenix.m' }, createdAt: new Date('2024-11-05'), lastLogin: new Date('2026-04-06'), }, @@ -308,16 +176,7 @@ const sampleMembers = [ avatar: 'sweet', slackInvited: true, craftTags: ['narrative-design', 'accessibility', 'education-and-mentoring'], - board: { - topics: [ - { tagSlug: 'equity-and-inclusion', state: 'interested' }, - { tagSlug: 'sustainability', state: 'seeking' }, - { tagSlug: 'community-building', state: 'interested' }, - { tagSlug: 'consensus-decision-making', state: 'seeking' }, - ], - offerPeerSupport: true, - slackHandle: 'sage.a', - }, + board: { slackHandle: 'sage.a' }, createdAt: new Date('2024-12-01'), lastLogin: new Date('2026-04-02'), }, @@ -330,17 +189,7 @@ const sampleMembers = [ avatar: 'mild', slackInvited: true, craftTags: ['game-design', 'art-and-animation', 'audio-and-music'], - board: { - topics: [ - { tagSlug: 'governance', state: 'interested' }, - { tagSlug: 'finance-and-budgeting', state: 'seeking' }, - { tagSlug: 'cooperative-bylaws', state: 'seeking' }, - { tagSlug: 'revenue-sharing', state: 'interested' }, - { tagSlug: 'democratic-management', state: 'interested' }, - ], - offerPeerSupport: true, - slackHandle: 'dakota.w', - }, + board: { slackHandle: 'dakota.w' }, createdAt: new Date('2025-01-10'), lastLogin: new Date('2026-04-10'), }, @@ -355,17 +204,7 @@ const sampleMembers = [ helcimSubscriptionId: 'sub_22222', slackInvited: true, craftTags: ['business-development', 'analytics-and-data', 'production-management'], - board: { - topics: [ - { tagSlug: 'finance-and-budgeting', state: 'help' }, - { tagSlug: 'cooperative-funding', state: 'help' }, - { tagSlug: 'collective-bargaining', state: 'help' }, - { tagSlug: 'sustainability', state: 'help' }, - { tagSlug: 'governance', state: 'interested' }, - ], - offerPeerSupport: true, - slackHandle: 'charlie.t', - }, + board: { slackHandle: 'charlie.t' }, createdAt: new Date('2025-02-14'), lastLogin: new Date('2026-04-12'), }, @@ -379,16 +218,7 @@ const sampleMembers = [ avatar: 'exasperated', slackInvited: true, craftTags: ['programming', 'game-design', 'devops-and-tools'], - board: { - topics: [ - { tagSlug: 'worker-ownership', state: 'help' }, - { tagSlug: 'cooperative-tech', state: 'help' }, - { tagSlug: 'platform-cooperativism', state: 'help' }, - { tagSlug: 'shared-resources', state: 'interested' }, - ], - offerPeerSupport: true, - slackHandle: 'robin.n', - }, + board: { slackHandle: 'robin.n' }, createdAt: new Date('2025-03-01'), lastLogin: new Date('2026-04-13'), }, @@ -401,16 +231,7 @@ const sampleMembers = [ avatar: 'wtf', slackInvited: true, craftTags: ['art-and-animation', 'community-management'], - board: { - topics: [ - { tagSlug: 'equity-and-inclusion', state: 'help' }, - { tagSlug: 'conflict-resolution', state: 'interested' }, - { tagSlug: 'community-building', state: 'help' }, - { tagSlug: 'consensus-decision-making', state: 'interested' }, - ], - offerPeerSupport: true, - slackHandle: 'emery.o', - }, + board: { slackHandle: 'emery.o' }, createdAt: new Date('2025-03-15'), lastLogin: new Date('2026-04-11'), }, @@ -423,17 +244,7 @@ const sampleMembers = [ avatar: 'disbelieving', slackInvited: true, craftTags: ['production-management', 'business-development', 'education-and-mentoring'], - board: { - topics: [ - { tagSlug: 'governance', state: 'help' }, - { tagSlug: 'democratic-management', state: 'help' }, - { tagSlug: 'cooperative-bylaws', state: 'interested' }, - { tagSlug: 'member-onboarding', state: 'help' }, - { tagSlug: 'inter-coop-collaboration', state: 'help' }, - ], - offerPeerSupport: true, - slackHandle: 'quinn.f', - }, + board: { slackHandle: 'quinn.f' }, createdAt: new Date('2025-04-01'), lastLogin: new Date('2026-04-14'), }, @@ -446,16 +257,7 @@ const sampleMembers = [ avatar: 'sweet', slackInvited: true, craftTags: ['ux-and-ui-design', 'accessibility', 'narrative-design'], - board: { - topics: [ - { tagSlug: 'platform-cooperativism', state: 'interested' }, - { tagSlug: 'cooperative-marketing', state: 'seeking' }, - { tagSlug: 'shared-resources', state: 'interested' }, - { tagSlug: 'sustainability', state: 'seeking' }, - { tagSlug: 'equity-and-inclusion', state: 'interested' }, - ], - offerPeerSupport: false, - }, + board: {}, createdAt: new Date('2025-05-10'), lastLogin: new Date('2026-04-09'), }, @@ -468,35 +270,13 @@ const sampleMembers = [ avatar: 'mild', slackInvited: true, craftTags: ['audio-and-music', 'localization'], - board: { - topics: [ - { tagSlug: 'collective-bargaining', state: 'seeking' }, - { tagSlug: 'revenue-sharing', state: 'seeking' }, - { tagSlug: 'worker-ownership', state: 'interested' }, - ], - offerPeerSupport: true, - slackHandle: 'indigo.r', - }, + board: { slackHandle: 'indigo.r' }, createdAt: new Date('2025-06-01'), lastLogin: new Date('2026-04-04'), }, ] -// Board topics for the test admin so the logged-in user sees matches const TEST_ADMIN_BOARD = { - topics: [ - { tagSlug: 'governance', state: 'interested' }, - { tagSlug: 'worker-ownership', state: 'seeking' }, - { tagSlug: 'cooperative-tech', state: 'interested' }, - { tagSlug: 'community-building', state: 'seeking' }, - { tagSlug: 'equity-and-inclusion', state: 'interested' }, - { tagSlug: 'revenue-sharing', state: 'seeking' }, - { tagSlug: 'cooperative-funding', state: 'interested' }, - { tagSlug: 'sustainability', state: 'interested' }, - { tagSlug: 'consensus-decision-making', state: 'seeking' }, - { tagSlug: 'platform-cooperativism', state: 'interested' }, - ], - offerPeerSupport: true, slackHandle: 'test-admin', } @@ -508,7 +288,7 @@ async function seedMembers() { await Member.deleteMany({ email: { $ne: 'test-admin@ghostguild.dev' } }) console.log('Cleared existing members (kept test admin)') - // Update test admin with board topics so the Board page shows matches + // Update test admin with slack handle + craft tags const adminUpdate = await Member.findOneAndUpdate( { email: 'test-admin@ghostguild.dev' }, { @@ -519,7 +299,7 @@ async function seedMembers() { }, ) if (adminUpdate) { - console.log('Updated test admin with board topics') + console.log('Updated test admin with board + craft tags') } else { console.log('Test admin not found — run /api/dev/test-login first to create it') } @@ -539,11 +319,8 @@ async function seedMembers() { console.log('\nBreakdown by circle:') circleBreakdown.forEach((c) => console.log(` ${c._id}: ${c.count}`)) - const withTopics = await Member.countDocuments({ 'board.topics.0': { $exists: true } }) - console.log(`\nMembers with board topics: ${withTopics}`) - const withSlack = await Member.countDocuments({ 'board.slackHandle': { $exists: true, $ne: null } }) - console.log(`Members with slack handles: ${withSlack}`) + console.log(`\nMembers with slack handles: ${withSlack}`) process.exit(0) } catch (error) { diff --git a/tests/client/composables/useOnboarding.test.js b/tests/client/composables/useOnboarding.test.js index e48a481..def57f5 100644 --- a/tests/client/composables/useOnboarding.test.js +++ b/tests/client/composables/useOnboarding.test.js @@ -100,7 +100,6 @@ describe('useOnboarding', () => { }) } if (url === '/api/events/recommended') return Promise.resolve([]) - if (url === '/api/board/suggestions') return Promise.resolve({ suggestions: [] }) if (url === '/api/wiki/recommended') return Promise.resolve([]) return Promise.resolve(null) }) @@ -252,7 +251,6 @@ describe('useOnboarding', () => { }) } if (url === '/api/events/recommended') return Promise.resolve([]) - if (url === '/api/board/suggestions') return Promise.resolve({ suggestions: [] }) if (url === '/api/wiki/recommended') return Promise.resolve([]) return Promise.resolve(null) }) @@ -289,9 +287,6 @@ describe('useOnboarding', () => { if (url === '/api/events/recommended') { return Promise.resolve([{ _id: 'e1', title: 'Game Jam' }]) } - if (url === '/api/board/suggestions') { - return Promise.resolve({ suggestions: [{ name: 'Alex' }] }) - } if (url === '/api/wiki/recommended') { return Promise.resolve([{ title: 'Co-op Guide', url: 'https://wiki.example.com/coop' }]) } @@ -329,9 +324,6 @@ describe('useOnboarding', () => { if (url === '/api/events/recommended') { return Promise.resolve([{ _id: 'e1', title: 'Game Jam' }]) } - if (url === '/api/board/suggestions') { - return Promise.resolve({ suggestions: [] }) - } if (url === '/api/wiki/recommended') { return Promise.resolve([]) } @@ -373,7 +365,6 @@ describe('useOnboarding', () => { }) } if (url === '/api/events/recommended') return Promise.resolve([]) - if (url === '/api/board/suggestions') return Promise.resolve({ suggestions: [] }) if (url === '/api/wiki/recommended') return Promise.resolve([]) return Promise.resolve(null) }) diff --git a/tests/server/api/board-suggestions.test.js b/tests/server/api/board-suggestions.test.js deleted file mode 100644 index c52deae..0000000 --- a/tests/server/api/board-suggestions.test.js +++ /dev/null @@ -1,325 +0,0 @@ -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 - }) - }) -}) diff --git a/tests/server/api/events-recommended.test.js b/tests/server/api/events-recommended.test.js index 0cbea30..b3ca5c4 100644 --- a/tests/server/api/events-recommended.test.js +++ b/tests/server/api/events-recommended.test.js @@ -39,7 +39,6 @@ function makeMember(overrides = {}) { return { _id: 'member-1', craftTags: [], - board: { topics: [] }, ...overrides } } @@ -82,31 +81,6 @@ describe('GET /api/events/recommended', () => { ) }) - it('returns events matching cooperative tags from board.topics', async () => { - const member = makeMember({ - board: { - topics: [ - { tagSlug: 'revenue-sharing', state: 'interested' }, - { tagSlug: 'co-op-governance', state: 'help' } - ] - } - }) - requireAuth.mockResolvedValue(member) - - const events = [makeEvent({ tags: ['revenue-sharing'] })] - 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(['revenue-sharing', 'co-op-governance']) } - }) - ) - }) - it('returns empty array when no tag overlap', async () => { const member = makeMember({ craftTags: ['audio'] }) requireAuth.mockResolvedValue(member) diff --git a/tests/server/api/onboarding-status.test.js b/tests/server/api/onboarding-status.test.js index b989b83..5030e5f 100644 --- a/tests/server/api/onboarding-status.test.js +++ b/tests/server/api/onboarding-status.test.js @@ -1,5 +1,9 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' +const { mockBoardPostExists } = vi.hoisted(() => ({ + mockBoardPostExists: vi.fn() +})) + vi.mock('../../../server/utils/auth.js', () => ({ requireAuth: vi.fn() })) @@ -8,6 +12,10 @@ vi.mock('../../../server/utils/mongoose.js', () => ({ connectDB: vi.fn() })) +vi.mock('../../../server/models/boardPost.js', () => ({ + default: { exists: mockBoardPostExists } +})) + import { requireAuth } from '../../../server/utils/auth.js' import handler from '../../../server/api/onboarding/status.get.js' import { createMockEvent } from '../helpers/createMockEvent.js' @@ -15,6 +23,7 @@ import { createMockEvent } from '../helpers/createMockEvent.js' describe('GET /api/onboarding/status', () => { beforeEach(() => { vi.clearAllMocks() + mockBoardPostExists.mockResolvedValue(null) }) // 1.1: Default state for new member — all false, completedAt null @@ -22,7 +31,6 @@ describe('GET /api/onboarding/status', () => { requireAuth.mockResolvedValue({ _id: 'member-1', craftTags: [], - board: { topics: [] }, onboarding: { completedAt: null, eventPageVisited: false, @@ -45,14 +53,11 @@ describe('GET /api/onboarding/status', () => { }) }) - // 1.2: hasProfileTags true when both tag types present - it('hasProfileTags is true when member has both craft tags and board topics', async () => { + // 1.2: hasProfileTags true when craft tags present + it('hasProfileTags is true when member has craft tags', async () => { requireAuth.mockResolvedValue({ _id: 'member-1', craftTags: ['game-design'], - board: { - topics: [{ tagSlug: 'governance', state: 'interested' }], - }, onboarding: { completedAt: null, eventPageVisited: false, @@ -67,12 +72,11 @@ describe('GET /api/onboarding/status', () => { expect(result.goals.hasProfileTags).toBe(true) }) - // 1.3: hasProfileTags false when only craft tags - it('hasProfileTags is false when member has craft tags but no board topics', async () => { + // 1.3: hasProfileTags false when no craft tags + it('hasProfileTags is false when member has no craft tags', async () => { requireAuth.mockResolvedValue({ _id: 'member-1', - craftTags: ['game-design'], - board: { topics: [] }, + craftTags: [], onboarding: { completedAt: null, eventPageVisited: false, @@ -87,14 +91,11 @@ describe('GET /api/onboarding/status', () => { expect(result.goals.hasProfileTags).toBe(false) }) - // 1.5: hasEngagedBoard true when visited AND has tag with engagement state - it('hasEngagedBoard is true when page visited and has engaged topic', async () => { + // 1.5: hasEngagedBoard true when visited AND has a BoardPost + it('hasEngagedBoard is true when page visited and member has posted', async () => { requireAuth.mockResolvedValue({ _id: 'member-1', craftTags: [], - board: { - topics: [{ tagSlug: 'governance', state: 'help' }], - }, onboarding: { completedAt: null, eventPageVisited: false, @@ -102,19 +103,20 @@ describe('GET /api/onboarding/status', () => { wikiClicked: false, }, }) + mockBoardPostExists.mockResolvedValue({ _id: 'post-1' }) const event = createMockEvent({ method: 'GET', path: '/api/onboarding/status' }) const result = await handler(event) expect(result.goals.hasEngagedBoard).toBe(true) + expect(mockBoardPostExists).toHaveBeenCalledWith({ author: 'member-1' }) }) - // 1.6: hasEngagedBoard false when visited but no engagement state - it('hasEngagedBoard is false when page visited but no topics have engagement state', async () => { + // 1.6: hasEngagedBoard false when visited but no posts + it('hasEngagedBoard is false when page visited but member has no posts', async () => { requireAuth.mockResolvedValue({ _id: 'member-1', craftTags: [], - board: { topics: [] }, onboarding: { completedAt: null, eventPageVisited: false, @@ -122,6 +124,7 @@ describe('GET /api/onboarding/status', () => { wikiClicked: false, }, }) + mockBoardPostExists.mockResolvedValue(null) const event = createMockEvent({ method: 'GET', path: '/api/onboarding/status' }) const result = await handler(event) @@ -134,7 +137,6 @@ describe('GET /api/onboarding/status', () => { requireAuth.mockResolvedValue({ _id: 'member-1', craftTags: [], - board: { topics: [] }, onboarding: { completedAt: null, eventPageVisited: true, diff --git a/tests/server/api/onboarding-track.test.js b/tests/server/api/onboarding-track.test.js index 80c329b..792d2bb 100644 --- a/tests/server/api/onboarding-track.test.js +++ b/tests/server/api/onboarding-track.test.js @@ -1,5 +1,13 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' +const { mockBoardPostExists } = vi.hoisted(() => ({ + mockBoardPostExists: vi.fn() +})) + +vi.mock('../../../server/models/boardPost.js', () => ({ + default: { exists: mockBoardPostExists } +})) + vi.mock('../../../server/utils/auth.js', () => ({ requireAuth: vi.fn() })) @@ -45,6 +53,7 @@ describe('POST /api/onboarding/track', () => { }) Member.findByIdAndUpdate.mockResolvedValue({}) Member.findOneAndUpdate.mockResolvedValue(null) // no graduation by default + mockBoardPostExists.mockResolvedValue({ _id: 'post-1' }) }) // 2.1: Sets eventPageVisited to true