gongxiangp2/lib/__tests__/notification-center-admin-routes.test.ts

234 lines
9.5 KiB
TypeScript

/** @jest-environment node */
import { getServerSession } from 'next-auth'
import { isSuperAdmin } from '@/lib/department-permission'
import {
createNotificationCenterProject,
getNotificationCenterProject,
listNotificationCenterAudits,
listNotificationCenterDeliveries,
listNotificationCenterProjectRecipients,
listNotificationCenterProjects,
rotateNotificationCenterProjectSecret,
sendNotificationCenterProjectTest,
updateNotificationCenterProject,
NotificationCenterAdminError,
} from '@/lib/notification-center-admin'
import { GET as getProjectsRoute, POST as postProjectsRoute } from '@/app/api/admin/notification-center/projects/route'
import { GET as getProjectRoute, PATCH as patchProjectRoute } from '@/app/api/admin/notification-center/projects/[code]/route'
import { POST as rotateProjectSecretRoute } from '@/app/api/admin/notification-center/projects/[code]/rotate-secret/route'
import { GET as getProjectRecipientsRoute } from '@/app/api/admin/notification-center/projects/[code]/recipients/route'
import { POST as postProjectTestRoute } from '@/app/api/admin/notification-center/projects/[code]/test/route'
import { GET as getDeliveriesRoute } from '@/app/api/admin/notification-center/deliveries/route'
import { GET as getAuditsRoute } from '@/app/api/admin/notification-center/audits/route'
jest.mock('next-auth', () => ({
getServerSession: jest.fn(),
}))
jest.mock('@/lib/auth', () => ({
authOptions: {},
}))
jest.mock('@/lib/department-permission', () => ({
isSuperAdmin: jest.fn(),
}))
jest.mock('@/lib/notification-center-admin', () => ({
NotificationCenterAdminError: class NotificationCenterAdminError extends Error {
statusCode: number
code: string
details?: string | null
constructor(message: string, options: { statusCode?: number; code?: string; details?: string | null } = {}) {
super(message)
this.name = 'NotificationCenterAdminError'
this.statusCode = options.statusCode ?? 500
this.code = options.code ?? 'NOTIFICATION_CENTER_ADMIN_ERROR'
this.details = options.details ?? null
}
},
listNotificationCenterProjects: jest.fn(),
createNotificationCenterProject: jest.fn(),
getNotificationCenterProject: jest.fn(),
updateNotificationCenterProject: jest.fn(),
rotateNotificationCenterProjectSecret: jest.fn(),
listNotificationCenterProjectRecipients: jest.fn(),
sendNotificationCenterProjectTest: jest.fn(),
listNotificationCenterDeliveries: jest.fn(),
listNotificationCenterAudits: jest.fn(),
}))
describe('notification-center admin routes', () => {
beforeEach(() => {
jest.clearAllMocks()
;(getServerSession as jest.Mock).mockResolvedValue({
user: {
id: 'admin-1',
role: 'SYSTEM_ADMIN',
isSuperAdmin: true,
username: 'wuxie',
realName: '无邪',
email: 'wuxie@example.com',
},
})
;(isSuperAdmin as jest.Mock).mockResolvedValue(true)
})
test('非真正超管即使 session 带 SYSTEM_ADMIN / isSuperAdmin 也返回 403', async () => {
;(isSuperAdmin as jest.Mock).mockResolvedValue(false)
const response = await getProjectsRoute(new Request('http://localhost/api/admin/notification-center/projects') as any)
const body = await response.json()
expect(response.status).toBe(403)
expect(body.error).toContain('仅超级管理员可访问')
expect(listNotificationCenterProjects).not.toHaveBeenCalled()
})
test('GET /projects 调用后端代理层并传操作人身份', async () => {
;(listNotificationCenterProjects as jest.Mock).mockResolvedValue({
status: 200,
data: [{ code: 'material_library' }],
})
const response = await getProjectsRoute(new Request('http://localhost/api/admin/notification-center/projects?page=1') as any)
const body = await response.json()
expect(response.status).toBe(200)
expect(body.success).toBe(true)
expect(listNotificationCenterProjects).toHaveBeenCalledWith(
expect.objectContaining({
userId: 'admin-1',
username: 'wuxie',
realName: '无邪',
}),
expect.any(URLSearchParams),
)
})
test('POST /projects 透传一次性 project secret/env', async () => {
;(createNotificationCenterProject as jest.Mock).mockResolvedValue({
status: 201,
data: {
code: 'material_library',
projectSecret: 'project-secret-1',
env: 'YYD_NOTIFICATION_CENTER_PROJECT_SECRET=project-secret-1',
},
})
const request = {
json: jest.fn().mockResolvedValue({ code: 'material_library' }),
} as any
const response = await postProjectsRoute(request)
const body = await response.json()
expect(response.status).toBe(201)
expect(body.data.projectSecret).toBe('project-secret-1')
expect(body.data.env).toContain('YYD_NOTIFICATION_CENTER_PROJECT_SECRET')
})
test('GET /projects/[code] 与 PATCH /projects/[code] 可用', async () => {
;(getNotificationCenterProject as jest.Mock).mockResolvedValue({
status: 200,
data: { code: 'material_library', name: '素材库' },
})
;(updateNotificationCenterProject as jest.Mock).mockResolvedValue({
status: 200,
data: { code: 'material_library', name: '素材库-新版' },
})
const getResponse = await getProjectRoute(new Request('http://localhost') as any, { params: { code: 'material_library' } } as any)
const patchResponse = await patchProjectRoute(
{ json: jest.fn().mockResolvedValue({ name: '素材库-新版' }) } as any,
{ params: { code: 'material_library' } } as any,
)
expect(getResponse.status).toBe(200)
expect((await getResponse.json()).data.code).toBe('material_library')
expect(patchResponse.status).toBe(200)
expect((await patchResponse.json()).data.name).toBe('素材库-新版')
})
test('POST /projects/[code]/rotate-secret 透传一次性 secret/env', async () => {
;(rotateNotificationCenterProjectSecret as jest.Mock).mockResolvedValue({
status: 200,
data: {
projectSecret: 'rotated-secret',
envText: 'YYD_NOTIFICATION_CENTER_PROJECT_SECRET=rotated-secret',
},
})
const response = await rotateProjectSecretRoute(
{ json: jest.fn().mockResolvedValue({ reason: 'rotate' }) } as any,
{ params: { code: 'material_library' } } as any,
)
const body = await response.json()
expect(response.status).toBe(200)
expect(body.data.projectSecret).toBe('rotated-secret')
})
test('GET /projects/[code]/recipients、GET /deliveries、GET /audits、POST /projects/[code]/test 可用', async () => {
;(listNotificationCenterProjectRecipients as jest.Mock).mockResolvedValue({ status: 200, data: [{ userId: 'ding-1' }] })
;(listNotificationCenterDeliveries as jest.Mock).mockResolvedValue({ status: 200, data: [{ id: 'delivery-1' }] })
;(listNotificationCenterAudits as jest.Mock).mockResolvedValue({ status: 200, data: [{ id: 'audit-1' }] })
;(sendNotificationCenterProjectTest as jest.Mock).mockResolvedValue({ status: 200, data: { id: 'test-1' } })
const recipients = await getProjectRecipientsRoute(
new Request('http://localhost/api/admin/notification-center/projects/material_library/recipients?page=2') as any,
{ params: { code: 'material_library' } } as any,
)
const deliveries = await getDeliveriesRoute(new Request('http://localhost/api/admin/notification-center/deliveries?projectCode=material_library') as any)
const audits = await getAuditsRoute(new Request('http://localhost/api/admin/notification-center/audits?projectCode=material_library') as any)
const testResponse = await postProjectTestRoute(
{ json: jest.fn().mockResolvedValue({ title: '连通性测试' }) } as any,
{ params: { code: 'material_library' } } as any,
)
expect(recipients.status).toBe(200)
expect(deliveries.status).toBe(200)
expect(audits.status).toBe(200)
expect(testResponse.status).toBe(200)
expect(listNotificationCenterProjectRecipients).toHaveBeenCalledWith(
expect.objectContaining({ userId: 'admin-1' }),
'material_library',
expect.any(URLSearchParams),
)
expect(sendNotificationCenterProjectTest).toHaveBeenCalledWith(
expect.objectContaining({ userId: 'admin-1' }),
'material_library',
{ title: '连通性测试' },
)
})
test('下游错误会返回中文错误且不泄漏管理员密钥', async () => {
const error = new NotificationCenterAdminError('通知中心管理员鉴权失败,请检查服务端密钥配置', {
statusCode: 502,
code: 'NOTIFICATION_CENTER_ADMIN_UNAUTHORIZED',
details: 'invalid admin key',
})
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined)
;(listNotificationCenterProjects as jest.Mock).mockRejectedValue(error)
const response = await getProjectsRoute(new Request('http://localhost/api/admin/notification-center/projects') as any)
const body = await response.json()
expect(response.status).toBe(502)
expect(body.error).toContain('通知中心管理员鉴权失败')
expect(JSON.stringify(body)).not.toContain('server-admin-secret')
expect(consoleSpy).not.toHaveBeenCalledWith(expect.stringContaining('server-admin-secret'))
})
test('未登录返回 401', async () => {
;(getServerSession as jest.Mock).mockResolvedValue(null)
const response = await getProjectsRoute(new Request('http://localhost/api/admin/notification-center/projects') as any)
const body = await response.json()
expect(response.status).toBe(401)
expect(body.error).toBe('未登录')
})
})