From 339df89fcf5552e2a01b6dff91f4aca53034e31f Mon Sep 17 00:00:00 2001 From: CieanYau Date: Sat, 7 Nov 2026 01:06:17 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=B7=A5=E5=8D=95=E6=9D=83?= =?UTF-8?q?=E9=99=90=E6=94=B6=E5=9B=9E=E5=90=8E=E4=B8=8A=E4=BC=A0=E9=98=9F?= =?UTF-8?q?=E5=88=97=E7=BB=A7=E7=BB=AD=E6=89=A7=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/pages/tickets/[id].vue | 1 + tests/review-routes.e2e.test.mjs | 99 ++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/app/pages/tickets/[id].vue b/app/pages/tickets/[id].vue index 1a51907..01f7038 100644 --- a/app/pages/tickets/[id].vue +++ b/app/pages/tickets/[id].vue @@ -203,6 +203,7 @@ var le = { try { for (let e of n) { let t = await z.upload(e); + if (!canOperate.value) return; if (!t?.save_name) throw Error(`附件「${e.name}」上传失败,请重新选择文件`); q.value.push({ diff --git a/tests/review-routes.e2e.test.mjs b/tests/review-routes.e2e.test.mjs index 40f4dc6..47ca7c9 100644 --- a/tests/review-routes.e2e.test.mjs +++ b/tests/review-routes.e2e.test.mjs @@ -435,3 +435,102 @@ for (const [routePath, status] of [ }), ); } + +test( + "in-flight ticket uploads stop the remaining files when server permission is revoked", + { timeout: 25000 }, + (t) => + scenario(t, { loginMode: "password-only" }, async (page) => { + const uploads = []; + const ticket = { + id: 2, + ticket_num: "AUDIT-2", + title: "上传期间关闭工单", + content: "

本机权限并发验收

", + status: "处理中", + can_operate: 1, + create_time: 1730000000, + username: "audit-user", + client_id: 1, + color: "#777", + attachment: [], + replies: [], + }; + let releaseFirstUpload; + const firstUploadResponse = new Promise((resolve) => { + releaseFirstUpload = resolve; + }); + await page.route( + (url) => + url.origin === backendOrigin && + (url.pathname.startsWith("/console/v1/ticket/") || + url.pathname === "/console/v1/upload"), + async (route) => { + const request = route.request(); + const path = new URL(request.url()).pathname.replace( + "/console/v1", + "", + ); + if (path === "/ticket/config") + return fulfillApi(route, { ticket_notice_open: 0 }); + if (request.method() === "GET" && path === "/ticket/2") + return fulfillApi(route, { ticket }); + if (request.method() === "PUT" && path === "/ticket/2/close") { + ticket.can_operate = 0; + ticket.status = "处理完毕"; + return fulfillApi(route, {}); + } + if (request.method() === "POST" && path === "/upload") { + const index = uploads.push({ method: "POST", path }); + if (index === 1) await firstUploadResponse; + return fulfillApi(route, { save_name: `attachment-${index}.txt` }); + } + return route.fallback(); + }, + ); + try { + await login(page, "/tickets/2"); + await page + .getByRole("heading", { name: ticket.title, exact: true }) + .waitFor(); + await page.locator("#reply-files").setInputFiles( + ["first.txt", "second.txt"].map((name) => ({ + name, + mimeType: "text/plain", + buffer: Buffer.from("local permission regression"), + })), + ); + await eventually( + () => uploads.length, + (count) => count === 1, + "First upload reached the local HTTP boundary", + ); + await page + .getByRole("button", { name: "关闭工单", exact: true }) + .click(); + await page + .getByRole("button", { name: "关闭工单", exact: true }) + .waitFor({ state: "hidden" }); + releaseFirstUpload(); + await page + .getByText("正在上传…", { exact: true }) + .waitFor({ state: "hidden" }); + assert.equal( + uploads.length, + 1, + "Revoked permission must stop the second upload", + ); + assert.equal(await page.locator(".reply-file-list").count(), 0); + return { + selectedFiles: 2, + uploadsStarted: uploads.length, + serverCanOperate: ticket.can_operate, + alreadyStartedUploadCompleted: true, + remainingUploadsStopped: true, + revokedUploadNotAddedToDraft: true, + }; + } finally { + releaseFirstUpload(); + } + }), +);