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

290 lines
10 KiB
TypeScript

/** @jest-environment node */
import { GET as getTargetsRoute, POST as createTargetRoute } from '@/app/api/admin/external-notification-targets/route'
import { PUT as updateTargetRoute } from '@/app/api/admin/external-notification-targets/[id]/route'
import { GET as getDeliveriesRoute } from '@/app/api/admin/external-notification-deliveries/route'
import { POST as retryDeliveryRoute } from '@/app/api/admin/external-notification-deliveries/[id]/retry/route'
import { getServerSession } from 'next-auth'
import {
buildExternalNotificationTargetAdminView,
createExternalNotificationTarget,
getExternalNotificationTargetById,
listExternalNotificationDeliveries,
listExternalNotificationTargets,
retryExternalNotificationDelivery,
updateExternalNotificationTarget,
} from '@/lib/external-auto-listing'
import { isSuperAdmin } from '@/lib/department-permission'
jest.mock('next-auth', () => ({
getServerSession: jest.fn(),
}))
jest.mock('@/lib/auth', () => ({
authOptions: {},
}))
jest.mock('@/lib/department-permission', () => ({
isSuperAdmin: jest.fn(),
}))
jest.mock('@/lib/external-auto-listing', () => ({
AUTO_LISTING_EVENT_TYPE: 'task-detail.full-set.ready',
buildExternalNotificationTargetAdminView: jest.fn((target) => ({
...target,
authConfig: target.authType === 'header'
? { headerName: target.authConfig.headerName, headerValue: '' }
: target.authType === 'bearer'
? { token: '' }
: {},
authConfigured: true,
authMasked: target.authType === 'header'
? `Header · ${target.authConfig.headerName} · conf****1234`
: 'Bearer · secr****oken',
})),
createExternalNotificationTarget: jest.fn(),
getExternalNotificationTargetById: jest.fn(),
listExternalNotificationTargets: jest.fn(),
updateExternalNotificationTarget: jest.fn(),
listExternalNotificationDeliveries: jest.fn(),
retryExternalNotificationDelivery: jest.fn(),
}))
describe('external notification admin routes', () => {
let consoleErrorSpy: jest.SpyInstance
beforeEach(() => {
jest.clearAllMocks()
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined)
;(getServerSession as jest.Mock).mockResolvedValue({
user: {
id: 'admin-1',
role: 'SYSTEM_ADMIN',
},
})
;(isSuperAdmin as jest.Mock).mockResolvedValue(true)
})
afterEach(() => {
consoleErrorSpy.mockRestore()
})
test('GET /api/admin/external-notification-targets 应返回通知目标列表', async () => {
;(listExternalNotificationTargets as jest.Mock).mockResolvedValue([
{
id: 'target-1',
name: '自动上架平台',
callbackUrl: 'https://auto.example.com/callback',
isActive: true,
authType: 'bearer',
authConfig: { token: 'secret-token' },
eventTypes: ['task-detail.full-set.ready'],
createdAt: '2026-03-25T10:00:00.000Z',
updatedAt: '2026-03-25T10:00:00.000Z',
},
])
const response = await getTargetsRoute(new Request('http://localhost/api/admin/external-notification-targets') as any)
const body = await response.json()
expect(response.status).toBe(200)
expect(body.success).toBe(true)
expect(body.data).toHaveLength(1)
expect(body.data[0].authConfig).toEqual({ token: '' })
expect(body.data[0].authMasked).toContain('Bearer')
expect(JSON.stringify(body)).not.toContain('secret-token')
expect(buildExternalNotificationTargetAdminView).toHaveBeenCalled()
})
test('POST /api/admin/external-notification-targets 应创建通知目标', async () => {
;(createExternalNotificationTarget as jest.Mock).mockResolvedValue({
id: 'target-1',
name: '自动上架平台',
callbackUrl: 'https://auto.example.com/callback',
isActive: true,
authType: 'bearer',
authConfig: { token: 'secret-token' },
eventTypes: ['task-detail.full-set.ready'],
createdAt: '2026-03-25T10:00:00.000Z',
updatedAt: '2026-03-25T10:00:00.000Z',
})
const request = {
json: jest.fn().mockResolvedValue({
name: '自动上架平台',
callbackUrl: 'https://auto.example.com/callback',
authType: 'bearer',
authConfig: { token: 'secret-token' },
}),
} as any
const response = await createTargetRoute(request)
const body = await response.json()
expect(response.status).toBe(200)
expect(body.success).toBe(true)
expect(body.data.authConfig).toEqual({ token: '' })
expect(JSON.stringify(body)).not.toContain('secret-token')
expect(createExternalNotificationTarget).toHaveBeenCalledWith({
name: '自动上架平台',
callbackUrl: 'https://auto.example.com/callback',
authType: 'bearer',
authConfig: { token: 'secret-token' },
eventTypes: undefined,
isActive: undefined,
})
})
test('POST /api/admin/external-notification-targets 参数不合法时应返回 400', async () => {
;(createExternalNotificationTarget as jest.Mock).mockRejectedValue(
new Error('Bearer 鉴权必须提供 token'),
)
const request = {
json: jest.fn().mockResolvedValue({
name: '自动上架平台',
callbackUrl: 'https://auto.example.com/callback',
authType: 'bearer',
authConfig: {},
}),
} as any
const response = await createTargetRoute(request)
const body = await response.json()
expect(response.status).toBe(400)
expect(body.success).toBe(false)
})
test('PUT /api/admin/external-notification-targets/[id] 应更新通知目标', async () => {
;(getExternalNotificationTargetById as jest.Mock).mockResolvedValue({
id: 'target-1',
name: '自动上架平台',
callbackUrl: 'https://auto.example.com/callback',
isActive: true,
authType: 'header',
authConfig: { headerName: 'X-Signature', headerValue: 'old-secret' },
eventTypes: ['task-detail.full-set.ready'],
createdAt: '2026-03-25T10:00:00.000Z',
updatedAt: '2026-03-25T10:00:00.000Z',
})
;(updateExternalNotificationTarget as jest.Mock).mockResolvedValue({
id: 'target-1',
name: '自动上架平台-新版',
callbackUrl: 'https://auto.example.com/callback',
isActive: false,
authType: 'header',
authConfig: { headerName: 'X-Signature', headerValue: 'abc123' },
eventTypes: ['task-detail.full-set.ready'],
createdAt: '2026-03-25T10:00:00.000Z',
updatedAt: '2026-03-25T10:30:00.000Z',
})
const request = {
json: jest.fn().mockResolvedValue({
name: '自动上架平台-新版',
callbackUrl: 'https://auto.example.com/callback',
isActive: false,
authType: 'header',
authConfig: { headerName: 'X-Signature', headerValue: '' },
}),
} as any
const response = await updateTargetRoute(request, {
params: { id: 'target-1' },
} as any)
const body = await response.json()
expect(response.status).toBe(200)
expect(body.success).toBe(true)
expect(body.data.authConfig).toEqual({ headerName: 'X-Signature', headerValue: '' })
expect(JSON.stringify(body)).not.toContain('abc123')
expect(updateExternalNotificationTarget).toHaveBeenCalledWith('target-1', {
name: '自动上架平台-新版',
callbackUrl: 'https://auto.example.com/callback',
isActive: false,
authType: 'header',
authConfig: { headerName: 'X-Signature', headerValue: 'old-secret' },
eventTypes: undefined,
})
})
test('GET /api/admin/external-notification-deliveries 应按查询条件返回投递记录', async () => {
;(listExternalNotificationDeliveries as jest.Mock).mockResolvedValue([
{
id: 'delivery-1',
targetId: 'target-1',
targetName: '自动上架平台',
eventType: 'task-detail.full-set.ready',
materialId: 'material-1',
materialType: '任务-详情',
taskNo: null,
status: 'retrying',
attemptCount: 1,
maxAttempts: 5,
nextRetryAt: '2026-03-25T10:01:00.000Z',
lastStatusCode: 500,
lastResponseBody: 'server error',
lastError: 'HTTP 500',
payload: { eventId: 'evt-1' },
createdAt: '2026-03-25T10:00:00.000Z',
updatedAt: '2026-03-25T10:00:01.000Z',
completedAt: null,
},
])
const response = await getDeliveriesRoute(
new Request(
'http://localhost/api/admin/external-notification-deliveries?status=retrying&targetId=target-1&limit=20',
) as any,
)
const body = await response.json()
expect(response.status).toBe(200)
expect(body.success).toBe(true)
expect(listExternalNotificationDeliveries).toHaveBeenCalledWith({
status: 'retrying',
targetId: 'target-1',
materialId: undefined,
limit: 20,
})
})
test('POST /api/admin/external-notification-deliveries/[id]/retry 应触发手工补发', async () => {
;(retryExternalNotificationDelivery as jest.Mock).mockResolvedValue(undefined)
const response = await retryDeliveryRoute(new Request('http://localhost') as any, {
params: { id: 'delivery-1' },
} as any)
const body = await response.json()
expect(response.status).toBe(200)
expect(body.success).toBe(true)
expect(retryExternalNotificationDelivery).toHaveBeenCalledWith('delivery-1')
})
test('非真正超管时即使是 SYSTEM_ADMIN 也返回 403', async () => {
;(isSuperAdmin as jest.Mock).mockResolvedValue(false)
const targetsResponse = await getTargetsRoute(new Request('http://localhost/api/admin/external-notification-targets') as any)
const retryResponse = await retryDeliveryRoute(new Request('http://localhost') as any, {
params: { id: 'delivery-1' },
} as any)
expect(targetsResponse.status).toBe(403)
expect(retryResponse.status).toBe(403)
expect(listExternalNotificationTargets).not.toHaveBeenCalled()
expect(retryExternalNotificationDelivery).not.toHaveBeenCalled()
})
test('未登录时应拒绝访问', async () => {
;(getServerSession as jest.Mock).mockResolvedValue(null)
const response = await getTargetsRoute(new Request('http://localhost/api/admin/external-notification-targets') as any)
const body = await response.json()
expect(response.status).toBe(401)
expect(body.success).toBe(false)
})
})