diff --git a/backend/src/services/importResolvers.ts b/backend/src/services/importResolvers.ts index fab71e5..f8bfd10 100644 --- a/backend/src/services/importResolvers.ts +++ b/backend/src/services/importResolvers.ts @@ -536,13 +536,15 @@ export function extractPageMetadata(html: string, pageHost: string): ImportedPag return meta; } -/** The profile named in the URL hash (if the design has it), then the default, then the first. */ -function makerworldInstanceIdFromNextData(data: unknown, requestedInstanceId: string | null): string | null { +/** The profile named in the URL hash (if the design has it), then the default (if the design still + * has it), then the first. MakerWorld can leave `defaultInstanceId` pointing at a deleted profile, + * whose download answers 403 "The client does not have access rights to the content." */ +export function makerworldInstanceIdFromNextData(data: unknown, requestedInstanceId: string | null): string | null { const design = getPath(data, "props", "pageProps", "design") as Record | undefined; if (!design) return null; if (requestedInstanceId && makerworldDesignHasInstance(design, requestedInstanceId)) return requestedInstanceId; const defaultInstance = design.defaultInstanceId; - if (defaultInstance) return String(defaultInstance); + if (defaultInstance && makerworldDesignHasInstance(design, String(defaultInstance))) return String(defaultInstance); const instances = design.instances; if (Array.isArray(instances)) { for (const inst of instances) { diff --git a/backend/tests/makerworldInstanceSelection.test.ts b/backend/tests/makerworldInstanceSelection.test.ts new file mode 100644 index 0000000..9153187 --- /dev/null +++ b/backend/tests/makerworldInstanceSelection.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; +import { makerworldInstanceIdFromNextData } from "../src/services/importResolvers"; + +function nextData(design: Record) { + return { props: { pageProps: { design } } }; +} + +describe("makerworldInstanceIdFromNextData", () => { + const design = { defaultInstanceId: 2, instances: [{ id: 1 }, { id: 2 }, { id: 3 }] }; + + it("takes the link's profile, else the default, else the first", () => { + expect(makerworldInstanceIdFromNextData(nextData(design), "3")).toBe("3"); + expect(makerworldInstanceIdFromNextData(nextData(design), null)).toBe("2"); + expect(makerworldInstanceIdFromNextData(nextData(design), "999")).toBe("2"); + expect(makerworldInstanceIdFromNextData(nextData({ instances: [{ id: 7 }] }), null)).toBe("7"); + }); + + // Seen on MakerWorld model 1106505: defaultInstanceId 1102432, but the only profile is 1113369. + // Downloading the stale default answers 403 "The client does not have access rights to the content." + it("skips a default that is no longer one of the design's profiles", () => { + const stale = { defaultInstanceId: 1102432, instances: [{ id: 1113369 }] }; + expect(makerworldInstanceIdFromNextData(nextData(stale), null)).toBe("1113369"); + }); + + it("answers null without a design or profiles", () => { + expect(makerworldInstanceIdFromNextData({}, null)).toBeNull(); + expect(makerworldInstanceIdFromNextData(nextData({ defaultInstanceId: 5, instances: [] }), null)).toBeNull(); + }); +}); diff --git a/extension/src/content/makerworld/pageData.ts b/extension/src/content/makerworld/pageData.ts index 847f7fa..334f20e 100644 --- a/extension/src/content/makerworld/pageData.ts +++ b/extension/src/content/makerworld/pageData.ts @@ -100,12 +100,16 @@ export async function loadMakerworldCollectionTotal(pageUrl: string): Promise inst && inst.id) : []; - if (requestedInstanceId && instances.some((inst) => String(inst.id) === requestedInstanceId)) - return requestedInstanceId; - if (design.defaultInstanceId) return String(design.defaultInstanceId); + const has = (id: string) => instances.some((inst) => String(inst.id) === id); + if (requestedInstanceId && has(requestedInstanceId)) return requestedInstanceId; + const defaultId = design.defaultInstanceId ? String(design.defaultInstanceId) : null; + if (defaultId && has(defaultId)) return defaultId; return instances.length ? String(instances[0].id) : null; }