ghostguild-org/tests/server/api/onboarding-status.test.js
Jennie Robinson Faber 7292b11c0b feat(member): account/profile polish + tier upgrade flow
- Timezone: curated USelectMenu dropdown (app/config/timezones.js), preserves unknown saved values
- Profile save now uses useToast() for success/error; remove inline save banner
- Nav onboarding dot nudged down 1px for optical alignment with lowercase text
- Onboarding: skip a suggestion with POST /api/onboarding/track {skip}; member.onboarding.skipped map; does not affect graduation
- CirclePicker takes :saved-value so 'Current' badge stays until save completes
- PrivacyToggle is binary (USwitch labeled Private); member schema enum reduced to ['members','private']; zod coerces legacy 'public'
- New /member/payment-setup page: HelcimPay $0 verify + update-contribution, wired from account.vue via requiresPaymentSetup redirect
- Helcim portal: NUXT_PUBLIC_HELCIM_PORTAL_URL env + account.vue 'Manage billing in Helcim' link
- Migration script: scripts/migrate-privacy-public-to-members.js
2026-04-14 20:35:37 +01:00

197 lines
5.6 KiB
JavaScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
const { mockBoardPostExists } = vi.hoisted(() => ({
mockBoardPostExists: vi.fn()
}))
vi.mock('../../../server/utils/auth.js', () => ({
requireAuth: vi.fn()
}))
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'
describe('GET /api/onboarding/status', () => {
beforeEach(() => {
vi.clearAllMocks()
mockBoardPostExists.mockResolvedValue(null)
})
// 1.1: Default state for new member — all false, completedAt null
it('returns all goals false for a new member with no data', async () => {
requireAuth.mockResolvedValue({
_id: 'member-1',
craftTags: [],
onboarding: {
completedAt: null,
eventPageVisited: false,
boardPageVisited: false,
wikiClicked: false,
},
})
const event = createMockEvent({ method: 'GET', path: '/api/onboarding/status' })
const result = await handler(event)
expect(result).toEqual({
goals: {
hasProfileTags: false,
hasVisitedEvent: false,
hasEngagedBoard: false,
hasClickedWiki: false,
},
skipped: {
profileTags: false,
visitEvent: false,
board: false,
wiki: false,
},
completedAt: null,
})
})
// Skip flags surface from member.onboarding.skipped
it('returns skipped map from member document', async () => {
requireAuth.mockResolvedValue({
_id: 'member-1',
craftTags: [],
onboarding: {
completedAt: null,
eventPageVisited: false,
boardPageVisited: false,
wikiClicked: false,
skipped: { profileTags: true, wiki: true },
},
})
const event = createMockEvent({ method: 'GET', path: '/api/onboarding/status' })
const result = await handler(event)
expect(result.skipped).toEqual({
profileTags: true,
visitEvent: false,
board: false,
wiki: true,
})
})
// 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'],
onboarding: {
completedAt: null,
eventPageVisited: false,
boardPageVisited: false,
wikiClicked: false,
},
})
const event = createMockEvent({ method: 'GET', path: '/api/onboarding/status' })
const result = await handler(event)
expect(result.goals.hasProfileTags).toBe(true)
})
// 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: [],
onboarding: {
completedAt: null,
eventPageVisited: false,
boardPageVisited: false,
wikiClicked: false,
},
})
const event = createMockEvent({ method: 'GET', path: '/api/onboarding/status' })
const result = await handler(event)
expect(result.goals.hasProfileTags).toBe(false)
})
// 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: [],
onboarding: {
completedAt: null,
eventPageVisited: false,
boardPageVisited: true,
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 posts
it('hasEngagedBoard is false when page visited but member has no posts', async () => {
requireAuth.mockResolvedValue({
_id: 'member-1',
craftTags: [],
onboarding: {
completedAt: null,
eventPageVisited: false,
boardPageVisited: true,
wikiClicked: false,
},
})
mockBoardPostExists.mockResolvedValue(null)
const event = createMockEvent({ method: 'GET', path: '/api/onboarding/status' })
const result = await handler(event)
expect(result.goals.hasEngagedBoard).toBe(false)
})
// 1.9: Maps stored booleans directly
it('maps stored onboarding booleans directly for visit and wiki goals', async () => {
requireAuth.mockResolvedValue({
_id: 'member-1',
craftTags: [],
onboarding: {
completedAt: null,
eventPageVisited: true,
boardPageVisited: false,
wikiClicked: true,
},
})
const event = createMockEvent({ method: 'GET', path: '/api/onboarding/status' })
const result = await handler(event)
expect(result.goals.hasVisitedEvent).toBe(true)
expect(result.goals.hasEngagedBoard).toBe(false)
expect(result.goals.hasClickedWiki).toBe(true)
})
// 1.11: Requires auth (401)
it('returns 401 when not authenticated', async () => {
requireAuth.mockRejectedValue(
createError({ statusCode: 401, statusMessage: 'Authentication required' })
)
const event = createMockEvent({ method: 'GET', path: '/api/onboarding/status' })
await expect(handler(event)).rejects.toMatchObject({ statusCode: 401 })
})
})