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

266 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** @jest-environment node */
import {
createNotificationCenterProject,
listNotificationCenterProjectRecipients,
listNotificationCenterProjects,
NotificationCenterAdminError,
revealNotificationCenterProjectSecret,
rotateNotificationCenterProjectSecret,
sendNotificationCenterProjectTest,
updateNotificationCenterProject,
} from '@/lib/notification-center-admin'
describe('notification-center-admin client', () => {
const actor = {
userId: 'super-1',
username: 'wuxie',
realName: '无邪',
email: 'wuxie@example.com',
}
beforeEach(() => {
jest.restoreAllMocks()
process.env.YYD_NOTIFICATION_CENTER_ADMIN_URL = 'https://notify-admin.example.com'
process.env.YYD_NOTIFICATION_CENTER_ADMIN_API_SECRET = 'server-admin-secret'
delete process.env.YYD_NOTIFICATION_CENTER_ADMIN_TIMEOUT_MS
global.fetch = jest.fn()
})
test('列表查询会透传操作人身份并剥离 project/admin secret', async () => {
;(global.fetch as jest.Mock).mockResolvedValue({
ok: true,
status: 200,
text: async () => JSON.stringify({
items: [
{
code: 'material_library',
name: '素材库',
projectSecret: 'should-not-return',
env: 'SECRET=1',
adminSecret: 'server-admin-secret',
},
],
adminKey: 'server-admin-secret',
}),
})
const result = await listNotificationCenterProjects(actor)
expect(result.status).toBe(200)
expect(result.data).toEqual({
items: [
{
code: 'material_library',
name: '素材库',
},
],
})
const [, init] = (global.fetch as jest.Mock).mock.calls[0]
const headers = init.headers as Record<string, string>
expect(headers.Authorization).toBe('Bearer server-admin-secret')
expect(headers['x-yyd-admin-actor']).toBe('super-1')
expect(headers['x-yyd-admin-display-name']).toBe('无邪')
})
test('创建项目仅保留一次性 project secret/env,不回传 admin secret', async () => {
;(global.fetch as jest.Mock).mockResolvedValue({
ok: true,
status: 201,
text: async () => JSON.stringify({
code: 'material_library',
projectSecret: 'project-secret-1',
env: 'YYD_NOTIFICATION_CENTER_PROJECT_SECRET=project-secret-1',
adminSecret: 'server-admin-secret',
adminKey: 'server-admin-secret',
}),
})
const result = await createNotificationCenterProject(actor, { code: 'material_library' })
expect(result.status).toBe(201)
expect(result.data).toEqual({
code: 'material_library',
projectSecret: 'project-secret-1',
env: 'YYD_NOTIFICATION_CENTER_PROJECT_SECRET=project-secret-1',
})
})
test('轮换密钥同样只保留一次性 project secret/env', async () => {
;(global.fetch as jest.Mock).mockResolvedValue({
ok: true,
status: 200,
text: async () => JSON.stringify({
projectSecret: 'next-secret',
envText: 'YYD_NOTIFICATION_CENTER_PROJECT_SECRET=next-secret',
adminToken: 'server-admin-secret',
}),
})
const result = await rotateNotificationCenterProjectSecret(actor, 'material_library')
expect(result.data).toEqual({
projectSecret: 'next-secret',
envText: 'YYD_NOTIFICATION_CENTER_PROJECT_SECRET=next-secret',
})
})
test('查看当前密钥命中专属接口,保留 project secret 且剥离 admin secret', async () => {
;(global.fetch as jest.Mock).mockResolvedValue({
ok: true,
status: 200,
text: async () => JSON.stringify({
projectCode: 'script_monitor',
secret: 'current-project-secret',
env: {
YYD_NOTIFICATION_CENTER_PROJECT_CODE: 'script_monitor',
YYD_NOTIFICATION_CENTER_PROJECT_SECRET: 'current-project-secret',
YYD_NOTIFICATION_CENTER_ADMIN_API_SECRET: 'server-admin-secret',
},
project: { recipients: [{ userId: 'ding-private-user' }] },
adminSecret: 'server-admin-secret',
}),
})
const result = await revealNotificationCenterProjectSecret(actor, 'script_monitor')
expect((global.fetch as jest.Mock).mock.calls[0][0]).toBe(
'https://notify-admin.example.com/projects/script_monitor/reveal-secret',
)
expect((global.fetch as jest.Mock).mock.calls[0][1]).toMatchObject({
method: 'POST',
cache: 'no-store',
})
expect(result.data).toEqual({
projectCode: 'script_monitor',
secret: 'current-project-secret',
env: {
YYD_NOTIFICATION_CENTER_PROJECT_CODE: 'script_monitor',
YYD_NOTIFICATION_CENTER_PROJECT_SECRET: 'current-project-secret',
},
})
expect(JSON.stringify(result.data)).not.toContain('server-admin-secret')
expect(JSON.stringify(result.data)).not.toContain('ding-private-user')
})
test('PATCH 项目更新与项目级 recipients/test 会命中嵌套路径', async () => {
;(global.fetch as jest.Mock)
.mockResolvedValueOnce({
ok: true,
status: 200,
text: async () => JSON.stringify({ code: 'material_library', name: '素材库-新版' }),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
text: async () => JSON.stringify([{ userId: 'ding-1' }]),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
text: async () => JSON.stringify({ id: 'test-1' }),
})
await updateNotificationCenterProject(actor, 'material_library', { name: '素材库-新版' })
await listNotificationCenterProjectRecipients(
actor,
'material_library',
new URLSearchParams('page=2'),
)
await sendNotificationCenterProjectTest(actor, 'material_library', {
title: '连通性测试',
projectCode: 'material_library',
})
expect((global.fetch as jest.Mock).mock.calls[0][0]).toBe(
'https://notify-admin.example.com/projects/material_library',
)
expect((global.fetch as jest.Mock).mock.calls[0][1]).toMatchObject({
method: 'PATCH',
})
expect((global.fetch as jest.Mock).mock.calls[1][0]).toBe(
'https://notify-admin.example.com/projects/material_library/recipients?page=2',
)
expect((global.fetch as jest.Mock).mock.calls[2][0]).toBe(
'https://notify-admin.example.com/projects/material_library/test',
)
expect((global.fetch as jest.Mock).mock.calls[2][1]).toMatchObject({
method: 'POST',
body: JSON.stringify({ title: '连通性测试' }),
})
})
test('下游 401 会转成中文配置错误且不泄漏管理员密钥', async () => {
;(global.fetch as jest.Mock).mockResolvedValue({
ok: false,
status: 401,
text: async () => JSON.stringify({
error: 'invalid admin key',
adminKey: 'server-admin-secret',
}),
})
await expect(listNotificationCenterProjects(actor)).rejects.toMatchObject({
statusCode: 502,
code: 'NOTIFICATION_CENTER_ADMIN_UNAUTHORIZED',
})
await listNotificationCenterProjects(actor).catch((error: NotificationCenterAdminError) => {
expect(error.message).toContain('通知中心管理员鉴权失败')
expect(error.message).not.toContain('server-admin-secret')
expect(error.details || '').not.toContain('server-admin-secret')
})
})
test('409 与 422 会保留业务状态并输出中文错误', async () => {
;(global.fetch as jest.Mock)
.mockResolvedValueOnce({
ok: false,
status: 409,
text: async () => JSON.stringify({ error: 'project already exists' }),
})
.mockResolvedValueOnce({
ok: false,
status: 422,
text: async () => JSON.stringify({ message: 'code is required' }),
})
await expect(createNotificationCenterProject(actor, {})).rejects.toMatchObject({
statusCode: 409,
code: 'NOTIFICATION_CENTER_ADMIN_CONFLICT',
})
await expect(createNotificationCenterProject(actor, {})).rejects.toMatchObject({
statusCode: 422,
code: 'NOTIFICATION_CENTER_ADMIN_VALIDATION',
})
})
test('下游 500 会转成中文服务异常', async () => {
;(global.fetch as jest.Mock).mockResolvedValue({
ok: false,
status: 500,
text: async () => JSON.stringify({ error: 'internal error' }),
})
await expect(listNotificationCenterProjects(actor)).rejects.toMatchObject({
statusCode: 502,
code: 'NOTIFICATION_CENTER_ADMIN_UPSTREAM',
})
})
test('离线与超时会映射为中文错误', async () => {
;(global.fetch as jest.Mock)
.mockRejectedValueOnce(new TypeError('fetch failed'))
.mockRejectedValueOnce(Object.assign(new Error('aborted'), { name: 'AbortError' }))
await expect(listNotificationCenterProjects(actor)).rejects.toMatchObject({
statusCode: 503,
code: 'NOTIFICATION_CENTER_ADMIN_OFFLINE',
})
await expect(listNotificationCenterProjects(actor)).rejects.toMatchObject({
statusCode: 504,
code: 'NOTIFICATION_CENTER_ADMIN_TIMEOUT',
})
})
})