234 lines
9.5 KiB
TypeScript
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('未登录')
|
|
})
|
|
})
|