feat(events): add tag validation to admin event create/edit routes

This commit is contained in:
Jennie Robinson Faber 2026-04-09 22:32:32 +01:00
parent 3797ff7925
commit 2166ee32ca
3 changed files with 185 additions and 0 deletions

View file

@ -1,4 +1,5 @@
import Event from "../../models/event.js"; import Event from "../../models/event.js";
import Tag from "../../models/tag.js";
import { connectDB } from "../../utils/mongoose.js"; import { connectDB } from "../../utils/mongoose.js";
import { requireAdmin } from "../../utils/auth.js"; import { requireAdmin } from "../../utils/auth.js";
import { validateBody } from "../../utils/validateBody.js"; import { validateBody } from "../../utils/validateBody.js";
@ -12,6 +13,19 @@ export default defineEventHandler(async (event) => {
await connectDB(); await connectDB();
// Validate tag slugs against Tag collection
if (body.tags && body.tags.length > 0) {
const foundTags = await Tag.find({ slug: { $in: body.tags } });
const foundSlugs = new Set(foundTags.map((t) => t.slug));
const invalid = body.tags.filter((s) => !foundSlugs.has(s));
if (invalid.length > 0) {
throw createError({
statusCode: 400,
statusMessage: `Unknown tag slugs: ${invalid.join(", ")}`,
});
}
}
const eventData = { const eventData = {
...body, ...body,
createdBy: admin.email, createdBy: admin.email,

View file

@ -1,4 +1,5 @@
import Event from '../../../models/event.js' import Event from '../../../models/event.js'
import Tag from '../../../models/tag.js'
import { connectDB } from '../../../utils/mongoose.js' import { connectDB } from '../../../utils/mongoose.js'
import { requireAdmin } from '../../../utils/auth.js' import { requireAdmin } from '../../../utils/auth.js'
@ -11,6 +12,19 @@ export default defineEventHandler(async (event) => {
await connectDB() await connectDB()
// Validate tag slugs against Tag collection
if (body.tags && body.tags.length > 0) {
const foundTags = await Tag.find({ slug: { $in: body.tags } })
const foundSlugs = new Set(foundTags.map(t => t.slug))
const invalid = body.tags.filter(s => !foundSlugs.has(s))
if (invalid.length > 0) {
throw createError({
statusCode: 400,
statusMessage: `Unknown tag slugs: ${invalid.join(', ')}`
})
}
}
const updateData = { const updateData = {
...body, ...body,
startDate: new Date(body.startDate), startDate: new Date(body.startDate),

View file

@ -0,0 +1,157 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
// --- Mocks (must be before imports) ---
const mockEventSave = vi.fn()
function MockEvent(data) {
Object.assign(this, data)
this._id = 'evt-123'
this.slug = '2026-04-01-test-event'
this.save = mockEventSave.mockResolvedValue(this)
}
MockEvent.findByIdAndUpdate = vi.fn()
vi.mock('../../../server/models/event.js', () => ({ default: MockEvent }))
vi.mock('../../../server/models/tag.js', () => ({
default: { find: vi.fn() }
}))
vi.mock('../../../server/utils/mongoose.js', () => ({ connectDB: vi.fn() }))
vi.mock('../../../server/utils/auth.js', () => ({
requireAdmin: vi.fn().mockResolvedValue({ email: 'admin@example.com', role: 'admin' })
}))
vi.mock('../../../server/utils/validateBody.js', () => ({ validateBody: vi.fn() }))
vi.mock('../../../server/utils/schemas.js', () => ({
adminEventCreateSchema: {},
adminEventUpdateSchema: {}
}))
import Tag from '../../../server/models/tag.js'
import { validateBody } from '../../../server/utils/validateBody.js'
import createHandler from '../../../server/api/admin/events.post.js'
import updateHandler from '../../../server/api/admin/events/[id].put.js'
import { createMockEvent } from '../helpers/createMockEvent.js'
// Stub the global adminEventUpdateSchema used by the update route (auto-imported by Nitro)
vi.stubGlobal('adminEventUpdateSchema', {})
// --- Helpers ---
const validEventBody = {
title: 'Test Event',
description: 'A test event',
startDate: '2026-04-01T10:00:00Z',
endDate: '2026-04-01T12:00:00Z',
location: 'https://meet.example.com'
}
// --- Tests ---
describe('Event tag validation', () => {
beforeEach(() => {
vi.clearAllMocks()
// Override the global validateBody stub (from setup.js) to use our mock
globalThis.validateBody = validateBody
})
describe('create route (events.post)', () => {
it('accepts valid tags', async () => {
validateBody.mockResolvedValue({
...validEventBody,
tags: ['game-design', 'cooperatives']
})
Tag.find.mockResolvedValue([
{ slug: 'game-design' },
{ slug: 'cooperatives' }
])
const event = createMockEvent({ method: 'POST', path: '/api/admin/events' })
const result = await createHandler(event)
expect(Tag.find).toHaveBeenCalledWith({ slug: { $in: ['game-design', 'cooperatives'] } })
expect(result.tags).toEqual(['game-design', 'cooperatives'])
})
it('rejects unknown tag slugs with 400', async () => {
validateBody.mockResolvedValue({
...validEventBody,
tags: ['game-design', 'nonexistent-tag']
})
Tag.find.mockResolvedValue([
{ slug: 'game-design' }
])
const event = createMockEvent({ method: 'POST', path: '/api/admin/events' })
await expect(createHandler(event)).rejects.toMatchObject({
statusCode: 400,
statusMessage: 'Unknown tag slugs: nonexistent-tag'
})
})
it('skips validation when tags are omitted', async () => {
validateBody.mockResolvedValue({ ...validEventBody })
const event = createMockEvent({ method: 'POST', path: '/api/admin/events' })
await createHandler(event)
expect(Tag.find).not.toHaveBeenCalled()
})
it('skips validation when tags array is empty', async () => {
validateBody.mockResolvedValue({ ...validEventBody, tags: [] })
const event = createMockEvent({ method: 'POST', path: '/api/admin/events' })
await createHandler(event)
expect(Tag.find).not.toHaveBeenCalled()
})
})
describe('update route (events/[id].put)', () => {
beforeEach(() => {
MockEvent.findByIdAndUpdate.mockResolvedValue({
_id: 'evt-123',
...validEventBody,
tags: ['game-design']
})
})
it('accepts valid tags', async () => {
validateBody.mockResolvedValue({
...validEventBody,
tags: ['game-design']
})
Tag.find.mockResolvedValue([
{ slug: 'game-design' }
])
const event = createMockEvent({ method: 'PUT', path: '/api/admin/events/evt-123' })
event.context = { ...event.context, params: { id: 'evt-123' } }
const result = await updateHandler(event)
expect(Tag.find).toHaveBeenCalledWith({ slug: { $in: ['game-design'] } })
expect(result.tags).toEqual(['game-design'])
})
it('rejects unknown tag slugs with 400', async () => {
validateBody.mockResolvedValue({
...validEventBody,
tags: ['valid-tag', 'bogus-tag', 'also-bogus']
})
Tag.find.mockResolvedValue([
{ slug: 'valid-tag' }
])
const event = createMockEvent({ method: 'PUT', path: '/api/admin/events/evt-123' })
event.context = { ...event.context, params: { id: 'evt-123' } }
await expect(updateHandler(event)).rejects.toMatchObject({
statusCode: 400,
statusMessage: 'Unknown tag slugs: bogus-tag, also-bogus'
})
})
})
})