From f0284c60b47c603fdc345895434e20986685414e Mon Sep 17 00:00:00 2001 From: Jennie Robinson Faber Date: Wed, 8 Apr 2026 11:17:50 +0100 Subject: [PATCH] feat(admin): add GET /api/admin/alerts endpoint --- server/api/admin/alerts/index.get.js | 8 +++ tests/server/api/admin-alerts.test.js | 89 +++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 server/api/admin/alerts/index.get.js create mode 100644 tests/server/api/admin-alerts.test.js diff --git a/server/api/admin/alerts/index.get.js b/server/api/admin/alerts/index.get.js new file mode 100644 index 0000000..4f992df --- /dev/null +++ b/server/api/admin/alerts/index.get.js @@ -0,0 +1,8 @@ +import { computeAllAlerts } from '../../../utils/adminAlerts.js' + +export default defineEventHandler(async (event) => { + const admin = await requireAdmin(event) + const adminId = admin._id.toString() + const alerts = await computeAllAlerts(adminId) + return { alerts } +}) diff --git a/tests/server/api/admin-alerts.test.js b/tests/server/api/admin-alerts.test.js new file mode 100644 index 0000000..8ea6011 --- /dev/null +++ b/tests/server/api/admin-alerts.test.js @@ -0,0 +1,89 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' + +vi.mock('../../../server/utils/adminAlerts.js', () => ({ + computeAllAlerts: vi.fn() +})) + +vi.mock('../../../server/models/adminAlertDismissal.js', () => ({ + default: { + findOneAndUpdate: vi.fn() + }, + ADMIN_ALERT_TYPES: [ + 'slack_invite_failed', + 'no_slack_handle_week', + 'stuck_pending_payment', + 'member_suspended', + 'preregistrant_selected_not_invited', + 'preregistrant_expired', + 'event_draft_imminent', + 'event_near_capacity', + 'tag_suggestions_pending' + ] +})) + +vi.mock('../../../server/utils/mongoose.js', () => ({ + connectDB: vi.fn() +})) + +import getHandler from '../../../server/api/admin/alerts/index.get.js' +import { computeAllAlerts } from '../../../server/utils/adminAlerts.js' +import { createMockEvent } from '../helpers/createMockEvent.js' + +describe('GET /api/admin/alerts', () => { + beforeEach(() => { + vi.clearAllMocks() + requireAdmin.mockResolvedValue({ _id: { toString: () => 'admin-1' } }) + }) + + describe('source inspection', () => { + const source = readFileSync( + resolve(import.meta.dirname, '../../../server/api/admin/alerts/index.get.js'), + 'utf-8' + ) + it('calls requireAdmin before any business logic', () => { + const adminIdx = source.indexOf('requireAdmin(event)') + const computeIdx = source.indexOf('computeAllAlerts(') + expect(adminIdx).toBeGreaterThan(-1) + expect(computeIdx).toBeGreaterThan(-1) + expect(adminIdx).toBeLessThan(computeIdx) + }) + }) + + it('returns the active alerts for the current admin', async () => { + computeAllAlerts.mockResolvedValue([ + { + type: 'slack_invite_failed', + severity: 'critical', + title: 'Slack invites failed', + count: 1, + items: [{ id: 'm1', label: 'Alex', sublabel: 'alex@example.com', href: '/admin/members/m1' }], + signature: 'abc' + } + ]) + + const event = createMockEvent({ method: 'GET', path: '/api/admin/alerts' }) + const result = await getHandler(event) + + expect(result).toEqual({ + alerts: [ + { + type: 'slack_invite_failed', + severity: 'critical', + title: 'Slack invites failed', + count: 1, + items: [{ id: 'm1', label: 'Alex', sublabel: 'alex@example.com', href: '/admin/members/m1' }], + signature: 'abc' + } + ] + }) + expect(computeAllAlerts).toHaveBeenCalledWith('admin-1') + }) + + it('rejects non-admin requests', async () => { + requireAdmin.mockRejectedValue(createError({ statusCode: 403, statusMessage: 'Forbidden' })) + const event = createMockEvent({ method: 'GET', path: '/api/admin/alerts' }) + await expect(getHandler(event)).rejects.toMatchObject({ statusCode: 403 }) + }) +})