Tests, UX improvements.
This commit is contained in:
parent
4e6f5d36b8
commit
0ae18f495e
63 changed files with 1384 additions and 2330 deletions
|
|
@ -3,7 +3,7 @@ import { readFileSync } from 'node:fs'
|
|||
import { resolve } from 'node:path'
|
||||
|
||||
vi.mock('../../../server/models/member.js', () => ({
|
||||
default: { findByIdAndUpdate: vi.fn() }
|
||||
default: { findById: vi.fn(), findByIdAndUpdate: vi.fn() }
|
||||
}))
|
||||
|
||||
vi.mock('../../../server/utils/mongoose.js', () => ({
|
||||
|
|
@ -104,7 +104,7 @@ describe('admin role PATCH endpoint', () => {
|
|||
|
||||
describe('member not found', () => {
|
||||
it('returns 404 when member does not exist', async () => {
|
||||
Member.findByIdAndUpdate.mockResolvedValue(null)
|
||||
Member.findById.mockResolvedValue(null)
|
||||
|
||||
const event = createMockEvent({
|
||||
method: 'PATCH',
|
||||
|
|
@ -122,7 +122,9 @@ describe('admin role PATCH endpoint', () => {
|
|||
describe('successful role changes', () => {
|
||||
it('promotes a member to admin', async () => {
|
||||
validateBody.mockResolvedValue({ role: 'admin' })
|
||||
const existingMember = { _id: 'target-member-id', role: 'member', name: 'Test User' }
|
||||
const updatedMember = { _id: 'target-member-id', role: 'admin', name: 'Test User' }
|
||||
Member.findById.mockResolvedValue(existingMember)
|
||||
Member.findByIdAndUpdate.mockResolvedValue(updatedMember)
|
||||
|
||||
const event = createMockEvent({
|
||||
|
|
@ -143,7 +145,9 @@ describe('admin role PATCH endpoint', () => {
|
|||
|
||||
it('demotes a member to regular role', async () => {
|
||||
validateBody.mockResolvedValue({ role: 'member' })
|
||||
const existingMember = { _id: 'target-member-id', role: 'admin', name: 'Test User' }
|
||||
const updatedMember = { _id: 'target-member-id', role: 'member', name: 'Test User' }
|
||||
Member.findById.mockResolvedValue(existingMember)
|
||||
Member.findByIdAndUpdate.mockResolvedValue(updatedMember)
|
||||
|
||||
const event = createMockEvent({
|
||||
|
|
|
|||
|
|
@ -1,112 +0,0 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const updatesDir = resolve(import.meta.dirname, '../../../server/api/updates')
|
||||
|
||||
describe('Updates API auth guards', () => {
|
||||
describe('index.post.js (create)', () => {
|
||||
const source = readFileSync(resolve(updatesDir, 'index.post.js'), 'utf-8')
|
||||
|
||||
it('requires auth via requireAuth(event)', () => {
|
||||
expect(source).toContain('requireAuth(event)')
|
||||
})
|
||||
|
||||
it('does not wrap requireAuth in a try/catch (auth is mandatory)', () => {
|
||||
// The public GET routes wrap requireAuth in try/catch to make it optional.
|
||||
// The create route must NOT do that — auth failure should halt the request.
|
||||
const lines = source.split('\n')
|
||||
const authLine = lines.findIndex(l => l.includes('requireAuth(event)'))
|
||||
expect(authLine).toBeGreaterThan(-1)
|
||||
// Check the line before requireAuth is not a try {
|
||||
const preceding = lines.slice(Math.max(0, authLine - 2), authLine).join(' ')
|
||||
expect(preceding).not.toMatch(/try\s*\{/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('[id].patch.js (edit)', () => {
|
||||
const source = readFileSync(resolve(updatesDir, '[id].patch.js'), 'utf-8')
|
||||
|
||||
it('requires auth via requireAuth(event)', () => {
|
||||
expect(source).toContain('requireAuth(event)')
|
||||
})
|
||||
|
||||
it('does not wrap requireAuth in a try/catch (auth is mandatory)', () => {
|
||||
const lines = source.split('\n')
|
||||
const authLine = lines.findIndex(l => l.includes('requireAuth(event)'))
|
||||
expect(authLine).toBeGreaterThan(-1)
|
||||
const preceding = lines.slice(Math.max(0, authLine - 2), authLine).join(' ')
|
||||
expect(preceding).not.toMatch(/try\s*\{/)
|
||||
})
|
||||
|
||||
it('verifies ownership by comparing update.author with authenticated member ID', () => {
|
||||
expect(source).toContain('update.author.toString() !== memberId')
|
||||
})
|
||||
|
||||
it('throws 403 when user is not the author', () => {
|
||||
expect(source).toContain('statusCode: 403')
|
||||
})
|
||||
})
|
||||
|
||||
describe('[id].delete.js (delete)', () => {
|
||||
const source = readFileSync(resolve(updatesDir, '[id].delete.js'), 'utf-8')
|
||||
|
||||
it('requires auth via requireAuth(event)', () => {
|
||||
expect(source).toContain('requireAuth(event)')
|
||||
})
|
||||
|
||||
it('does not wrap requireAuth in a try/catch (auth is mandatory)', () => {
|
||||
const lines = source.split('\n')
|
||||
const authLine = lines.findIndex(l => l.includes('requireAuth(event)'))
|
||||
expect(authLine).toBeGreaterThan(-1)
|
||||
const preceding = lines.slice(Math.max(0, authLine - 2), authLine).join(' ')
|
||||
expect(preceding).not.toMatch(/try\s*\{/)
|
||||
})
|
||||
|
||||
it('verifies ownership by comparing update.author with authenticated member ID', () => {
|
||||
expect(source).toContain('update.author.toString() !== memberId')
|
||||
})
|
||||
|
||||
it('throws 403 when user is not the author', () => {
|
||||
expect(source).toContain('statusCode: 403')
|
||||
})
|
||||
})
|
||||
|
||||
describe('index.get.js (list — public)', () => {
|
||||
const source = readFileSync(resolve(updatesDir, 'index.get.js'), 'utf-8')
|
||||
|
||||
it('does NOT enforce requireAuth (public access allowed)', () => {
|
||||
// The route uses requireAuth inside a try/catch so unauthenticated
|
||||
// users can still access it — auth failure is caught and ignored.
|
||||
const lines = source.split('\n')
|
||||
const authLine = lines.findIndex(l => l.includes('requireAuth(event)'))
|
||||
// If requireAuth is present, it must be wrapped in try/catch
|
||||
if (authLine > -1) {
|
||||
const preceding = lines.slice(Math.max(0, authLine - 2), authLine).join(' ')
|
||||
expect(preceding).toMatch(/try\s*\{/)
|
||||
}
|
||||
// Either way, the route must not throw on unauthenticated access
|
||||
})
|
||||
|
||||
it('does not call requireAdmin', () => {
|
||||
expect(source).not.toContain('requireAdmin')
|
||||
})
|
||||
})
|
||||
|
||||
describe('[id].get.js (get — public)', () => {
|
||||
const source = readFileSync(resolve(updatesDir, '[id].get.js'), 'utf-8')
|
||||
|
||||
it('does NOT enforce requireAuth (public access allowed)', () => {
|
||||
const lines = source.split('\n')
|
||||
const authLine = lines.findIndex(l => l.includes('requireAuth(event)'))
|
||||
if (authLine > -1) {
|
||||
const preceding = lines.slice(Math.max(0, authLine - 2), authLine).join(' ')
|
||||
expect(preceding).toMatch(/try\s*\{/)
|
||||
}
|
||||
})
|
||||
|
||||
it('does not call requireAdmin', () => {
|
||||
expect(source).not.toContain('requireAdmin')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -16,7 +16,6 @@ import {
|
|||
eventPaymentSchema,
|
||||
updateContributionSchema,
|
||||
peerSupportUpdateSchema,
|
||||
updatePatchSchema,
|
||||
seriesTicketPurchaseSchema,
|
||||
seriesTicketEligibilitySchema,
|
||||
adminSeriesCreateSchema,
|
||||
|
|
@ -329,28 +328,6 @@ describe('peerSupportUpdateSchema', () => {
|
|||
})
|
||||
})
|
||||
|
||||
// --- Update schemas ---
|
||||
|
||||
describe('updatePatchSchema', () => {
|
||||
it('accepts valid update patch', () => {
|
||||
const result = updatePatchSchema.safeParse({
|
||||
content: 'Updated content',
|
||||
privacy: 'members'
|
||||
})
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts empty object (all optional)', () => {
|
||||
const result = updatePatchSchema.safeParse({})
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects invalid privacy enum', () => {
|
||||
const result = updatePatchSchema.safeParse({ privacy: 'invalid' })
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// --- Series schemas ---
|
||||
|
||||
describe('seriesTicketPurchaseSchema', () => {
|
||||
|
|
@ -529,7 +506,6 @@ describe('validateBody migration coverage', () => {
|
|||
'events/[id]/payment.post.js',
|
||||
'members/update-contribution.post.js',
|
||||
'members/me/peer-support.patch.js',
|
||||
'updates/[id].patch.js',
|
||||
'series/[id]/tickets/purchase.post.js',
|
||||
'series/[id]/tickets/check-eligibility.post.js',
|
||||
'admin/series.post.js',
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import {
|
|||
memberCreateSchema,
|
||||
memberProfileUpdateSchema,
|
||||
eventRegistrationSchema,
|
||||
updateCreateSchema,
|
||||
paymentVerifySchema,
|
||||
adminEventCreateSchema
|
||||
} from '../../../server/utils/schemas.js'
|
||||
|
|
@ -120,55 +119,6 @@ describe('eventRegistrationSchema', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('updateCreateSchema', () => {
|
||||
it('accepts valid content', () => {
|
||||
const result = updateCreateSchema.safeParse({ content: 'Hello world' })
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects empty content', () => {
|
||||
const result = updateCreateSchema.safeParse({ content: '' })
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects content exceeding 50000 chars', () => {
|
||||
const result = updateCreateSchema.safeParse({ content: 'a'.repeat(50001) })
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('accepts content at exactly 50000 chars', () => {
|
||||
const result = updateCreateSchema.safeParse({ content: 'a'.repeat(50000) })
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('validates images are URLs', () => {
|
||||
const result = updateCreateSchema.safeParse({
|
||||
content: 'test',
|
||||
images: ['not-a-url']
|
||||
})
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('accepts valid images array', () => {
|
||||
const result = updateCreateSchema.safeParse({
|
||||
content: 'test',
|
||||
images: ['https://example.com/img.png']
|
||||
})
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects more than 20 images', () => {
|
||||
const images = Array.from({ length: 21 }, (_, i) => `https://example.com/img${i}.png`)
|
||||
const result = updateCreateSchema.safeParse({ content: 'test', images })
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
it('validates privacy enum', () => {
|
||||
const result = updateCreateSchema.safeParse({ content: 'test', privacy: 'invalid' })
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('paymentVerifySchema', () => {
|
||||
it('accepts valid card token and customer ID', () => {
|
||||
const result = paymentVerifySchema.safeParse({ cardToken: 'tok_123', customerId: 'cust_456' })
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue