/** @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) }) })