From 091ec58073db9de0647c33181f68fc6f6a91596b Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Tue, 14 Apr 2026 12:00:15 +0100 Subject: [PATCH] =?UTF-8?q?rename=20communityEcology=20=E2=86=92=20board?= =?UTF-8?q?=20across=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Model, schemas, API routes, activity log, and all server handlers updated. Old ecology/ and community-ecology routes removed, new board/ routes added. Tests updated and new board-suggestions tests written (10 cases). --- app/utils/activityText.js | 4 +- server/api/auth/member.get.js | 2 +- .../api/{ecology => board}/suggestions.get.js | 12 +- server/api/events/recommended.get.js | 2 +- server/api/members/[id].get.js | 22 +- server/api/members/directory.get.js | 22 +- ...munity-ecology.patch.js => board.patch.js} | 26 +- server/api/members/profile.patch.js | 2 +- server/api/onboarding/status.get.js | 6 +- server/api/onboarding/track.post.js | 4 +- server/api/wiki/recommended.get.js | 2 +- server/models/activityLog.js | 2 +- server/models/member.js | 6 +- server/utils/activityLog.js | 4 +- server/utils/schemas.js | 6 +- tests/server/api/board-suggestions.test.js | 325 ++++++++++++++++++ tests/server/api/events-recommended.test.js | 6 +- tests/server/api/onboarding-status.test.js | 24 +- tests/server/api/onboarding-track.test.js | 6 +- tests/server/api/wiki-recommended.test.js | 2 +- 20 files changed, 405 insertions(+), 80 deletions(-) rename server/api/{ecology => board}/suggestions.get.js (84%) rename server/api/members/me/{community-ecology.patch.js => board.patch.js} (59%) create mode 100644 tests/server/api/board-suggestions.test.js diff --git a/app/utils/activityText.js b/app/utils/activityText.js index f98f388..9a06900 100644 --- a/app/utils/activityText.js +++ b/app/utils/activityText.js @@ -76,8 +76,8 @@ const formatters = { text: 'Updated community connections', icon: 'i-lucide-users' }), - community_ecology_updated: () => ({ - text: 'Updated community ecology', + board_updated: () => ({ + text: 'Updated board', icon: 'i-lucide-users' }), connection_requested: (m) => ({ diff --git a/server/api/auth/member.get.js b/server/api/auth/member.get.js index dbb6dfa..74ee1f9 100644 --- a/server/api/auth/member.get.js +++ b/server/api/auth/member.get.js @@ -22,7 +22,7 @@ export default defineEventHandler(async (event) => { location: member.location, socialLinks: member.socialLinks, craftTags: member.craftTags, - communityEcology: member.communityEcology, + board: member.board, showInDirectory: member.showInDirectory, notifications: member.notifications, privacy: member.privacy, diff --git a/server/api/ecology/suggestions.get.js b/server/api/board/suggestions.get.js similarity index 84% rename from server/api/ecology/suggestions.get.js rename to server/api/board/suggestions.get.js index 940ed98..103a521 100644 --- a/server/api/ecology/suggestions.get.js +++ b/server/api/board/suggestions.get.js @@ -5,7 +5,7 @@ export default defineEventHandler(async (event) => { const member = await requireAuth(event) const memberId = member._id - const topics = member.communityEcology?.topics || [] + const topics = member.board?.topics || [] if (!topics.length) { return { suggestions: [] } } @@ -26,9 +26,9 @@ export default defineEventHandler(async (event) => { const candidates = await Member.find({ _id: { $ne: memberId }, status: 'active', - 'communityEcology.topics.tagSlug': { $in: mySlugs }, + 'board.topics.tagSlug': { $in: mySlugs }, }) - .select('name avatar craftTags circle communityEcology privacy') + .select('name avatar craftTags circle board privacy') .lean() if (!candidates.length) { @@ -42,7 +42,7 @@ export default defineEventHandler(async (event) => { const suggestions = [] for (const candidate of candidates) { - const theirTopics = candidate.communityEcology?.topics || [] + const theirTopics = candidate.board?.topics || [] const matchingTags = [] for (const theirTopic of theirTopics) { @@ -79,8 +79,8 @@ export default defineEventHandler(async (event) => { // Expose slackHandle only when the candidate has opted into peer support. // Slack handle is the contact-in-place path — without it, there is no way // for the current member to reach out. - if (candidate.communityEcology?.offerPeerSupport && candidate.communityEcology?.slackHandle) { - filtered.slackHandle = candidate.communityEcology.slackHandle + if (candidate.board?.offerPeerSupport && candidate.board?.slackHandle) { + filtered.slackHandle = candidate.board.slackHandle } suggestions.push({ diff --git a/server/api/events/recommended.get.js b/server/api/events/recommended.get.js index 9766582..7036fd2 100644 --- a/server/api/events/recommended.get.js +++ b/server/api/events/recommended.get.js @@ -7,7 +7,7 @@ export default defineEventHandler(async (event) => { // Combine craft tags and cooperative ecology tags const craftTags = member.craftTags || [] - const ecologyTags = (member.communityEcology?.topics || []).map(t => t.tagSlug) + const ecologyTags = (member.board?.topics || []).map(t => t.tagSlug) const memberTags = [...new Set([...craftTags, ...ecologyTags].filter(Boolean))] if (!memberTags.length) { diff --git a/server/api/members/[id].get.js b/server/api/members/[id].get.js index 43f17e4..c5d910d 100644 --- a/server/api/members/[id].get.js +++ b/server/api/members/[id].get.js @@ -30,7 +30,7 @@ export default defineEventHandler(async (event) => { status: "active", }) .select( - "name pronouns timeZone avatar studio bio location socialLinks privacy circle craftTags communityEcology createdAt memberNumber", + "name pronouns timeZone avatar studio bio location socialLinks privacy circle craftTags board createdAt memberNumber", ) .lean(); @@ -70,18 +70,18 @@ export default defineEventHandler(async (event) => { if (isVisible("socialLinks")) filtered.socialLinks = member.socialLinks; if (isVisible("craftTags")) filtered.craftTags = member.craftTags; - if (isVisible("communityEcology")) { - const ecology = member.communityEcology || {}; - filtered.communityEcology = { - topics: ecology.topics, - offerPeerSupport: ecology.offerPeerSupport, - availability: ecology.availability, - details: ecology.details, + if (isVisible("board")) { + const board = member.board || {}; + filtered.board = { + topics: board.topics, + offerPeerSupport: board.offerPeerSupport, + availability: board.availability, + details: board.details, // Contact-in-place: surface the handle + personal message only when // the member has explicitly opted into peer support. - ...(ecology.offerPeerSupport && { - slackHandle: ecology.slackHandle, - personalMessage: ecology.personalMessage, + ...(board.offerPeerSupport && { + slackHandle: board.slackHandle, + personalMessage: board.personalMessage, }), }; } diff --git a/server/api/members/directory.get.js b/server/api/members/directory.get.js index 7390679..ecf8c3b 100644 --- a/server/api/members/directory.get.js +++ b/server/api/members/directory.get.js @@ -38,7 +38,7 @@ export default defineEventHandler(async (event) => { const andConditions = []; if (peerSupport === "true") { - dbQuery["communityEcology.offerPeerSupport"] = true; + dbQuery["board.offerPeerSupport"] = true; } if (search) { @@ -56,7 +56,7 @@ export default defineEventHandler(async (event) => { } if (connectionTag) { - dbQuery["communityEcology.topics.tagSlug"] = connectionTag; + dbQuery["board.topics.tagSlug"] = connectionTag; } if (andConditions.length > 0) { @@ -66,7 +66,7 @@ export default defineEventHandler(async (event) => { try { const members = await Member.find(dbQuery) .select( - "name pronouns timeZone avatar studio bio location socialLinks privacy circle craftTags communityEcology createdAt", + "name pronouns timeZone avatar studio bio location socialLinks privacy circle craftTags board createdAt", ) .sort({ createdAt: -1 }) .lean(); @@ -96,14 +96,14 @@ export default defineEventHandler(async (event) => { if (isVisible("socialLinks")) filtered.socialLinks = member.socialLinks; if (isVisible("craftTags")) filtered.craftTags = member.craftTags; - if (isVisible("communityEcology")) { - const ecology = member.communityEcology || {}; - filtered.communityEcology = { - topics: ecology.topics, - offerPeerSupport: ecology.offerPeerSupport, - availability: ecology.availability, - ...(ecology.offerPeerSupport && { - slackHandle: ecology.slackHandle, + if (isVisible("board")) { + const board = member.board || {}; + filtered.board = { + topics: board.topics, + offerPeerSupport: board.offerPeerSupport, + availability: board.availability, + ...(board.offerPeerSupport && { + slackHandle: board.slackHandle, }), }; } diff --git a/server/api/members/me/community-ecology.patch.js b/server/api/members/me/board.patch.js similarity index 59% rename from server/api/members/me/community-ecology.patch.js rename to server/api/members/me/board.patch.js index 272af97..3955429 100644 --- a/server/api/members/me/community-ecology.patch.js +++ b/server/api/members/me/board.patch.js @@ -5,15 +5,15 @@ export default defineEventHandler(async (event) => { await connectDB() const member = await requireAuth(event) - const body = await validateBody(event, communityEcologyUpdateSchema) + const body = await validateBody(event, boardUpdateSchema) const updateData = { - 'communityEcology.topics': body.topics || [], - 'communityEcology.offerPeerSupport': body.offerPeerSupport || false, - 'communityEcology.availability': body.availability || '', - 'communityEcology.slackHandle': body.slackHandle || '', - 'communityEcology.personalMessage': body.personalMessage || '', - 'communityEcology.details': body.details || '', + 'board.topics': body.topics || [], + 'board.offerPeerSupport': body.offerPeerSupport || false, + 'board.availability': body.availability || '', + 'board.slackHandle': body.slackHandle || '', + 'board.personalMessage': body.personalMessage || '', + 'board.details': body.details || '', } if (body.offerPeerSupport && body.slackHandle) { @@ -27,12 +27,12 @@ export default defineEventHandler(async (event) => { updateData.slackUserId = slackUserId } else { console.warn( - `[Community Ecology] Could not find Slack user ID for handle: ${body.slackHandle}`, + `[Board] Could not find Slack user ID for handle: ${body.slackHandle}`, ) } } } catch (error) { - console.error('[Community Ecology] Error fetching Slack user ID:', error.message) + console.error('[Board] Error fetching Slack user ID:', error.message) } } @@ -50,21 +50,21 @@ export default defineEventHandler(async (event) => { }) } - logActivity(member._id, 'community_ecology_updated', { + logActivity(member._id, 'board_updated', { topicCount: (body.topics || []).length, offerPeerSupport: body.offerPeerSupport || false, }) return { success: true, - communityEcology: updated.communityEcology, + board: updated.board, } } catch (error) { if (error.statusCode) throw error - console.error('Community ecology update error:', error) + console.error('Board update error:', error) throw createError({ statusCode: 500, - statusMessage: 'Failed to update community ecology settings', + statusMessage: 'Failed to update board settings', }) } }) diff --git a/server/api/members/profile.patch.js b/server/api/members/profile.patch.js index fc3d450..7ce74d1 100644 --- a/server/api/members/profile.patch.js +++ b/server/api/members/profile.patch.js @@ -32,7 +32,7 @@ export default defineEventHandler(async (event) => { "locationPrivacy", "socialLinksPrivacy", "craftTagsPrivacy", - "communityEcologyPrivacy", + "boardPrivacy", ]; // Build update object from validated data diff --git a/server/api/onboarding/status.get.js b/server/api/onboarding/status.get.js index e33fac9..5f3448c 100644 --- a/server/api/onboarding/status.get.js +++ b/server/api/onboarding/status.get.js @@ -5,13 +5,13 @@ export default defineEventHandler(async (event) => { const hasProfileTags = member.craftTags.length > 0 && - (member.communityEcology?.topics || []).length > 0 + (member.board?.topics || []).length > 0 const hasVisitedEvent = !!member.onboarding?.eventPageVisited - const topics = member.communityEcology?.topics || [] + const topics = member.board?.topics || [] const hasEngagedEcology = - !!member.onboarding?.ecologyPageVisited && + !!member.onboarding?.boardPageVisited && topics.some((t) => ['help', 'interested', 'seeking'].includes(t.state)) const hasClickedWiki = !!member.onboarding?.wikiClicked diff --git a/server/api/onboarding/track.post.js b/server/api/onboarding/track.post.js index 8ecb20a..adbb7da 100644 --- a/server/api/onboarding/track.post.js +++ b/server/api/onboarding/track.post.js @@ -32,10 +32,10 @@ export default defineEventHandler(async (event) => { _id: member._id, 'onboarding.completedAt': null, 'onboarding.eventPageVisited': true, - 'onboarding.ecologyPageVisited': true, + 'onboarding.boardPageVisited': true, 'onboarding.wikiClicked': true, 'craftTags.0': { $exists: true }, - 'communityEcology.topics': { + 'board.topics': { $elemMatch: { state: { $in: ['help', 'interested', 'seeking'] } }, }, }, diff --git a/server/api/wiki/recommended.get.js b/server/api/wiki/recommended.get.js index 104960a..fc37ad7 100644 --- a/server/api/wiki/recommended.get.js +++ b/server/api/wiki/recommended.get.js @@ -7,7 +7,7 @@ export default defineEventHandler(async (event) => { // Combine craft tags and cooperative ecology tags const craftTags = member.craftTags || [] - const ecologyTags = (member.communityEcology?.topics || []).map(t => t.tagSlug) + const ecologyTags = (member.board?.topics || []).map(t => t.tagSlug) const memberTags = [...new Set([...craftTags, ...ecologyTags].filter(Boolean))] if (!memberTags.length) { diff --git a/server/models/activityLog.js b/server/models/activityLog.js index 290b8a8..e9a2b80 100644 --- a/server/models/activityLog.js +++ b/server/models/activityLog.js @@ -17,7 +17,7 @@ const ACTIVITY_TYPES = [ 'slack_invited', 'email_sent', 'community_connections_updated', - 'community_ecology_updated', + 'board_updated', 'connection_requested', 'connection_confirmed', 'tag_suggested' diff --git a/server/models/member.js b/server/models/member.js index 34624d7..59367ab 100644 --- a/server/models/member.js +++ b/server/models/member.js @@ -72,7 +72,7 @@ const memberSchema = new mongoose.Schema({ showInDirectory: { type: Boolean, default: true }, craftTags: [String], - communityEcology: { + board: { topics: [ { tagSlug: String, @@ -128,7 +128,7 @@ const memberSchema = new mongoose.Schema({ enum: ["public", "members", "private"], default: "members", }, - communityEcology: { + board: { type: String, enum: ["public", "members", "private"], default: "members", @@ -155,7 +155,7 @@ const memberSchema = new mongoose.Schema({ onboarding: { completedAt: { type: Date, default: null }, eventPageVisited: { type: Boolean, default: false }, - ecologyPageVisited: { type: Boolean, default: false }, + boardPageVisited: { type: Boolean, default: false }, wikiClicked: { type: Boolean, default: false }, }, diff --git a/server/utils/activityLog.js b/server/utils/activityLog.js index f1b0246..05bfc08 100644 --- a/server/utils/activityLog.js +++ b/server/utils/activityLog.js @@ -17,7 +17,7 @@ export const ACTIVITY_TYPES = { SLACK_INVITED: 'slack_invited', EMAIL_SENT: 'email_sent', COMMUNITY_CONNECTIONS_UPDATED: 'community_connections_updated', - COMMUNITY_ECOLOGY_UPDATED: 'community_ecology_updated', + BOARD_UPDATED: 'board_updated', CONNECTION_REQUESTED: 'connection_requested', CONNECTION_CONFIRMED: 'connection_confirmed', TAG_SUGGESTED: 'tag_suggested' @@ -40,7 +40,7 @@ export const ACTIVITY_TYPE_DEFAULTS = { slack_invited: 'admin', email_sent: 'member', community_connections_updated: 'member', - community_ecology_updated: 'member', + board_updated: 'member', connection_requested: 'member', connection_confirmed: 'member', tag_suggested: 'member' diff --git a/server/utils/schemas.js b/server/utils/schemas.js index 7add495..f152988 100644 --- a/server/utils/schemas.js +++ b/server/utils/schemas.js @@ -41,7 +41,7 @@ export const memberProfileUpdateSchema = z.object({ socialLinksPrivacy: privacyEnum.optional(), craftTags: z.array(z.string().max(100)).max(16).optional(), craftTagsPrivacy: privacyEnum.optional(), - communityEcologyPrivacy: privacyEnum.optional() + boardPrivacy: privacyEnum.optional() }) export const eventRegistrationSchema = z.object({ @@ -367,7 +367,7 @@ export const inviteAcceptSchema = z.object({ // --- Onboarding schemas --- export const onboardingTrackSchema = z.object({ - goal: z.enum(['eventPageVisited', 'ecologyPageVisited', 'wikiClicked']) + goal: z.enum(['eventPageVisited', 'boardPageVisited', 'wikiClicked']) }) // --- Tag schemas --- @@ -377,7 +377,7 @@ export const tagSuggestionSchema = z.object({ pool: z.enum(['craft', 'cooperative']) }) -export const communityEcologyUpdateSchema = z.object({ +export const boardUpdateSchema = z.object({ topics: z.array(z.object({ tagSlug: z.string().min(1).max(100), state: z.enum(['help', 'interested', 'seeking']) diff --git a/tests/server/api/board-suggestions.test.js b/tests/server/api/board-suggestions.test.js new file mode 100644 index 0000000..c52deae --- /dev/null +++ b/tests/server/api/board-suggestions.test.js @@ -0,0 +1,325 @@ +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 631e501..0cbea30 100644 --- a/tests/server/api/events-recommended.test.js +++ b/tests/server/api/events-recommended.test.js @@ -39,7 +39,7 @@ function makeMember(overrides = {}) { return { _id: 'member-1', craftTags: [], - communityEcology: { topics: [] }, + board: { topics: [] }, ...overrides } } @@ -82,9 +82,9 @@ describe('GET /api/events/recommended', () => { ) }) - it('returns events matching cooperative tags from communityEcology.topics', async () => { + it('returns events matching cooperative tags from board.topics', async () => { const member = makeMember({ - communityEcology: { + board: { topics: [ { tagSlug: 'revenue-sharing', state: 'interested' }, { tagSlug: 'co-op-governance', state: 'help' } diff --git a/tests/server/api/onboarding-status.test.js b/tests/server/api/onboarding-status.test.js index b671dca..1118329 100644 --- a/tests/server/api/onboarding-status.test.js +++ b/tests/server/api/onboarding-status.test.js @@ -22,11 +22,11 @@ describe('GET /api/onboarding/status', () => { requireAuth.mockResolvedValue({ _id: 'member-1', craftTags: [], - communityEcology: { topics: [] }, + board: { topics: [] }, onboarding: { completedAt: null, eventPageVisited: false, - ecologyPageVisited: false, + boardPageVisited: false, wikiClicked: false, }, }) @@ -50,13 +50,13 @@ describe('GET /api/onboarding/status', () => { requireAuth.mockResolvedValue({ _id: 'member-1', craftTags: ['game-design'], - communityEcology: { + board: { topics: [{ tagSlug: 'governance', state: 'interested' }], }, onboarding: { completedAt: null, eventPageVisited: false, - ecologyPageVisited: false, + boardPageVisited: false, wikiClicked: false, }, }) @@ -72,11 +72,11 @@ describe('GET /api/onboarding/status', () => { requireAuth.mockResolvedValue({ _id: 'member-1', craftTags: ['game-design'], - communityEcology: { topics: [] }, + board: { topics: [] }, onboarding: { completedAt: null, eventPageVisited: false, - ecologyPageVisited: false, + boardPageVisited: false, wikiClicked: false, }, }) @@ -92,13 +92,13 @@ describe('GET /api/onboarding/status', () => { requireAuth.mockResolvedValue({ _id: 'member-1', craftTags: [], - communityEcology: { + board: { topics: [{ tagSlug: 'governance', state: 'help' }], }, onboarding: { completedAt: null, eventPageVisited: false, - ecologyPageVisited: true, + boardPageVisited: true, wikiClicked: false, }, }) @@ -114,11 +114,11 @@ describe('GET /api/onboarding/status', () => { requireAuth.mockResolvedValue({ _id: 'member-1', craftTags: [], - communityEcology: { topics: [] }, + board: { topics: [] }, onboarding: { completedAt: null, eventPageVisited: false, - ecologyPageVisited: true, + boardPageVisited: true, wikiClicked: false, }, }) @@ -134,11 +134,11 @@ describe('GET /api/onboarding/status', () => { requireAuth.mockResolvedValue({ _id: 'member-1', craftTags: [], - communityEcology: { topics: [] }, + board: { topics: [] }, onboarding: { completedAt: null, eventPageVisited: true, - ecologyPageVisited: false, + boardPageVisited: false, wikiClicked: true, }, }) diff --git a/tests/server/api/onboarding-track.test.js b/tests/server/api/onboarding-track.test.js index 754ff23..80c329b 100644 --- a/tests/server/api/onboarding-track.test.js +++ b/tests/server/api/onboarding-track.test.js @@ -39,7 +39,7 @@ describe('POST /api/onboarding/track', () => { onboarding: { completedAt: null, eventPageVisited: false, - ecologyPageVisited: false, + boardPageVisited: false, wikiClicked: false, }, }) @@ -108,7 +108,7 @@ describe('POST /api/onboarding/track', () => { onboarding: { completedAt: null, eventPageVisited: true, - ecologyPageVisited: false, + boardPageVisited: false, wikiClicked: false, }, }) @@ -138,7 +138,7 @@ describe('POST /api/onboarding/track', () => { onboarding: { completedAt: new Date(), eventPageVisited: true, - ecologyPageVisited: true, + boardPageVisited: true, wikiClicked: true, }, } diff --git a/tests/server/api/wiki-recommended.test.js b/tests/server/api/wiki-recommended.test.js index c6ae5f0..401ce06 100644 --- a/tests/server/api/wiki-recommended.test.js +++ b/tests/server/api/wiki-recommended.test.js @@ -37,7 +37,7 @@ function makeMember(overrides = {}) { return { _id: 'member-1', craftTags: [], - communityEcology: { topics: [] }, + board: { topics: [] }, ...overrides } }