mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-06-22 14:13:49 +08:00
fix(account): guard download record user races
This commit is contained in:
+13
-4
@@ -1249,9 +1249,16 @@ const onDetailRemove = (app: App) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onDetailInstall = async (app: App) => {
|
const onDetailInstall = async (app: App) => {
|
||||||
|
const initiatingUserId = currentUser.value?.id ?? null;
|
||||||
const download = await handleInstall(app);
|
const download = await handleInstall(app);
|
||||||
const initiatingUserId = currentUser.value?.id;
|
if (
|
||||||
if (!download || !isLoggedIn.value || initiatingUserId === undefined) return;
|
!download ||
|
||||||
|
initiatingUserId === null ||
|
||||||
|
!isLoggedIn.value ||
|
||||||
|
currentUser.value?.id !== initiatingUserId
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
pendingDownloadRecords.set(download.id, {
|
pendingDownloadRecords.set(download.id, {
|
||||||
userId: initiatingUserId,
|
userId: initiatingUserId,
|
||||||
@@ -1272,6 +1279,10 @@ const handleInstallCompleteForDownloadRecord = async (
|
|||||||
const pendingRecord = pendingDownloadRecords.get(result.id);
|
const pendingRecord = pendingDownloadRecords.get(result.id);
|
||||||
if (!pendingRecord) return;
|
if (!pendingRecord) return;
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
pendingDownloadRecords.delete(result.id);
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!result.success ||
|
!result.success ||
|
||||||
!isLoggedIn.value ||
|
!isLoggedIn.value ||
|
||||||
@@ -1294,8 +1305,6 @@ const handleInstallCompleteForDownloadRecord = async (
|
|||||||
await recordDownloadedApp(downloadRecord);
|
await recordDownloadedApp(downloadRecord);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
logger.warn({ err: error }, "记录下载应用失败");
|
logger.warn({ err: error }, "记录下载应用失败");
|
||||||
} finally {
|
|
||||||
pendingDownloadRecords.delete(result.id);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,46 @@ const invoke = vi.fn();
|
|||||||
const send = vi.fn();
|
const send = vi.fn();
|
||||||
const ipcHandlers = new Map<string, (...args: unknown[]) => void>();
|
const ipcHandlers = new Map<string, (...args: unknown[]) => 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 = <T>() => {
|
||||||
|
let resolve!: (value: T) => void;
|
||||||
|
const promise = new Promise<T>((promiseResolve) => {
|
||||||
|
resolve = promiseResolve;
|
||||||
|
});
|
||||||
|
return { promise, resolve };
|
||||||
|
};
|
||||||
|
|
||||||
vi.mock("axios", () => {
|
vi.mock("axios", () => {
|
||||||
const get = vi.fn(async (url: string) => {
|
const get = vi.fn(async (url: string) => {
|
||||||
if (url.includes("categories.json")) {
|
if (url.includes("categories.json")) {
|
||||||
@@ -124,19 +164,7 @@ describe("App download records", () => {
|
|||||||
});
|
});
|
||||||
window.apm_store.arch = "amd64";
|
window.apm_store.arch = "amd64";
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
setAuthSession({
|
setInitialUserSession();
|
||||||
accessToken: "backend-token",
|
|
||||||
tokenType: "bearer",
|
|
||||||
user: {
|
|
||||||
id: 1,
|
|
||||||
flarumUserId: "42",
|
|
||||||
username: "momen",
|
|
||||||
displayName: "Momen",
|
|
||||||
avatarUrl: "https://bbs.spark-app.store/avatar.png",
|
|
||||||
forumLevel: "管理员",
|
|
||||||
forumGroups: ["管理员"],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.stubGlobal(
|
vi.stubGlobal(
|
||||||
"matchMedia",
|
"matchMedia",
|
||||||
@@ -296,19 +324,7 @@ describe("App download records", () => {
|
|||||||
await fireEvent.click(screen.getByRole("button", { name: "Momen" }));
|
await fireEvent.click(screen.getByRole("button", { name: "Momen" }));
|
||||||
await fireEvent.click(await screen.findByText("退出登录"));
|
await fireEvent.click(await screen.findByText("退出登录"));
|
||||||
|
|
||||||
setAuthSession({
|
setSecondUserSession();
|
||||||
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 completion: DownloadResult = {
|
const completion: DownloadResult = {
|
||||||
id: queuedDownload.id,
|
id: queuedDownload.id,
|
||||||
@@ -326,4 +342,111 @@ describe("App download records", () => {
|
|||||||
expect(recordDownloadedApp).not.toHaveBeenCalled();
|
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<boolean>();
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user