diff --git a/typescript/bot/src/utils/api-requests.ts b/typescript/bot/src/utils/api-requests.ts index 7cf4449ab..6c374358c 100644 --- a/typescript/bot/src/utils/api-requests.ts +++ b/typescript/bot/src/utils/api-requests.ts @@ -126,7 +126,7 @@ export async function PerformScoreImport( authToken, ); - if (pollRes.success) { + if (pollRes.success && pollRes.statusCode < 400) { if (pollRes.body.importStatus === "completed") { // is there even a nice way around this -- // why *are* we nested so deeply? @@ -138,15 +138,27 @@ export async function PerformScoreImport( return pollRes.body.import; } - if (interaction) { - void interaction.editReply( - `Importing Scores: ${pollRes.body.progress.description}..`, - ); - } + if (pollRes.body.importStatus === "ongoing") { + const progress = pollRes.body.progress; + const description = + progress && + typeof progress === "object" && + "description" in progress && + typeof progress.description === "string" + ? progress.description + : "Importing."; - // eslint-disable-next-line no-await-in-loop - await Sleep(1000); - } else { + if (interaction) { + void interaction.editReply(`Importing Scores: ${description}..`); + } + + // eslint-disable-next-line no-await-in-loop + await Sleep(1000); + continue; + } + } + + if (!pollRes.success || pollRes.statusCode >= 400) { // silly kai bug they won't ever fix. hacking around it in the bot here. if (/attempting reauthentication/u.exec(pollRes.description)) { throw new Error(`Failed to import scores. @@ -157,6 +169,10 @@ Please go to ${Env.TACHI_SERVER_LOCATION}/u/me/integrations/services to un-link throw new Error(`Failed to import scores. ${pollRes.description}.`); } + + throw new Error( + `Failed to import scores. ${pollRes.description ?? "Unexpected poll response."}.`, + ); } } diff --git a/typescript/client/src/components/imports/ImportStateRenderer.tsx b/typescript/client/src/components/imports/ImportStateRenderer.tsx index 3acbdce0c..9ec627a35 100644 --- a/typescript/client/src/components/imports/ImportStateRenderer.tsx +++ b/typescript/client/src/components/imports/ImportStateRenderer.tsx @@ -30,7 +30,7 @@ export default function ImportStateRenderer({ state: s }: { state: ImportStates have, this might take a while. -
{s.progressInfo.description ?? "Importing."}..
+
{s.progressInfo?.description ?? "Importing."}..
) : s.state === "done" ? ( <> diff --git a/typescript/client/src/components/util/import/useImport.ts b/typescript/client/src/components/util/import/useImport.ts index 28d44b112..01889a84b 100644 --- a/typescript/client/src/components/util/import/useImport.ts +++ b/typescript/client/src/components/util/import/useImport.ts @@ -64,27 +64,44 @@ export default function useImport(url: string, options: RequestInit) { `/imports/${initRes.body.importID}/poll-status`, ); - if (pollRes.success) { - if (pollRes.body.importStatus === "completed") { - isImportFinished = true; - if (importState.state === "done") { - setImportState({ ...importState, import: pollRes.body.import }); - } else { - setImportState({ state: "done", import: pollRes.body.import }); - } - } else { - setImportState({ - state: "waiting_processing", - progressInfo: pollRes.body.progress, - }); - - await Sleep(1000); - } - } else { + if (!pollRes.success || pollRes.statusCode >= 400) { setImportState({ state: "failed", error: pollRes.description }); isImportFinished = true; + continue; + } + + if (pollRes.body.importStatus === "completed") { + isImportFinished = true; + setImportState({ state: "done", import: pollRes.body.import }); + } else if (pollRes.body.importStatus === "ongoing") { + const progress = pollRes.body.progress; + const description = + progress && + typeof progress === "object" && + "description" in progress && + typeof progress.description === "string" + ? progress.description + : "Importing."; + + setImportState({ + state: "waiting_processing", + progressInfo: { description }, + }); + + await Sleep(1000); + } else { + setImportState({ + state: "failed", + error: pollRes.description ?? "Import failed.", + }); + isImportFinished = true; } } + } else { + setImportState({ + state: "failed", + error: initRes.description ?? "Import failed.", + }); } }; diff --git a/typescript/server/src/server/router/api/v1/imports/router.test.ts b/typescript/server/src/server/router/api/v1/imports/router.test.ts index 97fdbef50..64dc5bcaa 100644 --- a/typescript/server/src/server/router/api/v1/imports/router.test.ts +++ b/typescript/server/src/server/router/api/v1/imports/router.test.ts @@ -285,4 +285,25 @@ describe("GET /api/v1/imports/:importID/poll-status", () => { expect(res.body.body.importStatus).toBe("completed"); expect(res.body.body.import.importID).toBe(FakeImport.importID); }); + + it("returns 400 with success false when import_tracker records a failure", async () => { + const { id: userID } = await seedUser({ username: "poll_failed_user" }); + + await DB.insertInto("import_tracker") + .values({ + import_id: "failed-import", + user_id: userID, + import_type: "file/batch-manual", + user_intent: true, + time_started: new Date().toISOString(), + error: { message: "Invalid score file.", statusCode: 400 }, + }) + .execute(); + + const res = await mockApi.get("/api/v1/imports/failed-import/poll-status"); + + expect(res.status).toBe(400); + expect(res.body.success).toBe(false); + expect(res.body.description).toBe("Invalid score file."); + }); }); diff --git a/typescript/server/src/server/router/api/v1/imports/router.ts b/typescript/server/src/server/router/api/v1/imports/router.ts index e55e1eb93..a0859c775 100644 --- a/typescript/server/src/server/router/api/v1/imports/router.ts +++ b/typescript/server/src/server/router/api/v1/imports/router.ts @@ -210,15 +210,16 @@ API_V1_ROUTER.add("GET /imports/:importID/poll-status", async ({ params }) => { switch (tracker.type) { case "ONGOING": - return success("Import is ongoing.", { importStatus: "ongoing", progress: 0 }); + return success("Import is ongoing.", { + importStatus: "ongoing", + progress: { description: "Importing scores." }, + }); case "FAILED": - return { - $status: tracker.error.statusCode ?? 500, - body: {}, - description: tracker.error.message, - success: true as const, - }; + throw new ExpectedErr( + tracker.error.statusCode ?? 500, + tracker.error.message, + ); default: // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -242,15 +243,13 @@ API_V1_ROUTER.add("GET /imports/:importID/poll-status", async ({ params }) => { const tracker = await GetImportTrackerByImportId(params.importID); if (tracker?.type === "FAILED") { - return { - $status: tracker.error.statusCode ?? 500, - body: {}, - description: tracker.error.message, - success: true as const, - }; + throw new ExpectedErr(tracker.error.statusCode ?? 500, tracker.error.message); } - return success("Import is ongoing.", { importStatus: "ongoing", progress: 0 }); + return success("Import is ongoing.", { + importStatus: "ongoing", + progress: { description: "Importing scores." }, + }); } throw new ExpectedErr(500, "Unrecognised job queue state.");