🆙 Fix full cms 🆙

This commit is contained in:
Remco
2026-10-28 20:49:43 +01:00
parent d77869eea9
commit 77bca88392
422 changed files with 2577 additions and 8434 deletions
+6 -6
View File
@@ -140,7 +140,7 @@ test.describe("rejects malformed figures", () => {
test(name, async ({ request }) => {
const response = await getThroughEdge(
request,
`/api/imaging/avatar${query}`,
`/cms_api/imaging/avatar${query}`,
);
expect(response.status()).toBe(400);
});
@@ -171,14 +171,14 @@ test("collapses a burst of concurrent requests for one figure", async ({
test("the badge endpoint serves an image and revalidates", async ({
request,
}) => {
const first = await getThroughEdge(request, "/api/imaging/badge?code=ADM");
const first = await getThroughEdge(request, "/cms_api/imaging/badge?code=ADM");
expect(first.status()).toBe(200);
expect(first.headers()["content-type"]).toContain("image/");
expect(first.headers()["access-control-allow-origin"]).toBe("*");
const etag = first.headers().etag;
expect(etag).toBeTruthy();
const second = await getThroughEdge(request, "/api/imaging/badge?code=ADM", {
const second = await getThroughEdge(request, "/cms_api/imaging/badge?code=ADM", {
headers: { "if-none-match": etag as string },
});
expect(second.status()).toBe(304);
@@ -189,7 +189,7 @@ test("the badge endpoint can resolve from a local source", async ({
}) => {
const response = await getThroughEdge(
request,
"/api/imaging/badge?code=ADM&source=local",
"/cms_api/imaging/badge?code=ADM&source=local",
);
expect(response.status()).toBe(200);
expect(response.headers()["content-type"]).toContain("image/");
@@ -208,7 +208,7 @@ test.describe("rejects malformed badge codes", () => {
test(name, async ({ request }) => {
const response = await getThroughEdge(
request,
`/api/imaging/badge${query}`,
`/cms_api/imaging/badge${query}`,
);
expect(response.status()).toBe(400);
});
@@ -218,7 +218,7 @@ test.describe("rejects malformed badge codes", () => {
test("the badge preflight advertises GET", async ({ request }) => {
const response = await fetchThroughEdge(
request,
"/api/imaging/badge?code=ADM",
"/cms_api/imaging/badge?code=ADM",
{
method: "OPTIONS",
},
+1 -1
View File
@@ -71,7 +71,7 @@ export const FIGURE =
"hr-11782-40-40.hd-180-7-14.ch-11592-66.lg-10726-79-1408.sh-11764-1408.ha-11958-70-1408.wa-2007-0.0";
export const avatarPath = (params: Record<string, string> = {}) =>
`/api/imaging/avatar?${new URLSearchParams({ figure: FIGURE, ...params })}`;
`/cms_api/imaging/avatar?${new URLSearchParams({ figure: FIGURE, ...params })}`;
/** Public imager endpoint served through the edge proxy (imager container). */
export const IMAGER_PATH = (params: Record<string, string> = {}) =>
+2 -2
View File
@@ -8,7 +8,7 @@ import {
} from "./imaging-target";
// Regression guard for the public topology: the edge proxy must front the
// imager container at `/imaging/*` while `/api/imaging/*` reaches the CMS.
// imager container at `/imaging/*` while `/cms_api/imaging/*` reaches the CMS.
// Mis-routing here blanks avatars for every visitor, and no unit test sees it.
// Only meaningful against the public origin, so it is skipped when the CMS is
// reached directly (set `IMAGING_EDGE=1` for the public origin).
@@ -40,7 +40,7 @@ test("the CMS avatar endpoint is reachable through the edge", async ({
});
test("the edge does not shadow the CMS badge endpoint", async ({ request }) => {
const response = await getThroughEdge(request, "/api/imaging/badge?code=ADM");
const response = await getThroughEdge(request, "/cms_api/imaging/badge?code=ADM");
expect(response.status()).toBe(200);
expect(response.headers()["content-type"]).toContain("image/");
});
+3 -3
View File
@@ -93,7 +93,7 @@ test("staff signs in, saves a draft, previews it and publishes to anonymous read
// navigates there explicitly anyway; this asserts the redirect target.
await expect(page).toHaveURL(/\/admin\/articles\/new(?:\?|$)/);
const session = await context.request
.get("/api/auth/session")
.get("/cms_api/auth/session")
.then((response) => response.json());
expect(session.user).toMatchObject({
id: String(fixture.userId),
@@ -168,7 +168,7 @@ test("staff signs in, saves a draft, previews it and publishes to anonymous read
await expect(reader.locator(".error-screen-code")).toHaveText("404");
await expect(publicTitle).toHaveCount(0);
const listing = await anonymous.request
.get("/api/articles")
.get("/cms_api/articles")
.then((response) => response.json());
expect(listing.data).toEqual([]);
expect(listing.meta.total).toBe(0);
@@ -266,7 +266,7 @@ test("staff signs in, saves a draft, previews it and publishes to anonymous read
await expect(reader.locator(".article-body")).toContainText(body);
await expect(reader.locator(".error-screen-code")).toHaveCount(0);
const listing = await anonymous.request
.get("/api/articles")
.get("/cms_api/articles")
.then((response) => response.json());
expect(listing.data).toHaveLength(1);
expect(listing.data[0]).toMatchObject({
+2 -2
View File
@@ -253,7 +253,7 @@ try {
LOG_LEVEL: "warn",
})
.withExposedPorts(3002)
.withWaitStrategy(Wait.forHttp("/api/health", 3002).forStatusCode(200))
.withWaitStrategy(Wait.forHttp("/cms_api/health", 3002).forStatusCode(200))
.withStartupTimeout(120_000)
.withLogConsumer((stream) =>
stream.on("data", (chunk: Buffer) => {
@@ -263,7 +263,7 @@ try {
.start();
upstream = new URL(`http://${app.getHost()}:${app.getMappedPort(3002)}`);
abort.signal.throwIfAborted();
const health = (await fetch(new URL("/api/health", upstream), {
const health = (await fetch(new URL("/cms_api/health", upstream), {
signal: AbortSignal.timeout(10_000),
}).then((response) => response.json())) as {
status?: string;
+4 -4
View File
@@ -18,7 +18,7 @@ test("attachment upload and double-click resume send one retry and refresh histo
page,
}) => {
let retries = 0;
await page.route("**/api/admin/studio/import-attachment", async (route) => {
await page.route("**/cms_api/admin/studio/import-attachment", async (route) => {
expect(route.request().method()).toBe("POST");
expect(route.request().postData()).toContain('name="classname"');
expect(route.request().postData()).toContain("fixture_chair");
@@ -27,7 +27,7 @@ test("attachment upload and double-click resume send one retry and refresh histo
);
await route.fulfill({ json: { ok: true, attachmentId } });
});
await page.route("**/api/admin/studio/import-jobs", async (route) => {
await page.route("**/cms_api/admin/studio/import-jobs", async (route) => {
retries++;
expect(route.request().method()).toBe("PATCH");
expect(route.request().postDataJSON()).toEqual({
@@ -82,11 +82,11 @@ for (const scenario of [
page,
}) => {
let retries = 0;
await page.route("**/api/admin/studio/import-jobs", async (route) => {
await page.route("**/cms_api/admin/studio/import-jobs", async (route) => {
retries++;
await route.fulfill({ json: { ok: true } });
});
await page.route("**/api/admin/studio/import-attachment", (route) =>
await page.route("**/cms_api/admin/studio/import-attachment", (route) =>
scenario.status
? route.fulfill({
status: scenario.status,
+3 -3
View File
@@ -33,12 +33,12 @@ const catalog = {
};
async function mockStudio(page: Page) {
await page.route("**/api/**", async (route) => {
await page.route("**/cms_api/**", async (route) => {
const url = new URL(route.request().url());
if (url.pathname === "/api/admin/catalog/revision")
if (url.pathname === "/cms_api/admin/catalog/revision")
return route.fulfill({ json: { ok: true, revision: catalog.revision } });
if (
url.pathname === "/api/admin/catalog/tree" &&
url.pathname === "/cms_api/admin/catalog/tree" &&
url.searchParams.get("mode") === "full"
)
return route.fulfill({
+3 -3
View File
@@ -241,7 +241,7 @@ const server = createServer(async (req, res) => {
}
body = JSON.parse(Buffer.concat(chunks).toString() || "{}");
}
if (url.pathname === "/api/admin/studio/import-jobs") {
if (url.pathname === "/cms_api/admin/studio/import-jobs") {
if (req.method === "PATCH") {
const job = jobs.get(body.id);
if (job) job.state = "cancelled";
@@ -253,7 +253,7 @@ const server = createServer(async (req, res) => {
nextCursor: null,
});
}
if (url.pathname === "/api/admin/import/official/sync-all") {
if (url.pathname === "/cms_api/admin/import/official/sync-all") {
if (!jobs.has(body.id))
jobs.set(body.id, {
id: body.id,
@@ -279,7 +279,7 @@ const server = createServer(async (req, res) => {
await new Promise((resolve) => setTimeout(resolve, 150));
return json(res, { ok: true, job: jobs.get(body.id) });
}
if (url.pathname === "/api/media") return json(res, { files: [] });
if (url.pathname === "/cms_api/media") return json(res, { files: [] });
if (url.pathname.startsWith("/fixture/actions/")) {
const name = url.pathname.split("/").at(-1);
if (name === "load-recovery")
+5 -5
View File
@@ -9,7 +9,7 @@ test("history hook deduplicates refreshes, pages and reports completion once", a
}) => {
let requests = 0;
let completed = false;
await page.route("**/api/admin/studio/import-jobs?*", async (route) => {
await page.route("**/cms_api/admin/studio/import-jobs?*", async (route) => {
requests++;
const secondPage = new URL(route.request().url()).searchParams.has(
"before",
@@ -68,7 +68,7 @@ test("history polling pauses while hidden and refreshes on visibility return", a
}) => {
let requests = 0;
await page.clock.install();
await page.route("**/api/admin/studio/import-jobs?*", (route) => {
await page.route("**/cms_api/admin/studio/import-jobs?*", (route) => {
requests++;
return route.fulfill({
json: { ok: true, jobs: [furnitureJobFixture], nextCursor: null },
@@ -102,7 +102,7 @@ test("history polling pauses while hidden and refreshes on visibility return", a
test("history shows the current phase and validated recovery source", async ({
page,
}) => {
await page.route("**/api/admin/studio/import-jobs?*", (route) =>
await page.route("**/cms_api/admin/studio/import-jobs?*", (route) =>
route.fulfill({
json: {
ok: true,
@@ -148,10 +148,10 @@ test("uncertain import submissions keep their identity after a page reload", asy
page,
}) => {
const ids: string[] = [];
await page.route("**/api/admin/csrf", (route) =>
await page.route("**/cms_api/admin/csrf", (route) =>
route.fulfill({ json: { token: "a".repeat(64) } }),
);
await page.route("**/api/admin/studio/import-jobs**", async (route) => {
await page.route("**/cms_api/admin/studio/import-jobs**", async (route) => {
if (route.request().method() !== "POST") {
await route.fulfill({ json: { ok: true, jobs: [], nextCursor: null } });
return;
+3 -3
View File
@@ -12,7 +12,7 @@ test("notification reads persist, new replies are unread and failed preferences
let key = "support:42";
const readKeys = new Set<string>();
let fail = false;
await page.route("**/api/notifications**", async (route) => {
await page.route("**/cms_api/notifications**", async (route) => {
if (route.request().method() === "POST") {
if (fail)
return route.fulfill({ status: 503, json: { error: "Unavailable" } });
@@ -83,7 +83,7 @@ test("notification service failure offers a retry and recovers to empty state",
page,
}) => {
let failing = true;
await page.route("**/api/notifications**", (route) =>
await page.route("**/cms_api/notifications**", (route) =>
route.fulfill(
failing
? { status: 503, json: { error: "Unavailable" } }
@@ -113,7 +113,7 @@ test("category changes persist and pagination recovers when filtering shrinks th
page,
}) => {
let enabled = true;
await page.route("**/api/notifications**", async (route) => {
await page.route("**/cms_api/notifications**", async (route) => {
if (route.request().method() === "POST") {
enabled = route.request().postDataJSON().preferences.support;
return route.fulfill({ json: { ok: true } });
+5 -5
View File
@@ -49,11 +49,11 @@ test("the furniture pane is painted, not merely present in the DOM", async ({
});
const page = await context.newPage();
await page.route("**/api/**", async (route) => {
await page.route("**/cms_api/**", async (route) => {
const request = route.request();
const url = new URL(request.url());
if (url.pathname === "/api/admin/import/furni") {
if (url.pathname === "/cms_api/admin/import/furni") {
return route.fulfill({
json:
url.searchParams.get("action") === "stats"
@@ -64,11 +64,11 @@ test("the furniture pane is painted, not merely present in the DOM", async ({
},
});
}
if (url.pathname === "/api/admin/import/clone")
if (url.pathname === "/cms_api/admin/import/clone")
return route.fulfill({ json: { sources: [] } });
if (url.pathname === "/api/admin/studio/import-jobs")
if (url.pathname === "/cms_api/admin/studio/import-jobs")
return route.fulfill({ json: { ok: true, jobs: [], nextCursor: null } });
if (url.pathname === "/api/admin/studio/nitro-quality")
if (url.pathname === "/cms_api/admin/studio/nitro-quality")
return route.fulfill({
json: {
report: {
+2 -2
View File
@@ -13,7 +13,7 @@ const items = Array.from({ length: 60 }, (_, index) => ({
iconUrl: "/fixture/cover.svg",
}));
async function mockStudio(page: Page) {
await page.route("**/api/**", async (route) => {
await page.route("**/cms_api/**", async (route) => {
const url = new URL(route.request().url());
if (url.pathname.endsWith("/inspect"))
return route.fulfill({
@@ -147,7 +147,7 @@ test("an older search response cannot replace newer furniture", async ({
page,
}) => {
await mockStudio(page);
await page.route("**/api/admin/import/furni?*", async (route) => {
await page.route("**/cms_api/admin/import/furni?*", async (route) => {
const search = new URL(route.request().url()).searchParams.get("search");
if (!search) return route.fallback();
if (search === "old")
+13 -13
View File
@@ -35,10 +35,10 @@ test("Studio opens and dismisses detail, Nitro editor and import review", async
let metadataRequests = 0;
let qualityRequests = 0;
page.on("pageerror", (error) => errors.push(error.message));
await page.route("**/api/**", async (route) => {
await page.route("**/cms_api/**", async (route) => {
const request = route.request();
const url = new URL(request.url());
if (url.pathname === "/api/admin/studio/nitro-quality") {
if (url.pathname === "/cms_api/admin/studio/nitro-quality") {
qualityRequests++;
return route.fulfill({
json: {
@@ -55,7 +55,7 @@ test("Studio opens and dismisses detail, Nitro editor and import review", async
},
});
}
if (url.pathname === "/api/admin/studio/inspect") {
if (url.pathname === "/cms_api/admin/studio/inspect") {
const { classnames } = request.postDataJSON() as { classnames: string[] };
return route.fulfill({
json: {
@@ -71,7 +71,7 @@ test("Studio opens and dismisses detail, Nitro editor and import review", async
},
});
}
if (url.pathname === "/api/admin/studio/source-assets") {
if (url.pathname === "/cms_api/admin/studio/source-assets") {
const body = request.postDataJSON() as {
items: Array<{ classname: string }>;
};
@@ -107,7 +107,7 @@ test("Studio opens and dismisses detail, Nitro editor and import review", async
},
});
}
if (url.pathname === "/api/admin/import/furni") {
if (url.pathname === "/cms_api/admin/import/furni") {
return route.fulfill({
json:
url.searchParams.get("action") === "stats"
@@ -118,9 +118,9 @@ test("Studio opens and dismisses detail, Nitro editor and import review", async
},
});
}
if (url.pathname === "/api/admin/import/clone")
if (url.pathname === "/cms_api/admin/import/clone")
return route.fulfill({ json: { sources: [] } });
if (url.pathname === "/api/admin/studio/import-jobs")
if (url.pathname === "/cms_api/admin/studio/import-jobs")
return route.fulfill({ json: { ok: true, jobs: [], nextCursor: null } });
return route.fulfill({
status: 404,
@@ -220,7 +220,7 @@ test("Studio loads organize imports on demand and preserves the dialog on reopen
json: { error: "No mutations expected" },
});
});
await page.route("**/api/**", async (route) => {
await page.route("**/cms_api/**", async (route) => {
const request = route.request();
const url = new URL(request.url());
if (request.method() !== "GET") {
@@ -230,7 +230,7 @@ test("Studio loads organize imports on demand and preserves the dialog on reopen
json: { error: "No mutations expected" },
});
}
if (url.pathname === "/api/admin/import/organize") {
if (url.pathname === "/cms_api/admin/import/organize") {
organizeRequests++;
return route.fulfill({
json: {
@@ -263,9 +263,9 @@ test("Studio loads organize imports on demand and preserves the dialog on reopen
},
});
}
if (url.pathname === "/api/admin/catalog/tree")
if (url.pathname === "/cms_api/admin/catalog/tree")
return route.fulfill({ json: { pages: [] } });
if (url.pathname === "/api/admin/import/furni")
if (url.pathname === "/cms_api/admin/import/furni")
return route.fulfill({
json:
url.searchParams.get("action") === "stats"
@@ -275,9 +275,9 @@ test("Studio loads organize imports on demand and preserves the dialog on reopen
meta: { currentPage: 1, lastPage: 1, total: 0, perPage: 20 },
},
});
if (url.pathname === "/api/admin/import/clone")
if (url.pathname === "/cms_api/admin/import/clone")
return route.fulfill({ json: { sources: [] } });
if (url.pathname === "/api/admin/studio/import-jobs")
if (url.pathname === "/cms_api/admin/studio/import-jobs")
return route.fulfill({ json: { ok: true, jobs: [], nextCursor: null } });
return route.fulfill({
status: 404,