feat(events): add tag validation to admin event create/edit routes
This commit is contained in:
parent
3797ff7925
commit
2166ee32ca
3 changed files with 185 additions and 0 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import Event from "../../models/event.js";
|
||||
import Tag from "../../models/tag.js";
|
||||
import { connectDB } from "../../utils/mongoose.js";
|
||||
import { requireAdmin } from "../../utils/auth.js";
|
||||
import { validateBody } from "../../utils/validateBody.js";
|
||||
|
|
@ -12,6 +13,19 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
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 = {
|
||||
...body,
|
||||
createdBy: admin.email,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import Event from '../../../models/event.js'
|
||||
import Tag from '../../../models/tag.js'
|
||||
import { connectDB } from '../../../utils/mongoose.js'
|
||||
import { requireAdmin } from '../../../utils/auth.js'
|
||||
|
||||
|
|
@ -11,6 +12,19 @@ export default defineEventHandler(async (event) => {
|
|||
|
||||
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 = {
|
||||
...body,
|
||||
startDate: new Date(body.startDate),
|
||||
|
|
|
|||
157
tests/server/api/event-tags.test.js
Normal file
157
tests/server/api/event-tags.test.js
Normal 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'
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue