import { describe, it, expect, vi, beforeEach } from 'vitest' vi.mock('../../../server/models/wikiArticle.js', () => ({ default: { find: vi.fn(), findByIdAndUpdate: vi.fn(), updateMany: vi.fn() } })) vi.mock('../../../server/utils/mongoose.js', () => ({ connectDB: vi.fn() })) import WikiArticle from '../../../server/models/wikiArticle.js' import indexHandler from '../../../server/api/admin/wiki/index.get.js' import patchHandler from '../../../server/api/admin/wiki/[id].patch.js' import batchTagHandler from '../../../server/api/admin/wiki/batch-tag.post.js' import { createMockEvent } from '../helpers/createMockEvent.js' // --- GET /api/admin/wiki --- describe('GET /api/admin/wiki', () => { beforeEach(() => { vi.clearAllMocks() requireAdmin.mockResolvedValue({ _id: 'admin-1', role: 'admin' }) }) it('returns all articles with collection, title, tags', async () => { const articles = [ { _id: 'a1', collection: 'General', title: 'Alpha', tags: ['co-ops'], url: '/doc/alpha', outlineId: 'o1', publishedAt: new Date() }, { _id: 'a2', collection: 'General', title: 'Beta', tags: [], url: '/doc/beta', outlineId: 'o2', publishedAt: new Date() } ] WikiArticle.find.mockReturnValue({ select: () => ({ sort: () => ({ lean: () => Promise.resolve(articles) }) }) }) const event = createMockEvent({ method: 'GET', path: '/api/admin/wiki' }) const result = await indexHandler(event) expect(result).toEqual(articles) expect(WikiArticle.find).toHaveBeenCalledWith({}) }) it('filters by collection', async () => { WikiArticle.find.mockReturnValue({ select: () => ({ sort: () => ({ lean: () => Promise.resolve([]) }) }) }) const event = createMockEvent({ method: 'GET', path: '/api/admin/wiki?collection=Guides' }) await indexHandler(event) expect(WikiArticle.find).toHaveBeenCalledWith({ collection: 'Guides' }) }) it('searches by title (case-insensitive)', async () => { WikiArticle.find.mockReturnValue({ select: () => ({ sort: () => ({ lean: () => Promise.resolve([]) }) }) }) const event = createMockEvent({ method: 'GET', path: '/api/admin/wiki?search=cooperative' }) await indexHandler(event) expect(WikiArticle.find).toHaveBeenCalledWith({ title: { $regex: 'cooperative', $options: 'i' } }) }) it('requires admin auth (403)', async () => { requireAdmin.mockRejectedValue( createError({ statusCode: 403, statusMessage: 'Admin access required' }) ) const event = createMockEvent({ method: 'GET', path: '/api/admin/wiki' }) await expect(indexHandler(event)).rejects.toMatchObject({ statusCode: 403 }) }) }) // --- PATCH /api/admin/wiki/:id --- describe('PATCH /api/admin/wiki/:id', () => { beforeEach(() => { vi.clearAllMocks() requireAdmin.mockResolvedValue({ _id: 'admin-1', role: 'admin' }) validateBody.mockImplementation(async (event) => readBody(event)) }) it('sets tags on an article', async () => { const updated = { _id: 'a1', title: 'Alpha', tags: ['co-ops', 'governance'], collection: 'General' } WikiArticle.findByIdAndUpdate.mockResolvedValue(updated) const event = createMockEvent({ method: 'PATCH', path: '/api/admin/wiki/a1', body: { tags: ['co-ops', 'governance'] } }) // Simulate getRouterParam returning the id event.context.params = { id: 'a1' } const result = await patchHandler(event) expect(result).toEqual(updated) expect(WikiArticle.findByIdAndUpdate).toHaveBeenCalledWith( 'a1', { tags: ['co-ops', 'governance'] }, { new: true } ) }) it('returns 404 for invalid article ID', async () => { WikiArticle.findByIdAndUpdate.mockResolvedValue(null) const event = createMockEvent({ method: 'PATCH', path: '/api/admin/wiki/nonexistent', body: { tags: ['test'] } }) event.context.params = { id: 'nonexistent' } await expect(patchHandler(event)).rejects.toMatchObject({ statusCode: 404 }) }) it('requires admin auth (403)', async () => { requireAdmin.mockRejectedValue( createError({ statusCode: 403, statusMessage: 'Admin access required' }) ) const event = createMockEvent({ method: 'PATCH', path: '/api/admin/wiki/a1', body: { tags: ['test'] } }) event.context.params = { id: 'a1' } await expect(patchHandler(event)).rejects.toMatchObject({ statusCode: 403 }) }) }) // --- POST /api/admin/wiki/batch-tag --- describe('POST /api/admin/wiki/batch-tag', () => { beforeEach(() => { vi.clearAllMocks() requireAdmin.mockResolvedValue({ _id: 'admin-1', role: 'admin' }) validateBody.mockImplementation(async (event) => readBody(event)) }) it('adds tags to multiple articles (add/merge semantics)', async () => { WikiArticle.updateMany.mockResolvedValue({ modifiedCount: 3 }) const event = createMockEvent({ method: 'POST', path: '/api/admin/wiki/batch-tag', body: { articleIds: ['a1', 'a2', 'a3'], addTags: ['governance', 'finance'] } }) const result = await batchTagHandler(event) expect(result).toEqual({ modified: 3 }) expect(WikiArticle.updateMany).toHaveBeenCalledWith( { _id: { $in: ['a1', 'a2', 'a3'] } }, { $addToSet: { tags: { $each: ['governance', 'finance'] } } } ) }) it('removes tags from multiple articles', async () => { WikiArticle.updateMany.mockResolvedValue({ modifiedCount: 2 }) const event = createMockEvent({ method: 'POST', path: '/api/admin/wiki/batch-tag', body: { collection: 'Guides', removeTags: ['outdated'] } }) const result = await batchTagHandler(event) expect(result).toEqual({ modified: 2 }) expect(WikiArticle.updateMany).toHaveBeenCalledWith( { collection: 'Guides' }, { $pull: { tags: { $in: ['outdated'] } } } ) }) it('does not duplicate existing tags on add', async () => { // $addToSet guarantees no duplicates — verify the operator is used WikiArticle.updateMany.mockResolvedValue({ modifiedCount: 1 }) const event = createMockEvent({ method: 'POST', path: '/api/admin/wiki/batch-tag', body: { articleIds: ['a1'], addTags: ['co-ops'] } }) await batchTagHandler(event) // Confirm $addToSet is used, not $push const call = WikiArticle.updateMany.mock.calls[0] expect(call[1]).toHaveProperty('$addToSet') expect(call[1]).not.toHaveProperty('$push') }) it('requires admin auth (403)', async () => { requireAdmin.mockRejectedValue( createError({ statusCode: 403, statusMessage: 'Admin access required' }) ) const event = createMockEvent({ method: 'POST', path: '/api/admin/wiki/batch-tag', body: { articleIds: ['a1'], addTags: ['test'] } }) await expect(batchTagHandler(event)).rejects.toMatchObject({ statusCode: 403 }) }) })