diff --git a/src/App.vue b/src/App.vue index 60867b38..ff021ac3 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1249,9 +1249,16 @@ const onDetailRemove = (app: App) => { }; const onDetailInstall = async (app: App) => { + const initiatingUserId = currentUser.value?.id ?? null; const download = await handleInstall(app); - const initiatingUserId = currentUser.value?.id; - if (!download || !isLoggedIn.value || initiatingUserId === undefined) return; + if ( + !download || + initiatingUserId === null || + !isLoggedIn.value || + currentUser.value?.id !== initiatingUserId + ) { + return; + } pendingDownloadRecords.set(download.id, { userId: initiatingUserId, @@ -1272,6 +1279,10 @@ const handleInstallCompleteForDownloadRecord = async ( const pendingRecord = pendingDownloadRecords.get(result.id); if (!pendingRecord) return; + if (result.success) { + pendingDownloadRecords.delete(result.id); + } + if ( !result.success || !isLoggedIn.value || @@ -1294,8 +1305,6 @@ const handleInstallCompleteForDownloadRecord = async ( await recordDownloadedApp(downloadRecord); } catch (error: unknown) { logger.warn({ err: error }, "记录下载应用失败"); - } finally { - pendingDownloadRecords.delete(result.id); } }; diff --git a/src/__tests__/unit/App.download-records.test.ts b/src/__tests__/unit/App.download-records.test.ts index 726aa1f3..00756522 100644 --- a/src/__tests__/unit/App.download-records.test.ts +++ b/src/__tests__/unit/App.download-records.test.ts @@ -17,6 +17,46 @@ const invoke = vi.fn(); const send = vi.fn(); const ipcHandlers = new Map void>(); +const setSecondUserSession = () => { + setAuthSession({ + accessToken: "backend-token-b", + tokenType: "bearer", + user: { + id: 2, + flarumUserId: "84", + username: "second", + displayName: "Second User", + avatarUrl: "https://bbs.spark-app.store/avatar-b.png", + forumLevel: "用户", + forumGroups: ["用户"], + }, + }); +}; + +const setInitialUserSession = () => { + setAuthSession({ + accessToken: "backend-token", + tokenType: "bearer", + user: { + id: 1, + flarumUserId: "42", + username: "momen", + displayName: "Momen", + avatarUrl: "https://bbs.spark-app.store/avatar.png", + forumLevel: "管理员", + forumGroups: ["管理员"], + }, + }); +}; + +const createControlledPromise = () => { + let resolve!: (value: T) => void; + const promise = new Promise((promiseResolve) => { + resolve = promiseResolve; + }); + return { promise, resolve }; +}; + vi.mock("axios", () => { const get = vi.fn(async (url: string) => { if (url.includes("categories.json")) { @@ -124,19 +164,7 @@ describe("App download records", () => { }); window.apm_store.arch = "amd64"; localStorage.clear(); - setAuthSession({ - accessToken: "backend-token", - tokenType: "bearer", - user: { - id: 1, - flarumUserId: "42", - username: "momen", - displayName: "Momen", - avatarUrl: "https://bbs.spark-app.store/avatar.png", - forumLevel: "管理员", - forumGroups: ["管理员"], - }, - }); + setInitialUserSession(); vi.stubGlobal( "matchMedia", @@ -296,19 +324,7 @@ describe("App download records", () => { await fireEvent.click(screen.getByRole("button", { name: "Momen" })); await fireEvent.click(await screen.findByText("退出登录")); - setAuthSession({ - accessToken: "backend-token-b", - tokenType: "bearer", - user: { - id: 2, - flarumUserId: "84", - username: "second", - displayName: "Second User", - avatarUrl: "https://bbs.spark-app.store/avatar-b.png", - forumLevel: "用户", - forumGroups: ["用户"], - }, - }); + setSecondUserSession(); const completion: DownloadResult = { id: queuedDownload.id, @@ -326,4 +342,111 @@ describe("App download records", () => { expect(recordDownloadedApp).not.toHaveBeenCalled(); }); }); + + it("does not bind a queued install to a user who logged in during the APM availability check", async () => { + const apmCheck = createControlledPromise(); + let apmCheckCalls = 0; + invoke.mockImplementation(async (channel: string) => { + if (channel === "get-store-filter") return "apm"; + if (channel === "check-spark-available") return false; + if (channel === "check-apm-available") { + apmCheckCalls += 1; + return apmCheckCalls === 1 ? true : apmCheck.promise; + } + if (channel === "get-app-version") return "5.0.0"; + if (channel === "get-system-info") return { distro: "deepin 25" }; + if (channel === "list-installed") return { success: true, apps: [] }; + if (channel === "check-installed") return false; + return []; + }); + render(App); + + await fireEvent.click( + await screen.findByRole("button", { name: "全部应用 1" }), + ); + await fireEvent.click(await screen.findByText("WPS")); + await fireEvent.click(await screen.findByRole("button", { name: "安装" })); + + await waitFor(() => { + expect(apmCheckCalls).toBe(2); + }); + setSecondUserSession(); + apmCheck.resolve(true); + + await waitFor(() => { + expect(send).toHaveBeenCalledWith( + "queue-install", + expect.stringContaining('"pkgname":"wps"'), + ); + }); + + const queuedPayload = vi + .mocked(send) + .mock.calls.find( + ([channel]) => channel === "queue-install", + )?.[1] as string; + const queuedDownload = JSON.parse(queuedPayload) as { id: number }; + const completion: DownloadResult = { + id: queuedDownload.id, + time: Date.now(), + message: "installed", + success: true, + exitCode: 0, + status: "completed", + origin: "apm", + }; + + ipcHandlers.get("install-complete")?.({}, completion); + + await waitFor(() => { + expect(recordDownloadedApp).not.toHaveBeenCalled(); + }); + }); + + it("cleans up a successful pending record even when the active user does not match", async () => { + render(App); + + await fireEvent.click( + await screen.findByRole("button", { name: "全部应用 1" }), + ); + await fireEvent.click(await screen.findByText("WPS")); + await fireEvent.click(await screen.findByRole("button", { name: "安装" })); + + await waitFor(() => { + expect(send).toHaveBeenCalledWith( + "queue-install", + expect.stringContaining('"pkgname":"wps"'), + ); + }); + + const queuedPayload = vi + .mocked(send) + .mock.calls.find( + ([channel]) => channel === "queue-install", + )?.[1] as string; + const queuedDownload = JSON.parse(queuedPayload) as { id: number }; + const completion: DownloadResult = { + id: queuedDownload.id, + time: Date.now(), + message: "installed", + success: true, + exitCode: 0, + status: "completed", + origin: "apm", + }; + + setSecondUserSession(); + ipcHandlers.get("install-complete")?.({}, completion); + + await waitFor(() => { + expect(recordDownloadedApp).not.toHaveBeenCalled(); + }); + + setInitialUserSession(); + ipcHandlers.get("install-complete")?.({}, completion); + + await waitFor(() => { + expect(recordDownloadedApp).not.toHaveBeenCalled(); + }); + }); });