feat: add notification project deletion
This commit is contained in:
parent
b917c09402
commit
15b4501ffa
@ -1,6 +1,7 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
import {
|
||||
deleteNotificationCenterProject,
|
||||
getNotificationCenterProject,
|
||||
updateNotificationCenterProject,
|
||||
} from '@/lib/notification-center-admin'
|
||||
@ -70,3 +71,23 @@ export async function PUT(
|
||||
) {
|
||||
return handlePatch(request, context)
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: { code: string } },
|
||||
) {
|
||||
try {
|
||||
const auth = await requireNotificationCenterSuperAdmin()
|
||||
if ('response' in auth) return auth.response
|
||||
|
||||
const code = readProjectCode(params)
|
||||
if (!code) {
|
||||
return NextResponse.json({ success: false, error: '通知中心项目不存在' }, { status: 404 })
|
||||
}
|
||||
|
||||
const result = await deleteNotificationCenterProject(auth.actor, code)
|
||||
return NextResponse.json({ success: true, data: result.data }, { status: result.status })
|
||||
} catch (error) {
|
||||
return toNotificationCenterAdminErrorResponse(error)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import NotificationCenterPanel from './NotificationCenterPanel'
|
||||
|
||||
@ -35,10 +35,14 @@ const jsonResponse = (body: unknown, status = 200) =>
|
||||
describe('NotificationCenterPanel', () => {
|
||||
const showToast = jest.fn()
|
||||
let clipboardText = ''
|
||||
let projectList = [project]
|
||||
let deleteShouldFail = false
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
clipboardText = ''
|
||||
projectList = [{ ...project }]
|
||||
deleteShouldFail = false
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
configurable: true,
|
||||
value: {
|
||||
@ -51,7 +55,7 @@ describe('NotificationCenterPanel', () => {
|
||||
global.fetch = jest.fn((input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input)
|
||||
if (url.endsWith('/api/admin/notification-center/projects') && (!init?.method || init.method === 'GET')) {
|
||||
return jsonResponse({ success: true, data: { projects: [project] } })
|
||||
return jsonResponse({ success: true, data: { projects: projectList } })
|
||||
}
|
||||
if (url.endsWith('/api/admin/notification-center/projects') && init?.method === 'POST') {
|
||||
return jsonResponse({
|
||||
@ -90,6 +94,13 @@ describe('NotificationCenterPanel', () => {
|
||||
},
|
||||
})
|
||||
}
|
||||
if (url.endsWith('/api/admin/notification-center/projects/material_library') && init?.method === 'DELETE') {
|
||||
if (deleteShouldFail) {
|
||||
return jsonResponse({ error: { message: '通知中心服务异常,请稍后重试' } }, 502)
|
||||
}
|
||||
projectList = []
|
||||
return jsonResponse({ success: true, data: { projectCode: 'material_library', deleted: true } })
|
||||
}
|
||||
if (url.endsWith('/test') && init?.method === 'POST') {
|
||||
return jsonResponse({ deliveryId: 'delivery-1', status: 'pending' }, 202)
|
||||
}
|
||||
@ -165,6 +176,54 @@ describe('NotificationCenterPanel', () => {
|
||||
expect(screen.getByRole('button', { name: '查看当前密钥' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('删除项目需要单独确认,成功后刷新列表并关闭抽屉', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<NotificationCenterPanel showToast={showToast} />)
|
||||
await screen.findByText('素材平台')
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '查看配置并编辑资料 素材平台' }))
|
||||
await user.click(screen.getByRole('button', { name: '删除项目' }))
|
||||
|
||||
const dialog = await screen.findByRole('alertdialog', { name: '确认删除“素材平台”项目?' })
|
||||
expect(within(dialog).getByText('删除项目配置并使密钥立即失效,历史投递记录保留。')).toBeInTheDocument()
|
||||
expect(
|
||||
within(dialog).getByText('如仍需继续发送通知,请在删除后重新创建项目并同步更新调用方环境变量。'),
|
||||
).toBeInTheDocument()
|
||||
|
||||
await user.click(within(dialog).getByRole('button', { name: '确认删除项目' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(global.fetch).toHaveBeenCalledWith(
|
||||
'/api/admin/notification-center/projects/material_library',
|
||||
expect.objectContaining({ method: 'DELETE' }),
|
||||
)
|
||||
})
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('heading', { name: '查看配置 / 编辑资料' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
expect(await screen.findByText('还没有接入项目')).toBeInTheDocument()
|
||||
expect(showToast).toHaveBeenCalledWith('接入项目已删除,项目密钥已立即失效', 'success')
|
||||
})
|
||||
|
||||
test('删除项目失败时保留确认弹窗并提示错误 toast', async () => {
|
||||
deleteShouldFail = true
|
||||
const user = userEvent.setup()
|
||||
render(<NotificationCenterPanel showToast={showToast} />)
|
||||
await screen.findByText('素材平台')
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '查看配置并编辑资料 素材平台' }))
|
||||
await user.click(screen.getByRole('button', { name: '删除项目' }))
|
||||
|
||||
const dialog = await screen.findByRole('alertdialog', { name: '确认删除“素材平台”项目?' })
|
||||
await user.click(within(dialog).getByRole('button', { name: '确认删除项目' }))
|
||||
|
||||
expect(await within(dialog).findByRole('alert')).toHaveTextContent('通知中心服务异常,请稍后重试')
|
||||
expect(showToast).toHaveBeenCalledWith('通知中心服务异常,请稍后重试', 'error')
|
||||
expect(screen.getByRole('alertdialog', { name: '确认删除“素材平台”项目?' })).toBeInTheDocument()
|
||||
expect(screen.queryByText('还没有接入项目')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('已接入项目可查看资料并按需查看、复制和隐藏当前密钥', async () => {
|
||||
const user = userEvent.setup()
|
||||
render(<NotificationCenterPanel showToast={showToast} />)
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useGSAP } from '@gsap/react'
|
||||
import gsap from 'gsap'
|
||||
import { useConfirmActionDialog } from '@/components/ConfirmActionDialog'
|
||||
import {
|
||||
Activity,
|
||||
AlertTriangle,
|
||||
@ -16,6 +17,7 @@ import {
|
||||
RefreshCw,
|
||||
Send,
|
||||
Settings2,
|
||||
Trash2,
|
||||
X,
|
||||
} from 'lucide-react'
|
||||
|
||||
@ -224,6 +226,7 @@ function useDialogKeyboard(open: boolean, close: () => void) {
|
||||
export default function NotificationCenterPanel({ showToast }: NotificationCenterPanelProps) {
|
||||
const rootRef = useRef<HTMLDivElement>(null)
|
||||
const revealRequestRef = useRef(0)
|
||||
const { confirmAction, confirmActionDialog } = useConfirmActionDialog()
|
||||
const [activeView, setActiveView] = useState<PanelView>('projects')
|
||||
const [projects, setProjects] = useState<NotificationProject[]>([])
|
||||
const [projectsLoading, setProjectsLoading] = useState(true)
|
||||
@ -551,6 +554,40 @@ export default function NotificationCenterPanel({ showToast }: NotificationCente
|
||||
}
|
||||
}
|
||||
|
||||
const confirmDeleteProject = async (project: NotificationProject) => {
|
||||
await confirmAction({
|
||||
title: `确认删除“${project.name}”项目?`,
|
||||
description: (
|
||||
<>
|
||||
<p>删除项目配置并使密钥立即失效,历史投递记录保留。</p>
|
||||
<p className="mt-2">如仍需继续发送通知,请在删除后重新创建项目并同步更新调用方环境变量。</p>
|
||||
</>
|
||||
),
|
||||
confirmLabel: '确认删除项目',
|
||||
loadingLabel: '删除中…',
|
||||
tone: 'danger',
|
||||
action: async () => {
|
||||
try {
|
||||
await readJsonResponse(
|
||||
await fetch(
|
||||
`/api/admin/notification-center/projects/${encodeURIComponent(project.projectCode)}`,
|
||||
{ method: 'DELETE' },
|
||||
),
|
||||
)
|
||||
closeDrawer()
|
||||
setProjects((current) => current.filter((item) => item.projectCode !== project.projectCode))
|
||||
setTestProjectCode((current) => (current === project.projectCode ? '' : current))
|
||||
await loadProjects()
|
||||
showToast('接入项目已删除,项目密钥已立即失效', 'success')
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '接入项目删除失败'
|
||||
showToast(message, 'error')
|
||||
throw error instanceof Error ? error : new Error(message)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const preparedTestRecipient = useMemo(() => {
|
||||
if (!selectedTestProject || !testRecipient.trim()) return null
|
||||
if (testRecipientMode === 'userId') {
|
||||
@ -929,18 +966,38 @@ export default function NotificationCenterPanel({ showToast }: NotificationCente
|
||||
</button>
|
||||
)}
|
||||
<div className="mt-4 border-t border-[#e2e8f0] pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const project = projects.find((item) => item.projectCode === editingCode)
|
||||
if (project) setRotateProject(project)
|
||||
}}
|
||||
className="inline-flex min-h-10 items-center justify-center gap-2 rounded-[8px] border border-[#f3c7c1] px-3 text-[13px] font-medium text-[#b42318] hover:bg-[#fef6f5] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1677ff]"
|
||||
>
|
||||
<KeyRound className="h-4 w-4" aria-hidden="true" />
|
||||
轮换项目密钥
|
||||
</button>
|
||||
<p className="mt-1 text-[11px] leading-5 text-[#8f2119]">轮换会使旧密钥立即失效,仅在确实需要更换时使用。</p>
|
||||
<p className="text-[13px] font-semibold text-[#8f2119]">危险操作</p>
|
||||
<p className="mt-1 text-[11px] leading-5 text-[#8f2119]">
|
||||
轮换会使旧密钥立即失效;删除会移除项目配置,但历史投递记录仍会保留。
|
||||
</p>
|
||||
<div className="mt-3 flex flex-col gap-2 sm:flex-row sm:flex-wrap">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const project = projects.find((item) => item.projectCode === editingCode)
|
||||
if (project) setRotateProject(project)
|
||||
}}
|
||||
className="inline-flex min-h-10 items-center justify-center gap-2 rounded-[8px] border border-[#f3c7c1] px-3 text-[13px] font-medium text-[#b42318] hover:bg-[#fef6f5] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1677ff]"
|
||||
>
|
||||
<KeyRound className="h-4 w-4" aria-hidden="true" />
|
||||
轮换项目密钥
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const project = projects.find((item) => item.projectCode === editingCode)
|
||||
if (!project) {
|
||||
showToast('当前项目不存在,请先刷新项目列表', 'error')
|
||||
return
|
||||
}
|
||||
void confirmDeleteProject(project)
|
||||
}}
|
||||
className="inline-flex min-h-10 items-center justify-center gap-2 rounded-[8px] border border-[#f3c7c1] px-3 text-[13px] font-medium text-[#b42318] hover:bg-[#fef6f5] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1677ff]"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" aria-hidden="true" />
|
||||
删除项目
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -955,6 +1012,7 @@ export default function NotificationCenterPanel({ showToast }: NotificationCente
|
||||
{confirmTestOpen && preparedTestRecipient && selectedTestProject && <div className="fixed inset-0 z-[80] flex items-center justify-center bg-[#06192d]/40 px-4"><section role="alertdialog" aria-modal="true" aria-labelledby="confirm-test-title" className="w-full max-w-lg rounded-[12px] bg-white p-5 shadow-[0_18px_48px_rgba(6,25,45,0.2)]"><div className="flex h-10 w-10 items-center justify-center rounded-[8px] bg-[#e6f4ff] text-[#1677ff]"><Send className="h-5 w-5" aria-hidden="true" /></div><h3 id="confirm-test-title" className="mt-4 text-[17px] font-semibold text-[#101828]">确认发送真实测试通知</h3><dl className="mt-4 grid gap-3 rounded-[8px] bg-[#f8fafc] p-4 text-[13px]"><div><dt className="text-[#66758b]">项目</dt><dd className="mt-1 font-medium text-[#101828]">{selectedTestProject.name} · {selectedTestProject.projectCode}</dd></div><div><dt className="text-[#66758b]">真实收件人</dt><dd className="mt-1 font-medium text-[#101828]">{preparedTestRecipient.label}</dd></div><div><dt className="text-[#66758b]">标题</dt><dd className="mt-1 font-medium text-[#101828]">{testTitle}</dd></div></dl><p className="mt-3 text-[13px] leading-6 text-[#9a4d08]">通知中心会统一在标题前加上项目名称;确认后会真实调用钉钉,不是页面预览。</p><div className="mt-5 flex justify-end gap-2"><button type="button" onClick={() => setConfirmTestOpen(false)} className="min-h-10 rounded-[8px] border border-[#cfd8e3] px-4 text-[14px] font-medium text-[#344054]">取消</button><button type="button" onClick={() => void sendTestNotification()} disabled={sendingTest} className="min-h-10 rounded-[8px] bg-[#1677ff] px-4 text-[14px] font-semibold text-white disabled:opacity-50">{sendingTest ? '正在提交…' : '确认发送给该员工'}</button></div></section></div>}
|
||||
|
||||
{oneTimeSecret && <div className="fixed inset-0 z-[90] flex items-center justify-center bg-[#06192d]/45 px-4"><section data-secret-reveal role="dialog" aria-modal="true" aria-labelledby="one-time-secret-title" className="max-h-[90dvh] w-full max-w-2xl overflow-y-auto rounded-[12px] bg-white p-5 shadow-[0_18px_48px_rgba(6,25,45,0.22)]"><div className="flex h-10 w-10 items-center justify-center rounded-[8px] bg-[#e8f7f2] text-[#087052]"><Check className="h-5 w-5" aria-hidden="true" /></div><h3 id="one-time-secret-title" className="mt-4 text-[18px] font-semibold text-[#101828]">{oneTimeSecret.reason === 'created' ? '接入项目已创建' : '项目密钥已轮换'}</h3><p className="mt-2 text-[13px] leading-6 text-[#66758b]">请把下面的 URL、projectCode 和项目密钥保存到调用方后端的安全环境变量中。关闭后仍可在“查看配置 / 编辑资料”里按需查看当前密钥。</p><div className="mt-5 grid gap-4 sm:grid-cols-2"><div className="sm:col-span-2"><span className="text-[13px] font-semibold text-[#344054]">统一通知中心 URL</span><code className="mt-2 block break-all rounded-[8px] border border-[#d9e0e7] bg-[#f8fafc] p-3 text-[13px] leading-6 text-[#101828]">{oneTimeSecret.env.YYD_NOTIFICATION_CENTER_URL || '后端未返回'}</code></div><div><span className="text-[13px] font-semibold text-[#344054]">projectCode</span><code className="mt-2 block break-all rounded-[8px] border border-[#d9e0e7] bg-[#f8fafc] p-3 text-[13px] leading-6 text-[#101828]">{oneTimeSecret.projectCode || oneTimeSecret.env.YYD_NOTIFICATION_CENTER_PROJECT_CODE || '后端未返回'}</code></div><div><div className="flex items-center justify-between"><span className="text-[13px] font-semibold text-[#344054]">项目密钥</span><button type="button" onClick={() => void copyText(oneTimeSecret.secret, '项目密钥')} className="inline-flex min-h-10 items-center gap-2 rounded-[8px] px-3 text-[13px] font-medium text-[#1677ff] hover:bg-[#f5f9ff]"><Clipboard className="h-4 w-4" aria-hidden="true" />复制</button></div><code className="mt-2 block break-all rounded-[8px] border border-[#d9e0e7] bg-[#f8fafc] p-3 text-[13px] leading-6 text-[#101828]">{oneTimeSecret.secret}</code></div></div><div className="mt-5"><div className="flex items-center justify-between"><span className="text-[13px] font-semibold text-[#344054]">调用方环境变量</span><button type="button" onClick={() => void copyText(Object.entries(oneTimeSecret.env).map(([key, value]) => `${key}=${value}`).join('\n'), '环境变量')} className="inline-flex min-h-10 items-center gap-2 rounded-[8px] px-3 text-[13px] font-medium text-[#1677ff] hover:bg-[#f5f9ff]"><Clipboard className="h-4 w-4" aria-hidden="true" />复制全部</button></div><pre className="mt-2 overflow-x-auto rounded-[8px] border border-[#d9e0e7] bg-[#06192d] p-3 text-[12px] leading-6 text-[#eaf1f8]">{Object.entries(oneTimeSecret.env).map(([key, value]) => `${key}=${value}`).join('\n')}</pre></div><div className="mt-5 flex justify-end"><button type="button" onClick={() => setOneTimeSecret(null)} className="min-h-10 rounded-[8px] bg-[#1677ff] px-4 text-[14px] font-semibold text-white">关闭</button></div></section></div>}
|
||||
{confirmActionDialog}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import { getServerSession } from 'next-auth'
|
||||
import { isSuperAdmin } from '@/lib/department-permission'
|
||||
import {
|
||||
createNotificationCenterProject,
|
||||
deleteNotificationCenterProject,
|
||||
getNotificationCenterProject,
|
||||
listNotificationCenterAudits,
|
||||
listNotificationCenterDeliveries,
|
||||
@ -18,7 +19,7 @@ import {
|
||||
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 { DELETE as deleteProjectRoute, 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 { POST as revealProjectSecretRoute } from '@/app/api/admin/notification-center/projects/[code]/reveal-secret/route'
|
||||
import { GET as getProjectRecipientsRoute } from '@/app/api/admin/notification-center/projects/[code]/recipients/route'
|
||||
@ -57,6 +58,7 @@ jest.mock('@/lib/notification-center-admin', () => ({
|
||||
createNotificationCenterProject: jest.fn(),
|
||||
getNotificationCenterProject: jest.fn(),
|
||||
updateNotificationCenterProject: jest.fn(),
|
||||
deleteNotificationCenterProject: jest.fn(),
|
||||
rotateNotificationCenterProjectSecret: jest.fn(),
|
||||
listNotificationCenterProjectRecipients: jest.fn(),
|
||||
sendNotificationCenterProjectTest: jest.fn(),
|
||||
@ -136,7 +138,7 @@ describe('notification-center admin routes', () => {
|
||||
expect(body.data.env).toContain('YYD_NOTIFICATION_CENTER_PROJECT_SECRET')
|
||||
})
|
||||
|
||||
test('GET /projects/[code] 与 PATCH /projects/[code] 可用', async () => {
|
||||
test('GET /projects/[code]、PATCH /projects/[code] 与 DELETE /projects/[code] 可用', async () => {
|
||||
;(getNotificationCenterProject as jest.Mock).mockResolvedValue({
|
||||
status: 200,
|
||||
data: { code: 'material_library', name: '素材库' },
|
||||
@ -145,17 +147,31 @@ describe('notification-center admin routes', () => {
|
||||
status: 200,
|
||||
data: { code: 'material_library', name: '素材库-新版' },
|
||||
})
|
||||
;(deleteNotificationCenterProject as jest.Mock).mockResolvedValue({
|
||||
status: 200,
|
||||
data: { code: 'material_library', deleted: true },
|
||||
})
|
||||
|
||||
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,
|
||||
)
|
||||
const deleteResponse = await deleteProjectRoute(
|
||||
new Request('http://localhost') 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('素材库-新版')
|
||||
expect(deleteResponse.status).toBe(200)
|
||||
expect((await deleteResponse.json()).data.deleted).toBe(true)
|
||||
expect(deleteNotificationCenterProject).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ userId: 'admin-1' }),
|
||||
'material_library',
|
||||
)
|
||||
})
|
||||
|
||||
test('POST /projects/[code]/rotate-secret 透传一次性 secret/env', async () => {
|
||||
|
||||
@ -10,6 +10,7 @@ jest.mock('@/lib/prisma', () => ({
|
||||
|
||||
import {
|
||||
createNotificationCenterProject,
|
||||
deleteNotificationCenterProject,
|
||||
listNotificationCenterProjectRecipients,
|
||||
listNotificationCenterProjects,
|
||||
NotificationCenterAdminError,
|
||||
@ -181,7 +182,7 @@ describe('notification-center-admin client', () => {
|
||||
expect(JSON.stringify(result.data)).not.toContain('ding-private-user')
|
||||
})
|
||||
|
||||
test('PATCH 项目更新与项目级 recipients/test 会命中嵌套路径', async () => {
|
||||
test('PATCH/DELETE 项目更新与项目级 recipients/test 会命中嵌套路径', async () => {
|
||||
;(global.fetch as jest.Mock)
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
@ -198,6 +199,11 @@ describe('notification-center-admin client', () => {
|
||||
status: 200,
|
||||
text: async () => JSON.stringify({ id: 'test-1' }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () => JSON.stringify({ deleted: true }),
|
||||
})
|
||||
|
||||
await updateNotificationCenterProject(actor, 'material_library', { name: '素材库-新版' })
|
||||
await listNotificationCenterProjectRecipients(
|
||||
@ -209,6 +215,7 @@ describe('notification-center-admin client', () => {
|
||||
title: '连通性测试',
|
||||
projectCode: 'material_library',
|
||||
})
|
||||
await deleteNotificationCenterProject(actor, 'material_library')
|
||||
|
||||
expect((global.fetch as jest.Mock).mock.calls[0][0]).toBe(
|
||||
'https://notify-admin.example.com/projects/material_library',
|
||||
@ -226,6 +233,12 @@ describe('notification-center-admin client', () => {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ title: '连通性测试' }),
|
||||
})
|
||||
expect((global.fetch as jest.Mock).mock.calls[3][0]).toBe(
|
||||
'https://notify-admin.example.com/projects/material_library',
|
||||
)
|
||||
expect((global.fetch as jest.Mock).mock.calls[3][1]).toMatchObject({
|
||||
method: 'DELETE',
|
||||
})
|
||||
})
|
||||
|
||||
test('下游 401 会转成中文配置错误且不泄漏管理员密钥', async () => {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { prisma } from '@/lib/prisma'
|
||||
|
||||
type NotificationCenterAdminMethod = 'GET' | 'POST' | 'PUT' | 'PATCH'
|
||||
type NotificationCenterAdminMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
||||
|
||||
export type NotificationCenterAdminActor = {
|
||||
userId: string
|
||||
@ -406,6 +406,17 @@ export async function updateNotificationCenterProject(
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteNotificationCenterProject(
|
||||
actor: NotificationCenterAdminActor,
|
||||
code: string,
|
||||
) {
|
||||
return requestNotificationCenterAdmin({
|
||||
method: 'DELETE',
|
||||
path: buildProjectScopedPath(code),
|
||||
actor,
|
||||
})
|
||||
}
|
||||
|
||||
export async function rotateNotificationCenterProjectSecret(
|
||||
actor: NotificationCenterAdminActor,
|
||||
code: string,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user