290 lines
10 KiB
TypeScript
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)
|
|
})
|
|
})
|