From 2fb69d461d68b26c1620253a38407ac0f37f1cda Mon Sep 17 00:00:00 2001 From: Adam Daley Date: Thu, 5 Nov 2026 18:02:01 +0000 Subject: [PATCH 1/2] Move the extension directory API to the v2 namespace --- README.md | 18 +- src/app/index.ts | 2 +- src/services/extensions/v2/README.md | 12 +- src/services/extensions/v2/index.ts | 6 +- test/services/extensions/v2/account.test.ts | 38 +- test/services/extensions/v2/badges.test.ts | 6 +- .../extensions/v2/claim-budget.test.ts | 14 +- test/services/extensions/v2/contract.test.ts | 72 ++- .../extensions/v2/developer-approval.test.ts | 45 +- .../extensions/v2/developer-profiles.test.ts | 472 ++++++++---------- .../extensions/v2/extension-writes.test.ts | 201 +++----- test/services/extensions/v2/harness.ts | 6 +- test/services/extensions/v2/index.test.ts | 13 +- test/services/extensions/v2/manifest.test.ts | 37 +- .../extensions/v2/moderation-correct.test.ts | 16 +- .../extensions/v2/moderation-notify.test.ts | 34 +- .../v2/moderation-revalidate.test.ts | 35 +- .../v2/moderation-revocation.test.ts | 8 +- .../services/extensions/v2/moderation.test.ts | 269 +++++----- test/services/extensions/v2/ownership.test.ts | 216 ++++---- .../extensions/v2/pending-edits.test.ts | 6 +- .../extensions/v2/public-extensions.test.ts | 49 +- test/services/extensions/v2/publisher.test.ts | 67 ++- .../extensions/v2/resource-bounds.test.ts | 83 ++- 24 files changed, 770 insertions(+), 955 deletions(-) diff --git a/README.md b/README.md index e46bcd39..e7eb7ca7 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ The worker exposes five services: - **Central Alerts** (`/central-alerts/v1`) Allows the project to push critical notifications to all FOSSBilling installations—useful for security hotfixes or major announcements. -- **Extensions** (`/extensions/v1`, `/extensions/v2`) +- **Extensions** (`/extensions/v1`, `/v2`) Owns the complete Extensions domain and its `DB_EXTENSIONS` schema. `v2` is authoritative; `v1` is legacy read-only. The separate Extensions site keeps OIDC/session state but accesses this domain through the generated HTTPS API client; it must not bind or migrate `DB_EXTENSIONS`. See [`src/services/extensions/v2/README.md`](src/services/extensions/v2/README.md). @@ -45,15 +45,15 @@ Cron and Durable Object code lives with its owning service and is wired in `src/ Each service documents its own endpoints and behaviour: -| Service | Base path | Docs | -| -------------- | ---------------------------------- | -------------------------------------------------------------------------------------- | -| Versions | `/versions/v1` | [`src/services/versions/v1/README.md`](src/services/versions/v1/README.md) | -| Central Alerts | `/central-alerts/v1` | [`src/services/central-alerts/v1/README.md`](src/services/central-alerts/v1/README.md) | -| Stats | `/stats/v1` | [`src/services/stats/v1/README.md`](src/services/stats/v1/README.md) | -| Extensions | `/extensions/v1`, `/extensions/v2` | [`src/services/extensions/v2/README.md`](src/services/extensions/v2/README.md) | -| Previews | `/previews/v1` | [`src/services/previews/v1/README.md`](src/services/previews/v1/README.md) | +| Service | Base path | Docs | +| -------------- | ----------------------- | -------------------------------------------------------------------------------------- | +| Versions | `/versions/v1` | [`src/services/versions/v1/README.md`](src/services/versions/v1/README.md) | +| Central Alerts | `/central-alerts/v1` | [`src/services/central-alerts/v1/README.md`](src/services/central-alerts/v1/README.md) | +| Stats | `/stats/v1` | [`src/services/stats/v1/README.md`](src/services/stats/v1/README.md) | +| Extensions | `/extensions/v1`, `/v2` | [`src/services/extensions/v2/README.md`](src/services/extensions/v2/README.md) | +| Previews | `/previews/v1` | [`src/services/previews/v1/README.md`](src/services/previews/v1/README.md) | -Extensions v2 and Previews v1 also publish a live OpenAPI document (`/extensions/v2/docs/openapi.json`, `/previews/v1/docs/openapi.json`) and a reference UI (`/extensions/v2/docs`, `/previews/v1/docs`). +Extensions v2 and Previews v1 also publish a live OpenAPI document (`/v2/docs/openapi.json`, `/previews/v1/docs/openapi.json`) and a reference UI (`/v2/docs`, `/previews/v1/docs`). ## Configuration diff --git a/src/app/index.ts b/src/app/index.ts index e03fddd5..353fae3d 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -25,7 +25,7 @@ app.use("*", async (c, next) => { app.route("/central-alerts/v1", centralAlertsV1); app.route("/extensions/v1", extensionsV1); -app.route("/extensions/v2", extensionsV2); +app.route("/v2", extensionsV2); app.route("/previews/v1", previewsV1); app.route("/versions/v1", versionsV1); app.route("/stats/v1", statsV1); diff --git a/src/services/extensions/v2/README.md b/src/services/extensions/v2/README.md index 4ddd4792..070de0c5 100644 --- a/src/services/extensions/v2/README.md +++ b/src/services/extensions/v2/README.md @@ -1,6 +1,8 @@ # Extensions v2 -**Base Path:** `/extensions/v2` +**Base Path:** `/v2` + +All v2 resources use this version-first namespace: `/v2/extensions`, `/v2/developers`, `/v2/users`, and `/v2/m2m`. The previous `/extensions/v2` base is removed; the extensions site, modules publisher, and FOSSBilling installer must switch with the API deployment. Legacy `/extensions/v1` routes keep their existing URLs. Self-service extension publishing, developer-profile ownership, moderation, and browsing published extensions. @@ -10,8 +12,8 @@ This service owns the complete Extensions domain and its `DB_EXTENSIONS` schema. Endpoints are not listed here. The service publishes its own contract: -- **OpenAPI document:** `GET /extensions/v2/docs/openapi.json` -- **Reference UI:** `GET /extensions/v2/docs` +- **OpenAPI document:** `GET /v2/docs/openapi.json` +- **Reference UI:** `GET /v2/docs` ## Manifest contract @@ -136,9 +138,9 @@ Provisioning spans both repos: create the client and its scopes/resource link in ## Public badges -`GET /extensions/v2/extensions/{id}/badges/{type}` returns a public SVG. Types are `version` (latest release tag), `fossbilling` (that release's full `requires.fossbilling` string), and `license` (package license). Requirements are displayed unchanged, including upper bounds, exclusions, and compound ranges. +`GET /v2/extensions/{id}/badges/{type}` returns a public SVG. Types are `version` (latest release tag), `fossbilling` (that release's full `requires.fossbilling` string), and `license` (package license). Requirements are displayed unchanged, including upper bounds, exclusions, and compound ranges. -For example: `/extensions/v2/extensions/branding/badges/fossbilling?color=green`. The optional `color` accepts badge-maker colors, up to 100 characters; unrecognized values use the library's default color. Badges are cached for five minutes in browsers and one hour at the CDN, matching v1. +For example: `/v2/extensions/branding/badges/fossbilling?color=green`. The optional `color` accepts badge-maker colors, up to 100 characters; unrecognized values use the library's default color. Badges are cached for five minutes in browsers and one hour at the CDN, matching v1. Missing, unpublished, and delisted extensions return 404. Unsupported badge types or color lengths outside 1–100 return 422; unusable stored badge fields return 409 for manual correction. With no releases, version and requirement badges show `unknown`; license badges still work. diff --git a/src/services/extensions/v2/index.ts b/src/services/extensions/v2/index.ts index 115cf4f9..60af455e 100644 --- a/src/services/extensions/v2/index.ts +++ b/src/services/extensions/v2/index.ts @@ -104,14 +104,14 @@ extensionsV2.route( (cachedOpenApiDocument ??= extensionsV2.getOpenAPI31Document({ openapi: "3.1.0", info: { - title: "FOSSBilling Extensions API (v2)", + title: "FOSSBilling API (extensionsV2)", version: "2.0.0", description: "Self-service extension publishing, ownership, moderation, and public browsing. v1 (/extensions/v1) remains available for existing integrations." }, - servers: [{ url: "/extensions/v2" }] + servers: [{ url: "/v2" }] })), - pageTitle: "FOSSBilling Extensions API (v2)", + pageTitle: "FOSSBilling API (extensionsV2)", agent: { disabled: true }, documentDownloadType: "none", hideClientButton: true, diff --git a/test/services/extensions/v2/account.test.ts b/test/services/extensions/v2/account.test.ts index 8adb66d9..f0ed008c 100644 --- a/test/services/extensions/v2/account.test.ts +++ b/test/services/extensions/v2/account.test.ts @@ -61,7 +61,7 @@ describe("Extensions API v2", () => { it("rejects ordinary assertions before storing attacker-selected identity", async () => { const response = await put( - "/extensions/v2/users/me/identity", + "/v2/users/me/identity", await authHeaders("attacker"), identity ); @@ -83,7 +83,7 @@ describe("Extensions API v2", () => { ["github_orgs_expires_at", "2099-02-01T00:00:00.000Z"] ])("rejects tampering with signed %s", async (field, value) => { const headers = await identityHeaders("new-identity", identity); - const response = await put("/extensions/v2/users/me/identity", headers, { + const response = await put("/v2/users/me/identity", headers, { ...identity, [field as string]: value }); @@ -108,7 +108,7 @@ describe("Extensions API v2", () => { async (rawBody) => { const ctx = createExecutionContext(); const res = await app.request( - "/extensions/v2/users/me/identity", + "/v2/users/me/identity", { method: "PUT", headers: await identityHeaders("raw-body", identity), @@ -142,7 +142,7 @@ describe("Extensions API v2", () => { }); const ctx = createExecutionContext(); const res = await app.request( - "/extensions/v2/users/me/identity", + "/v2/users/me/identity", { method: "PUT", headers: { @@ -161,7 +161,7 @@ describe("Extensions API v2", () => { it("keeps identity proofs out of user API authorization", async () => { const response = await get( - "/extensions/v2/users/me", + "/v2/users/me", await identityHeaders("service-sub", identity) ); expect(response.status).toBe(401); @@ -223,7 +223,7 @@ describe("Extensions API v2", () => { } }); - const profile = await patch("/extensions/v2/users/me", headers, { + const profile = await patch("/v2/users/me", headers, { display_name: "Account Display" }); expect(profile.status).toBe(200); @@ -236,7 +236,7 @@ describe("Extensions API v2", () => { } }); - const developer = await get("/extensions/v2/developers/me", headers); + const developer = await get("/v2/developers/me", headers); expect(developer.status).toBe(200); expect(await developer.json()).toEqual({ result: null }); @@ -262,7 +262,7 @@ describe("Extensions API v2", () => { download_url: "https://example.com/download.zip" }); - const owned = await get("/extensions/v2/extensions?scope=mine", headers); + const owned = await get("/v2/extensions?scope=mine", headers); expect(owned.status).toBe(200); expect(await owned.json()).toMatchObject({ result: [{ id: "account-extension" }], @@ -270,7 +270,7 @@ describe("Extensions API v2", () => { }); const filtered = await get( - "/extensions/v2/extensions?scope=mine&developer_id=someone-else", + "/v2/extensions?scope=mine&developer_id=someone-else", headers ); expect(filtered.status).toBe(422); @@ -281,7 +281,7 @@ describe("Extensions API v2", () => { it("validates a mine cursor before returning an empty owner page", async () => { const res = await get( - "/extensions/v2/extensions?scope=mine&cursor=not-a-cursor", + "/v2/extensions?scope=mine&cursor=not-a-cursor", await authHeaders("no-developer") ); expect(res.status).toBe(422); @@ -345,11 +345,11 @@ describe("Extensions API v2", () => { it("tombstones and later reactivates an account", async () => { const headers = await authHeaders("delete-me"); - const deleted = await del("/extensions/v2/users/me", headers); + const deleted = await del("/v2/users/me", headers); expect(deleted.status).toBe(200); expect(await deleted.json()).toEqual({ result: { deleted: true } }); - const afterDelete = await get("/extensions/v2/users/me", headers); + const afterDelete = await get("/v2/users/me", headers); expect(afterDelete.status).toBe(200); expect(await afterDelete.json()).toMatchObject({ result: { active: false, display_name: null } @@ -385,7 +385,7 @@ describe("Extensions API v2", () => { expect(row?.deleted_at).toBeTruthy(); const blockedWrite = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", headers, sampleDeveloper({ id: "deleted-developer" }) ); @@ -414,7 +414,7 @@ describe("Extensions API v2", () => { // cleanup statement must run in the same batch or the developers // delete would fail on the foreign key. await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("mapping-owner"), sampleDeveloper({ id: "mapped-developer" }) ); @@ -430,7 +430,7 @@ describe("Extensions API v2", () => { expect(before).not.toBeNull(); const deleted = await del( - "/extensions/v2/users/me", + "/v2/users/me", await authHeaders("mapping-owner") ); expect(deleted.status).toBe(200); @@ -444,7 +444,7 @@ describe("Extensions API v2", () => { it("blocks deletion while published extensions remain owned", async () => { await seedOwnedExtension(); const headers = await authHeaders("owner-1"); - const deleted = await del("/extensions/v2/users/me", headers); + const deleted = await del("/v2/users/me", headers); expect(deleted.status).toBe(409); const row = await db .prepare("SELECT deleted_at FROM users WHERE id = ?") @@ -468,7 +468,7 @@ describe("Extensions API v2", () => { }); const deleted = await del( - "/extensions/v2/users/me", + "/v2/users/me", await authHeaders("pending-owner") ); expect(deleted.status).toBe(409); @@ -544,7 +544,7 @@ describe("Extensions API v2", () => { .run(); const deleted = await del( - "/extensions/v2/users/me", + "/v2/users/me", await authHeaders("cleanup-user") ); expect(deleted.status).toBe(200); @@ -618,7 +618,7 @@ describe("Extensions API v2", () => { try { const deleted = await del( - "/extensions/v2/users/me", + "/v2/users/me", await authHeaders("rollback-user") ); expect(deleted.status).toBe(500); diff --git a/test/services/extensions/v2/badges.test.ts b/test/services/extensions/v2/badges.test.ts index 0a957611..28aacd6e 100644 --- a/test/services/extensions/v2/badges.test.ts +++ b/test/services/extensions/v2/badges.test.ts @@ -17,7 +17,7 @@ const release = (tag: string, fossbilling: string) => ({ requires: { fossbilling, php: ">=8.3" } }); const badge = (type: string) => - get(`/extensions/v2/extensions/example/badges/${type}`, {}); + get(`/v2/extensions/example/badges/${type}`, {}); beforeEach(async () => { await insertDeveloper(db, { @@ -68,7 +68,7 @@ describe("v2 public badges", () => { it("supports case-insensitive extension lookup and color overrides", async () => { const response = await get( - "/extensions/v2/extensions/EXAMPLE/badges/version?color=green", + "/v2/extensions/EXAMPLE/badges/version?color=green", {} ); expect(response.status).toBe(200); @@ -178,7 +178,7 @@ describe("v2 public badges", () => { it("publishes the badge contract in OpenAPI", async () => { const document = (await ( - await get("/extensions/v2/docs/openapi.json", {}) + await get("/v2/docs/openapi.json", {}) ).json()) as { paths: Record }; expect(document.paths["/extensions/{id}/badges/{type}"]).toMatchObject({ get: { diff --git a/test/services/extensions/v2/claim-budget.test.ts b/test/services/extensions/v2/claim-budget.test.ts index bbfc445b..f593d12d 100644 --- a/test/services/extensions/v2/claim-budget.test.ts +++ b/test/services/extensions/v2/claim-budget.test.ts @@ -17,7 +17,7 @@ vi.mock("@octokit/request", async () => (await import("../../../mocks/octokit")).octokitRequestMock() ); setupExtensionsV2Tests(); -const path = "/extensions/v2/developers/target/claim"; +const path = "/v2/developers/target/claim"; describe("durable claim verification budgets", () => { it("stops mismatches before GitHub, including after changing targets", async () => { @@ -29,7 +29,7 @@ describe("durable claim verification budgets", () => { for (let i = 0; i < 4; i++) { await seedUnownedDeveloper(`target-${i}`); const response = await post( - `/extensions/v2/developers/target-${i}/claim`, + `/v2/developers/target-${i}/claim`, headers, {} ); @@ -46,12 +46,8 @@ describe("durable claim verification budgets", () => { expect(response.status).toBe(201); const body = (await response.json()) as { result: { id: string } }; expect( - ( - await post( - `/extensions/v2/developers/claims/${body.result.id}/cancel`, - headers - ) - ).status + (await post(`/v2/developers/claims/${body.result.id}/cancel`, headers)) + .status ).toBe(200); } expect((await post(path, headers, {})).status).toBe(429); @@ -77,7 +73,7 @@ describe("durable claim verification budgets", () => { expect( ( await post( - "/extensions/v2/developers/independent-target/claim", + "/v2/developers/independent-target/claim", await authHeaders("independent"), {} ) diff --git a/test/services/extensions/v2/contract.test.ts b/test/services/extensions/v2/contract.test.ts index b031b82c..7c8d3c35 100644 --- a/test/services/extensions/v2/contract.test.ts +++ b/test/services/extensions/v2/contract.test.ts @@ -25,15 +25,12 @@ setupExtensionsV2Tests(); describe("Extensions API v2 contract", () => { it("serves static developer routes ahead of /developers/{id}", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); - const me = await get( - "/extensions/v2/developers/me", - await authHeaders("user-1") - ); + const me = await get("/v2/developers/me", await authHeaders("user-1")); expect(me.status).toBe(200); await expect(me.json()).resolves.toMatchObject({ result: { id: "dev-developer" } @@ -43,7 +40,7 @@ describe("Extensions API v2 contract", () => { it("rejects reserved developer ids", async () => { for (const reserved of ["me", "claims", "unapproved"]) { const res = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders(`user-${reserved}`), sampleDeveloper({ id: reserved }) ); @@ -57,13 +54,13 @@ describe("Extensions API v2 contract", () => { // developer_id is public-scope only. const scoped = await get( - "/extensions/v2/extensions?scope=mine&developer_id=dev-developer", + "/v2/extensions?scope=mine&developer_id=dev-developer", mod ); expect(scoped.status).toBe(422); // status/q are all-scope only. - const queued = await get("/extensions/v2/extensions?status=published", mod); + const queued = await get("/v2/extensions?status=published", mod); expect(queued.status).toBe(422); }); @@ -73,11 +70,11 @@ describe("Extensions API v2 contract", () => { const mod = await authHeaders("mod-1"); for (const path of [ - "/extensions/v2/extensions", - "/extensions/v2/revisions", - "/extensions/v2/developers", - "/extensions/v2/developers/claims?scope=pending", - "/extensions/v2/developers/new-developer/history" + "/v2/extensions", + "/v2/revisions", + "/v2/developers", + "/v2/developers/claims?scope=pending", + "/v2/developers/new-developer/history" ]) { const res = await get(path, mod); expect(res.status).toBe(200); @@ -105,7 +102,7 @@ describe("Extensions API v2 contract", () => { await insertUser(db, { id: "mod-1", is_moderator: 1 }); const mod = await authHeaders("mod-1"); - const first = await get("/extensions/v2/developers?limit=1", mod); + const first = await get("/v2/developers?limit=1", mod); expect(first.status).toBe(200); const firstBody = (await first.json()) as { result: Array<{ id: string }>; @@ -116,7 +113,7 @@ describe("Extensions API v2 contract", () => { expect(firstBody.pagination.next_cursor).toBeTruthy(); const second = await get( - `/extensions/v2/developers?limit=1&cursor=${encodeURIComponent(firstBody.pagination.next_cursor as string)}`, + `/v2/developers?limit=1&cursor=${encodeURIComponent(firstBody.pagination.next_cursor as string)}`, mod ); expect(second.status).toBe(200); @@ -136,8 +133,8 @@ describe("Extensions API v2 contract", () => { const mod = await authHeaders("mod-1"); for (const path of [ - "/extensions/v2/developers?limit=10&offset=10", - "/extensions/v2/developers/claims?scope=pending&limit=10&offset=10" + "/v2/developers?limit=10&offset=10", + "/v2/developers/claims?scope=pending&limit=10&offset=10" ]) { const res = await get(path, mod); expect(res.status).toBe(422); @@ -148,12 +145,12 @@ describe("Extensions API v2 contract", () => { await seedUnownedDeveloper("legacy-a"); await seedUnownedDeveloper("legacy-b"); await post( - "/extensions/v2/developers/legacy-a/claim", + "/v2/developers/legacy-a/claim", await authHeaders("user-1"), {} ); await post( - "/extensions/v2/developers/legacy-b/claim", + "/v2/developers/legacy-b/claim", await authHeaders("user-1"), {} ); @@ -161,7 +158,7 @@ describe("Extensions API v2 contract", () => { const mod = await authHeaders("mod-1"); const mine = await get( - "/extensions/v2/developers/claims?scope=mine&limit=1", + "/v2/developers/claims?scope=mine&limit=1", await authHeaders("user-1") ); const mineBody = (await mine.json()) as { @@ -170,7 +167,7 @@ describe("Extensions API v2 contract", () => { expect(mineBody.pagination.has_more).toBe(true); const crossed = await get( - `/extensions/v2/developers/claims?scope=pending&cursor=${encodeURIComponent(mineBody.pagination.next_cursor as string)}`, + `/v2/developers/claims?scope=pending&cursor=${encodeURIComponent(mineBody.pagination.next_cursor as string)}`, mod ); expect(crossed.status).toBe(422); @@ -181,23 +178,23 @@ describe("Extensions API v2 contract", () => { await seedUnownedDeveloper("legacy-b"); await seedUnownedDeveloper("legacy-c"); await post( - "/extensions/v2/developers/legacy-a/claim", + "/v2/developers/legacy-a/claim", await authHeaders("user-1"), {} ); await post( - "/extensions/v2/developers/legacy-b/claim", + "/v2/developers/legacy-b/claim", await authHeaders("user-1"), {} ); await post( - "/extensions/v2/developers/legacy-c/claim", + "/v2/developers/legacy-c/claim", await authHeaders("user-2"), {} ); const mine = await get( - "/extensions/v2/developers/claims?scope=mine&limit=1", + "/v2/developers/claims?scope=mine&limit=1", await authHeaders("user-1") ); const mineBody = (await mine.json()) as { @@ -206,7 +203,7 @@ describe("Extensions API v2 contract", () => { expect(mineBody.pagination.has_more).toBe(true); const crossed = await get( - `/extensions/v2/developers/claims?scope=mine&cursor=${encodeURIComponent(mineBody.pagination.next_cursor as string)}`, + `/v2/developers/claims?scope=mine&cursor=${encodeURIComponent(mineBody.pagination.next_cursor as string)}`, await authHeaders("user-2") ); expect(crossed.status).toBe(422); @@ -214,34 +211,31 @@ describe("Extensions API v2 contract", () => { it("rejects a history cursor from another developer", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper({ id: "dev-a", name: "Dev A" }) ); await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper({ id: "dev-a", name: "Dev A Edited" }) ); await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-2"), sampleDeveloper({ id: "dev-b", name: "Dev B" }) ); await insertUser(db, { id: "mod-1", is_moderator: 1 }); const mod = await authHeaders("mod-1"); - const first = await get( - "/extensions/v2/developers/dev-a/history?limit=1", - mod - ); + const first = await get("/v2/developers/dev-a/history?limit=1", mod); const firstBody = (await first.json()) as { pagination: { next_cursor: string | null; has_more: boolean }; }; expect(firstBody.pagination.has_more).toBe(true); const crossed = await get( - `/extensions/v2/developers/dev-b/history?cursor=${encodeURIComponent(firstBody.pagination.next_cursor as string)}`, + `/v2/developers/dev-b/history?cursor=${encodeURIComponent(firstBody.pagination.next_cursor as string)}`, mod ); expect(crossed.status).toBe(422); @@ -253,11 +247,11 @@ describe("Extensions API v2 contract", () => { const mod = await authHeaders("mod-1"); for (const path of [ - "/extensions/v2/extensions?cursor=nope", - "/extensions/v2/revisions?cursor=nope", - "/extensions/v2/developers?cursor=nope", - "/extensions/v2/developers/claims?scope=pending&cursor=nope", - "/extensions/v2/developers/new-developer/history?cursor=nope" + "/v2/extensions?cursor=nope", + "/v2/revisions?cursor=nope", + "/v2/developers?cursor=nope", + "/v2/developers/claims?scope=pending&cursor=nope", + "/v2/developers/new-developer/history?cursor=nope" ]) { const res = await get(path, mod); expect(res.status).toBe(422); diff --git a/test/services/extensions/v2/developer-approval.test.ts b/test/services/extensions/v2/developer-approval.test.ts index 1f29bcb0..ce07198a 100644 --- a/test/services/extensions/v2/developer-approval.test.ts +++ b/test/services/extensions/v2/developer-approval.test.ts @@ -25,7 +25,7 @@ vi.mock("@octokit/request", async () => setupExtensionsV2Tests(); -const profilePath = "/extensions/v2/developers/dev-developer"; +const profilePath = "/v2/developers/dev-developer"; async function review(headers: Record) { const response = await get(profilePath, headers); @@ -43,20 +43,20 @@ async function setupReview() { await insertUser(db, { id: "moderator", is_moderator: 1 }); const moderator = await authHeaders("moderator"); expect( - (await put("/extensions/v2/developers/me", owner, sampleDeveloper())).status + (await put("/v2/developers/me", owner, sampleDeveloper())).status ).toBe(200); return { owner, moderator, reviewed: await review(moderator) }; } describe("Developer approval binds to the reviewed profile instance and content", () => { it.each([ - ["profile deletion, same owner", "/extensions/v2/developers/me", "owner"], + ["profile deletion, same owner", "/v2/developers/me", "owner"], [ "profile deletion, different owner", - "/extensions/v2/developers/me", + "/v2/developers/me", "replacement-owner" ], - ["account deletion", "/extensions/v2/users/me", "replacement-owner"] + ["account deletion", "/v2/users/me", "replacement-owner"] ])( "rejects stale approval after %s", async (_label, deletionPath, replacementOwner) => { @@ -65,7 +65,7 @@ describe("Developer approval binds to the reviewed profile instance and content" expect( ( await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders(replacementOwner), sampleDeveloper({ name: "Unreviewed replacement" }) ) @@ -113,13 +113,11 @@ describe("Developer approval binds to the reviewed profile instance and content" ) { recreated = true; env.DB_EXTENSIONS = db; - expect((await del("/extensions/v2/developers/me", owner)).status).toBe( - 200 - ); + expect((await del("/v2/developers/me", owner)).status).toBe(200); expect( ( await put( - "/extensions/v2/developers/me", + "/v2/developers/me", owner, sampleDeveloper({ name: "Raced replacement" }) ) @@ -145,13 +143,11 @@ describe("Developer approval binds to the reviewed profile instance and content" ) { recreated = true; env.DB_EXTENSIONS = db; - expect((await del("/extensions/v2/developers/me", owner)).status).toBe( - 200 - ); + expect((await del("/v2/developers/me", owner)).status).toBe(200); expect( ( await put( - "/extensions/v2/developers/me", + "/v2/developers/me", owner, sampleDeveloper({ name: "Replacement" }) ) @@ -160,7 +156,7 @@ describe("Developer approval binds to the reviewed profile instance and content" } }); const edit = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", owner, sampleDeveloper({ name: "Paused old edit" }) ); @@ -204,13 +200,8 @@ describe("Developer approval binds to the reviewed profile instance and content" const { owner, moderator, reviewed } = await setupReview(); for (const name of ["Second version", "Third version"]) { expect( - ( - await put( - "/extensions/v2/developers/me", - owner, - sampleDeveloper({ name }) - ) - ).status + (await put("/v2/developers/me", owner, sampleDeveloper({ name }))) + .status ).toBe(200); // Model second-granular timestamps deterministically without timing a test. await db @@ -262,17 +253,17 @@ describe("Developer approval binds to the reviewed profile instance and content" result: { approved: boolean }; }; const detail = (await ( - await get("/extensions/v2/extensions/live-ext", {}) + await get("/v2/extensions/live-ext", {}) ).json()) as { result: { developer: { approved: boolean } } }; - const list = (await ( - await get("/extensions/v2/extensions", {}) - ).json()) as { result: Array<{ developer: { approved: boolean } }> }; + const list = (await (await get("/v2/extensions", {})).json()) as { + result: Array<{ developer: { approved: boolean } }>; + }; expect(profile.result.approved).toBe(expected); expect(detail.result.developer.approved).toBe(expected); expect(list.result[0].developer.approved).toBe(expected); for (const query of ["scope", "status"]) { const queue = (await ( - await get(`/extensions/v2/developers?${query}=unapproved`, moderator) + await get(`/v2/developers?${query}=unapproved`, moderator) ).json()) as { result: Array<{ id: string }> }; expect(queue.result.some((item) => item.id === "dev-developer")).toBe( !expected diff --git a/test/services/extensions/v2/developer-profiles.test.ts b/test/services/extensions/v2/developer-profiles.test.ts index a9dbaa4d..3a6830af 100644 --- a/test/services/extensions/v2/developer-profiles.test.ts +++ b/test/services/extensions/v2/developer-profiles.test.ts @@ -46,9 +46,9 @@ describe("Extensions API v2", () => { it("coalesces identical and concurrent replays without changing approval or history", async () => { const headers = await authHeaders("replay-owner"); const profile = sampleDeveloper({ id: "replay-profile" }); - expect( - (await put("/extensions/v2/developers/me", headers, profile)).status - ).toBe(200); + expect((await put("/v2/developers/me", headers, profile)).status).toBe( + 200 + ); await db .prepare( "UPDATE developers SET approved_at = CURRENT_TIMESTAMP, approved_revision = content_revision WHERE id = ?" @@ -58,7 +58,7 @@ describe("Extensions API v2", () => { const before = await getDeveloper(db, profile.id); const responses = await Promise.all( Array.from({ length: 6 }, () => - put("/extensions/v2/developers/me", headers, profile) + put("/v2/developers/me", headers, profile) ) ); expect(responses.map((r) => r.status)).toEqual(Array(6).fill(200)); @@ -69,9 +69,9 @@ describe("Extensions API v2", () => { it("budgets real edits atomically and retains the account budget after deletion", async () => { const headers = await authHeaders("budget-owner"); const profile = sampleDeveloper({ id: "budget-profile" }); - expect( - (await put("/extensions/v2/developers/me", headers, profile)).status - ).toBe(200); + expect((await put("/v2/developers/me", headers, profile)).status).toBe( + 200 + ); for (let i = 0; i < 18; i++) { await db .prepare( @@ -82,7 +82,7 @@ describe("Extensions API v2", () => { } const raced = await Promise.all( ["Last A", "Last B"].map((name) => - put("/extensions/v2/developers/me", headers, { ...profile, name }) + put("/v2/developers/me", headers, { ...profile, name }) ) ); expect(raced.filter((r) => r.status === 200)).toHaveLength(1); @@ -96,13 +96,13 @@ describe("Extensions API v2", () => { const current = await getDeveloper(db, profile.id); expect( ( - await put("/extensions/v2/developers/me", headers, { + await put("/v2/developers/me", headers, { ...profile, name: current!.name }) ).status ).toBe(200); - const denied = await put("/extensions/v2/developers/me", headers, { + const denied = await put("/v2/developers/me", headers, { ...profile, name: "Over quota" }); @@ -111,17 +111,15 @@ describe("Extensions API v2", () => { expect(await denied.json()).toMatchObject({ error: { code: "PROFILE_MUTATION_RATE_LIMITED" } }); - expect((await del("/extensions/v2/developers/me", headers)).status).toBe( - 200 + expect((await del("/v2/developers/me", headers)).status).toBe(200); + expect((await put("/v2/developers/me", headers, profile)).status).toBe( + 429 ); - expect( - (await put("/extensions/v2/developers/me", headers, profile)).status - ).toBe(429); expect(await listDeveloperHistory(db)).toHaveLength(20); expect( ( await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("independent-owner"), sampleDeveloper({ id: "independent-profile" }) ) @@ -140,9 +138,9 @@ describe("Extensions API v2", () => { .bind(`old-${i}`, profile.id, "expired-budget-owner") .run(); } - expect( - (await put("/extensions/v2/developers/me", headers, profile)).status - ).toBe(200); + expect((await put("/v2/developers/me", headers, profile)).status).toBe( + 200 + ); for (const edit of [ { name: "New name" }, { type: "organization" as const }, @@ -151,9 +149,9 @@ describe("Extensions API v2", () => { { contact_email: "new@example.com" } ]) { profile = { ...profile, ...edit }; - expect( - (await put("/extensions/v2/developers/me", headers, profile)).status - ).toBe(200); + expect((await put("/v2/developers/me", headers, profile)).status).toBe( + 200 + ); } expect(await listDeveloperHistory(db)).toHaveLength(26); expect((await getDeveloper(db, profile.id))?.content_revision).toBe(6); @@ -166,16 +164,16 @@ describe("Extensions API v2", () => { type: "user", name: "Null profile" }; - expect( - (await put("/extensions/v2/developers/me", headers, profile)).status - ).toBe(200); - expect( - (await put("/extensions/v2/developers/me", headers, profile)).status - ).toBe(200); + expect((await put("/v2/developers/me", headers, profile)).status).toBe( + 200 + ); + expect((await put("/v2/developers/me", headers, profile)).status).toBe( + 200 + ); expect(await listDeveloperHistory(db)).toHaveLength(1); expect( ( - await put("/extensions/v2/developers/me", headers, { + await put("/v2/developers/me", headers, { ...profile, URL: "https://example.com", avatar_url: "https://example.com/avatar", @@ -183,12 +181,12 @@ describe("Extensions API v2", () => { }) ).status ).toBe(200); - expect( - (await put("/extensions/v2/developers/me", headers, profile)).status - ).toBe(200); - expect( - (await put("/extensions/v2/developers/me", headers, profile)).status - ).toBe(200); + expect((await put("/v2/developers/me", headers, profile)).status).toBe( + 200 + ); + expect((await put("/v2/developers/me", headers, profile)).status).toBe( + 200 + ); expect(await listDeveloperHistory(db)).toHaveLength(3); const current = await getDeveloper(db, profile.id); expect(current?.url).toBeNull(); @@ -201,15 +199,15 @@ describe("Extensions API v2", () => { const firstHeaders = await authHeaders("rate-limited-account"); for (const id of ["attempt-one", "attempt-two", "attempt-three"]) { - const allowed = await put( - "/extensions/v2/developers/me", - firstHeaders, - { id, type: "user", name: id } - ); + const allowed = await put("/v2/developers/me", firstHeaders, { + id, + type: "user", + name: id + }); expect(allowed.status).toBe(403); } - const denied = await put("/extensions/v2/developers/me", firstHeaders, { + const denied = await put("/v2/developers/me", firstHeaders, { id: "attempt-four", type: "user", name: "Attempt four" @@ -226,7 +224,7 @@ describe("Extensions API v2", () => { expect(await listDevelopers(db)).toHaveLength(0); const otherAccount = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("independent-rate-limit-account"), { id: "other-attempt", type: "user", name: "Other attempt" } ); @@ -238,7 +236,7 @@ describe("Extensions API v2", () => { it("does not charge profile updates against creation allowance", async () => { const headers = await authHeaders("update-rate-limit-account"); const created = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", headers, sampleDeveloper({ id: "update-limit-profile" }) ); @@ -246,7 +244,7 @@ describe("Extensions API v2", () => { for (const name of ["First update", "Second update", "Third update"]) { const updated = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", headers, sampleDeveloper({ id: "update-limit-profile", name }) ); @@ -254,19 +252,19 @@ describe("Extensions API v2", () => { } expect(ghRequest).toHaveBeenCalledTimes(1); - const removed = await del("/extensions/v2/developers/me", headers); + const removed = await del("/v2/developers/me", headers); expect(removed.status).toBe(200); mockGithubEntity("Organization"); for (const id of ["remaining-one", "remaining-two"]) { - const allowed = await put("/extensions/v2/developers/me", headers, { + const allowed = await put("/v2/developers/me", headers, { id, type: "user", name: id }); expect(allowed.status).toBe(403); } - const denied = await put("/extensions/v2/developers/me", headers, { + const denied = await put("/v2/developers/me", headers, { id: "no-allowance", type: "user", name: "No allowance" @@ -278,7 +276,7 @@ describe("Extensions API v2", () => { it("creates a new developer profile, unapproved", async () => { const res = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); @@ -294,13 +292,13 @@ describe("Extensions API v2", () => { it("updates an existing profile, still unapproved", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); const res = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper({ name: "Renamed Developer" }) ); @@ -320,12 +318,12 @@ describe("Extensions API v2", () => { const headers = await authHeaders("user-1"); const [resA, resB] = await Promise.all([ put( - "/extensions/v2/developers/me", + "/v2/developers/me", headers, sampleDeveloper({ id: "developer-a" }) ), put( - "/extensions/v2/developers/me", + "/v2/developers/me", headers, sampleDeveloper({ id: "developer-b" }) ) @@ -342,13 +340,13 @@ describe("Extensions API v2", () => { it("rejects an id that already belongs to someone else", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); const res = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-2"), sampleDeveloper() ); @@ -374,7 +372,7 @@ describe("Extensions API v2", () => { }); const res = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", headers, sampleDeveloper({ id: "raced-developer" }) ); @@ -392,13 +390,13 @@ describe("Extensions API v2", () => { it("rejects changing the id on an existing profile", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); const res = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper({ id: "different-id" }) ); @@ -414,11 +412,11 @@ describe("Extensions API v2", () => { github_orgs: JSON.stringify(["acme-org"]) }); - const res = await put( - "/extensions/v2/developers/me", - await authHeaders("user-1"), - { id: "acme-org", type: "organization", name: "Acme Org" } - ); + const res = await put("/v2/developers/me", await authHeaders("user-1"), { + id: "acme-org", + type: "organization", + name: "Acme Org" + }); expect(res.status).toBe(200); const created = (await res.json()) as { @@ -437,7 +435,7 @@ describe("Extensions API v2", () => { }); const res = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); @@ -465,7 +463,7 @@ describe("Extensions API v2", () => { }); const res = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), { id: "acme-org", type: "organization", name: "Acme Org" } ); @@ -487,11 +485,11 @@ describe("Extensions API v2", () => { github_orgs_expires_at: "2099-01-01T00:00:00.000Z" }); - const res = await put( - "/extensions/v2/developers/me", - await authHeaders("user-1"), - { id: "acme-org", type: "organization", name: "Acme Org" } - ); + const res = await put("/v2/developers/me", await authHeaders("user-1"), { + id: "acme-org", + type: "organization", + name: "Acme Org" + }); expect(res.status).toBe(200); const body = (await res.json()) as { @@ -509,11 +507,11 @@ describe("Extensions API v2", () => { github_orgs_expires_at: "2099-01-01T00:00:00.000Z" }); - const res = await put( - "/extensions/v2/developers/me", - await authHeaders("user-1"), - { id: "acme-org", type: "organization", name: "Acme Org" } - ); + const res = await put("/v2/developers/me", await authHeaders("user-1"), { + id: "acme-org", + type: "organization", + name: "Acme Org" + }); expect(res.status).toBe(403); const body = (await res.json()) as { error: { code: string } }; @@ -528,16 +526,12 @@ describe("Extensions API v2", () => { github_orgs: JSON.stringify(["acme-org"]) }); - const res = await put( - "/extensions/v2/developers/me", - await authHeaders("user-1"), - { - id: "acme-org", - type: "organization", - name: "Acme Org", - URL: "https://acme.example" - } - ); + const res = await put("/v2/developers/me", await authHeaders("user-1"), { + id: "acme-org", + type: "organization", + name: "Acme Org", + URL: "https://acme.example" + }); expect(res.status).toBe(200); const created = (await res.json()) as { @@ -560,16 +554,12 @@ describe("Extensions API v2", () => { github_orgs: JSON.stringify(["acme-org"]) }); - const res = await put( - "/extensions/v2/developers/me", - await authHeaders("user-1"), - { - id: "acme-org", - type: "organization", - name: "Acme Org", - URL: "https://acme.example" - } - ); + const res = await put("/v2/developers/me", await authHeaders("user-1"), { + id: "acme-org", + type: "organization", + name: "Acme Org", + URL: "https://acme.example" + }); expect(res.status).toBe(200); const created = (await res.json()) as { @@ -588,16 +578,12 @@ describe("Extensions API v2", () => { github_orgs: JSON.stringify(["acme-org"]) }); - const res = await put( - "/extensions/v2/developers/me", - await authHeaders("user-1"), - { - id: "acme-org", - type: "organization", - name: "Acme Org", - URL: "http://acme.example/" - } - ); + const res = await put("/v2/developers/me", await authHeaders("user-1"), { + id: "acme-org", + type: "organization", + name: "Acme Org", + URL: "http://acme.example/" + }); const created = (await res.json()) as { result: { github_url_verified?: boolean }; @@ -613,16 +599,12 @@ describe("Extensions API v2", () => { github_orgs: JSON.stringify(["acme-org"]) }); - const res = await put( - "/extensions/v2/developers/me", - await authHeaders("user-1"), - { - id: "acme-org", - type: "organization", - name: "Acme Org", - URL: "https://acme.example" - } - ); + const res = await put("/v2/developers/me", await authHeaders("user-1"), { + id: "acme-org", + type: "organization", + name: "Acme Org", + URL: "https://acme.example" + }); const created = (await res.json()) as { result: { github_url_verified?: boolean }; @@ -638,16 +620,12 @@ describe("Extensions API v2", () => { github_orgs: JSON.stringify(["acme-org"]) }); - const res = await put( - "/extensions/v2/developers/me", - await authHeaders("user-1"), - { - id: "acme-org", - type: "organization", - name: "Acme Org", - URL: "https://acme.example/docs" - } - ); + const res = await put("/v2/developers/me", await authHeaders("user-1"), { + id: "acme-org", + type: "organization", + name: "Acme Org", + URL: "https://acme.example/docs" + }); const created = (await res.json()) as { result: { github_url_verified?: boolean }; @@ -663,11 +641,11 @@ describe("Extensions API v2", () => { github_orgs: JSON.stringify(["some-other-org"]) }); - const res = await put( - "/extensions/v2/developers/me", - await authHeaders("user-1"), - { id: "acme-org", type: "organization", name: "Acme Org" } - ); + const res = await put("/v2/developers/me", await authHeaders("user-1"), { + id: "acme-org", + type: "organization", + name: "Acme Org" + }); expect(res.status).toBe(403); const body = (await res.json()) as { error: { code: string } }; @@ -683,11 +661,11 @@ describe("Extensions API v2", () => { github_orgs: JSON.stringify(["acme-org"]) }); - const res = await put( - "/extensions/v2/developers/me", - await authHeaders("user-1"), - { id: "acme-org", type: "user", name: "Acme Org" } - ); + const res = await put("/v2/developers/me", await authHeaders("user-1"), { + id: "acme-org", + type: "user", + name: "Acme Org" + }); expect(res.status).toBe(403); const body = (await res.json()) as { error: { code: string } }; @@ -713,7 +691,7 @@ describe("Extensions API v2", () => { ); const res = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), { id: "unavailable-dev", type: "user", name: "Unavailable" } ); @@ -737,7 +715,7 @@ describe("Extensions API v2", () => { ); const res = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), { id: "unavailable-dev", type: "user", name: "Unavailable" } ); @@ -759,7 +737,7 @@ describe("Extensions API v2", () => { ); const res = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), { id: "invalid-response", type: "user", name: "Invalid" } ); @@ -776,11 +754,11 @@ describe("Extensions API v2", () => { async () => ({ data: { type: "Bot", blog: null } }) ); - const res = await put( - "/extensions/v2/developers/me", - await authHeaders("user-1"), - { id: "unsupported-entity", type: "user", name: "Unsupported" } - ); + const res = await put("/v2/developers/me", await authHeaders("user-1"), { + id: "unsupported-entity", + type: "user", + name: "Unsupported" + }); expect(res.status).toBe(422); const body = (await res.json()) as { error: { code: string } }; @@ -792,11 +770,11 @@ describe("Extensions API v2", () => { mockGithubEntity("Organization"); // No row in users for user-1 — never linked GitHub. - const res = await put( - "/extensions/v2/developers/me", - await authHeaders("user-1"), - { id: "acme-org", type: "organization", name: "Acme Org" } - ); + const res = await put("/v2/developers/me", await authHeaders("user-1"), { + id: "acme-org", + type: "organization", + name: "Acme Org" + }); expect(res.status).toBe(200); const created = (await res.json()) as { @@ -813,11 +791,11 @@ describe("Extensions API v2", () => { } }); - const res = await put( - "/extensions/v2/developers/me", - await authHeaders("user-1"), - { id: "acme-org", type: "organization", name: "Acme Org" } - ); + const res = await put("/v2/developers/me", await authHeaders("user-1"), { + id: "acme-org", + type: "organization", + name: "Acme Org" + }); expect(res.status).toBe(500); env.DB_EXTENSIONS = db; @@ -828,7 +806,7 @@ describe("Extensions API v2", () => { "rejects the reserved id %s", async (id) => { const res = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper({ id }) ); @@ -839,13 +817,13 @@ describe("Extensions API v2", () => { it("clears approval when an approved profile is edited", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); await insertUser(db, { id: "mod-1", is_moderator: 1 }); const approved = await post( - "/extensions/v2/developers/dev-developer/approve", + "/v2/developers/dev-developer/approve", await authHeaders("mod-1"), await developerApproval("dev-developer", 1) ); @@ -856,7 +834,7 @@ describe("Extensions API v2", () => { expect(approvedBody.result.approved).toBe(true); const res = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper({ name: "Edited Again" }) ); @@ -874,7 +852,7 @@ describe("Extensions API v2", () => { github_orgs: JSON.stringify([]) }); const created = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); @@ -885,14 +863,14 @@ describe("Extensions API v2", () => { await insertUser(db, { id: "mod-1", is_moderator: 1 }); const approved = await post( - "/extensions/v2/developers/dev-developer/approve", + "/v2/developers/dev-developer/approve", await authHeaders("mod-1"), await developerApproval("dev-developer", 1) ); expect(approved.status).toBe(200); const res = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper({ name: "Edited Again" }) ); @@ -913,7 +891,7 @@ describe("Extensions API v2", () => { github_orgs: JSON.stringify([]) }); const created = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); @@ -924,16 +902,15 @@ describe("Extensions API v2", () => { await insertUser(db, { id: "mod-1", is_moderator: 1 }); await post( - "/extensions/v2/developers/dev-developer/approve", + "/v2/developers/dev-developer/approve", await authHeaders("mod-1"), await developerApproval("dev-developer", 1) ); - const res = await put( - "/extensions/v2/developers/me", - await authHeaders("user-1"), - { ...sampleDeveloper(), type: "organization" } - ); + const res = await put("/v2/developers/me", await authHeaders("user-1"), { + ...sampleDeveloper(), + type: "organization" + }); expect(res.status).toBe(200); const data = (await res.json()) as { @@ -956,7 +933,7 @@ describe("Extensions API v2", () => { github_orgs: JSON.stringify([]) }); const created = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), { ...sampleDeveloper(), URL: "https://acme.example" } ); @@ -969,11 +946,10 @@ describe("Extensions API v2", () => { expect(createdBody.result.github_org_verified).toBe(true); expect(createdBody.result.github_url_verified).toBe(true); - const res = await put( - "/extensions/v2/developers/me", - await authHeaders("user-1"), - { ...sampleDeveloper(), URL: "https://different.example" } - ); + const res = await put("/v2/developers/me", await authHeaders("user-1"), { + ...sampleDeveloper(), + URL: "https://different.example" + }); expect(res.status).toBe(200); const data = (await res.json()) as { @@ -988,7 +964,7 @@ describe("Extensions API v2", () => { it("does not update a profile after ownership changes mid-request", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); @@ -1009,7 +985,7 @@ describe("Extensions API v2", () => { }); const raced = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper({ name: "Former owner write" }) ); @@ -1031,7 +1007,7 @@ describe("Extensions API v2", () => { it("reports an inactive account when deactivated during an existing profile update", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); @@ -1053,7 +1029,7 @@ describe("Extensions API v2", () => { }); const res = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper({ name: "Inactive owner write" }) ); @@ -1083,7 +1059,7 @@ describe("Extensions API v2", () => { }); const res = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", headers, sampleDeveloper({ id: "deleted-during-write-profile" }) ); @@ -1101,7 +1077,7 @@ describe("Extensions API v2", () => { it("round-trips avatar_url and contact_email", async () => { const headers = await authHeaders("user-1"); - const res = await put("/extensions/v2/developers/me", headers, { + const res = await put("/v2/developers/me", headers, { ...sampleDeveloper(), avatar_url: "https://example.com/avatar.png", contact_email: "dev@example.com" @@ -1124,13 +1100,13 @@ describe("Extensions API v2", () => { it("updates avatar_url and contact_email on an existing profile", async () => { const headers = await authHeaders("user-1"); - await put("/extensions/v2/developers/me", headers, { + await put("/v2/developers/me", headers, { ...sampleDeveloper(), avatar_url: "https://example.com/old.png", contact_email: "old@example.com" }); - const res = await put("/extensions/v2/developers/me", headers, { + const res = await put("/v2/developers/me", headers, { ...sampleDeveloper(), avatar_url: "https://example.com/new.png", contact_email: "new@example.com" @@ -1153,7 +1129,7 @@ describe("Extensions API v2", () => { it("accepts a payload without avatar_url or contact_email", async () => { const res = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); @@ -1174,11 +1150,10 @@ describe("Extensions API v2", () => { // the caller. Unknown keys report at the root path - the body has no // nesting. it("rejects an unknown field in the profile body", async () => { - const res = await put( - "/extensions/v2/developers/me", - await authHeaders("user-1"), - { ...sampleDeveloper(), approved: true } - ); + const res = await put("/v2/developers/me", await authHeaders("user-1"), { + ...sampleDeveloper(), + approved: true + }); expect(res.status).toBe(422); const body = (await res.json()) as { @@ -1196,7 +1171,7 @@ describe("Extensions API v2", () => { describe("DELETE /developers/me", () => { it("deletes a profile with no extensions", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); @@ -1207,17 +1182,14 @@ describe("Extensions API v2", () => { .bind("modules-publisher", "dev-developer") .run(); - const res = await del( - "/extensions/v2/developers/me", - await authHeaders("user-1") - ); + const res = await del("/v2/developers/me", await authHeaders("user-1")); expect(res.status).toBe(200); const body = (await res.json()) as { result: { id: string; deleted: boolean }; }; expect(body.result).toEqual({ id: "dev-developer", deleted: true }); - const getRes = await get("/extensions/v2/developers/dev-developer", {}); + const getRes = await get("/v2/developers/dev-developer", {}); expect(getRes.status).toBe(404); const mapping = await db .prepare("SELECT client_id FROM m2m_publisher_clients") @@ -1232,7 +1204,7 @@ describe("Extensions API v2", () => { // that the profile survives, and that the batch is all-or-nothing. it("does not delete a profile whose ownership moved after it was resolved", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("owner-before"), sampleDeveloper({ id: "raced-delete" }) ); @@ -1252,7 +1224,7 @@ describe("Extensions API v2", () => { }); const res = await del( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("owner-before") ); env.DB_EXTENSIONS = db; @@ -1270,7 +1242,7 @@ describe("Extensions API v2", () => { it("leaves pending transfers intact when the profile delete is blocked", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("owner-before"), sampleDeveloper({ id: "raced-delete" }) ); @@ -1295,7 +1267,7 @@ describe("Extensions API v2", () => { }); const res = await del( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("owner-before") ); env.DB_EXTENSIONS = db; @@ -1308,7 +1280,7 @@ describe("Extensions API v2", () => { it("404s for a caller with no developer profile", async () => { const res = await del( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("no-profile-user") ); expect(res.status).toBe(404); @@ -1319,10 +1291,7 @@ describe("Extensions API v2", () => { it("409s when the profile still has published extensions", async () => { await seedOwnedExtension(); - const res = await del( - "/extensions/v2/developers/me", - await authHeaders("owner-1") - ); + const res = await del("/v2/developers/me", await authHeaders("owner-1")); expect(res.status).toBe(409); const body = (await res.json()) as { error: { code: string; message: string }; @@ -1333,24 +1302,21 @@ describe("Extensions API v2", () => { it("409s when an unpublished extension is still owned", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); expect( ( await post( - "/extensions/v2/extensions", + "/v2/extensions", await authHeaders("user-1"), sampleCreate() ) ).status ).toBe(201); - const res = await del( - "/extensions/v2/developers/me", - await authHeaders("user-1") - ); + const res = await del("/v2/developers/me", await authHeaders("user-1")); expect(res.status).toBe(409); const body = (await res.json()) as { error: { code: string } }; expect(body.error.code).toBe("CONFLICT"); @@ -1358,12 +1324,12 @@ describe("Extensions API v2", () => { it("removes transfer tokens and claims but keeps history", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); await post( - "/extensions/v2/developers/dev-developer/transfer", + "/v2/developers/dev-developer/transfer", await authHeaders("user-1") ); await insertDeveloperClaim(db, { @@ -1376,10 +1342,7 @@ describe("Extensions API v2", () => { reviewed_at: new Date().toISOString() }); - const res = await del( - "/extensions/v2/developers/me", - await authHeaders("user-1") - ); + const res = await del("/v2/developers/me", await authHeaders("user-1")); expect(res.status).toBe(200); expect( @@ -1401,7 +1364,7 @@ describe("Extensions API v2", () => { it("refuses to delete if ownership moves away between the lookup and the delete", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); @@ -1420,17 +1383,11 @@ describe("Extensions API v2", () => { } }); - const res = await del( - "/extensions/v2/developers/me", - await authHeaders("user-1") - ); + const res = await del("/v2/developers/me", await authHeaders("user-1")); env.DB_EXTENSIONS = db; expect(res.status).toBe(404); - const stillThere = await get( - "/extensions/v2/developers/dev-developer", - {} - ); + const stillThere = await get("/v2/developers/dev-developer", {}); expect(stillThere.status).toBe(200); const body = (await stillThere.json()) as { result: { id: string } }; expect(body.result.id).toBe("dev-developer"); @@ -1438,7 +1395,7 @@ describe("Extensions API v2", () => { it("reports an inactive owner when the account is deactivated during deletion", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); @@ -1454,10 +1411,7 @@ describe("Extensions API v2", () => { } }); - const res = await del( - "/extensions/v2/developers/me", - await authHeaders("user-1") - ); + const res = await del("/v2/developers/me", await authHeaders("user-1")); env.DB_EXTENSIONS = db; expect(deactivated).toBe(true); @@ -1497,7 +1451,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/me/reverify?check_url=true", + "/v2/developers/me/reverify?check_url=true", await authHeaders("user-1") ); env.DB_EXTENSIONS = db; @@ -1528,7 +1482,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/me/reverify", + "/v2/developers/me/reverify", await authHeaders("user-1") ); expect(res.status).toBe(200); @@ -1570,7 +1524,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/me/reverify", + "/v2/developers/me/reverify", await authHeaders("user-1") ); expect(res.status).toBe(200); @@ -1605,7 +1559,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/me/reverify", + "/v2/developers/me/reverify", await authHeaders("user-1") ); expect(res.status).toBe(200); @@ -1636,7 +1590,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/me/reverify", + "/v2/developers/me/reverify", await authHeaders("user-1") ); expect(res.status).toBe(200); @@ -1669,7 +1623,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/me/reverify", + "/v2/developers/me/reverify", await authHeaders("user-1") ); expect(res.status).toBe(200); @@ -1695,7 +1649,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/me/reverify", + "/v2/developers/me/reverify", await authHeaders("user-1") ); expect(res.status).toBe(200); @@ -1722,7 +1676,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/me/reverify?check_url=true", + "/v2/developers/me/reverify?check_url=true", await authHeaders("user-1") ); expect(res.status).toBe(200); @@ -1761,7 +1715,7 @@ describe("Extensions API v2", () => { ); const failed = await post( - "/extensions/v2/developers/me/reverify?check_url=true", + "/v2/developers/me/reverify?check_url=true", await authHeaders("user-1") ); expect(failed.status).toBe(expectedStatus); @@ -1773,7 +1727,7 @@ describe("Extensions API v2", () => { vi.clearAllMocks(); const retry = await post( - "/extensions/v2/developers/me/reverify?check_url=true", + "/v2/developers/me/reverify?check_url=true", await authHeaders("user-1") ); expect(retry.status).toBe(429); @@ -1797,14 +1751,14 @@ describe("Extensions API v2", () => { }); const first = await post( - "/extensions/v2/developers/me/reverify?check_url=true", + "/v2/developers/me/reverify?check_url=true", await authHeaders("user-1") ); expect(first.status).toBe(200); vi.clearAllMocks(); const second = await post( - "/extensions/v2/developers/me/reverify?check_url=true", + "/v2/developers/me/reverify?check_url=true", await authHeaders("user-1") ); expect(second.status).toBe(429); @@ -1829,11 +1783,11 @@ describe("Extensions API v2", () => { }); const first = await post( - "/extensions/v2/developers/me/reverify", + "/v2/developers/me/reverify", await authHeaders("user-1") ); const second = await post( - "/extensions/v2/developers/me/reverify", + "/v2/developers/me/reverify", await authHeaders("user-1") ); expect(first.status).toBe(200); @@ -1868,11 +1822,11 @@ describe("Extensions API v2", () => { }); const first = await post( - "/extensions/v2/developers/me/reverify?check_url=true", + "/v2/developers/me/reverify?check_url=true", await authHeaders("user-1") ); const second = await post( - "/extensions/v2/developers/me/reverify?check_url=true", + "/v2/developers/me/reverify?check_url=true", await authHeaders("user-2") ); expect(first.status).toBe(200); @@ -1896,8 +1850,8 @@ describe("Extensions API v2", () => { const headers = await authHeaders("user-1"); const [first, second] = await Promise.all([ - post("/extensions/v2/developers/me/reverify?check_url=true", headers), - post("/extensions/v2/developers/me/reverify?check_url=true", headers) + post("/v2/developers/me/reverify?check_url=true", headers), + post("/v2/developers/me/reverify?check_url=true", headers) ]); const statuses = [first.status, second.status].sort(); @@ -1933,7 +1887,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/me/reverify?check_url=true", + "/v2/developers/me/reverify?check_url=true", await authHeaders("user-1") ); env.DB_EXTENSIONS = db; @@ -1966,7 +1920,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/me/reverify?check_url=true", + "/v2/developers/me/reverify?check_url=true", await authHeaders("user-1") ); expect(res.status).toBe(200); @@ -1998,7 +1952,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/me/reverify", + "/v2/developers/me/reverify", await authHeaders("user-1") ); expect(res.status).toBe(200); @@ -2035,7 +1989,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/me/reverify?check_url=true", + "/v2/developers/me/reverify?check_url=true", await authHeaders("user-1") ); expect(res.status).toBe(200); @@ -2074,7 +2028,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/me/reverify?check_url=true", + "/v2/developers/me/reverify?check_url=true", await authHeaders("user-1") ); expect(res.status).toBe(200); @@ -2100,7 +2054,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/me/reverify?check_url=false", + "/v2/developers/me/reverify?check_url=false", await authHeaders("user-1") ); expect(res.status).toBe(200); @@ -2113,7 +2067,7 @@ describe("Extensions API v2", () => { it("404s when the caller doesn't own a developer profile", async () => { const res = await post( - "/extensions/v2/developers/me/reverify", + "/v2/developers/me/reverify", await authHeaders("user-1") ); expect(res.status).toBe(404); @@ -2144,7 +2098,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/me/reverify", + "/v2/developers/me/reverify", await authHeaders("user-1") ); env.DB_EXTENSIONS = db; @@ -2185,7 +2139,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/me/reverify", + "/v2/developers/me/reverify", await authHeaders("user-1") ); env.DB_EXTENSIONS = db; @@ -2240,7 +2194,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/me/reverify", + "/v2/developers/me/reverify", await authHeaders("user-1") ); env.DB_EXTENSIONS = db; @@ -2266,7 +2220,7 @@ describe("Extensions API v2", () => { approved_at: new Date().toISOString() }); - const res = await get("/extensions/v2/developers/public-dev", {}); + const res = await get("/v2/developers/public-dev", {}); expect(res.status).toBe(200); const body = (await res.json()) as { result: Record }; expect(body.result).toEqual({ @@ -2282,14 +2236,14 @@ describe("Extensions API v2", () => { }); it("404s for an unknown developer", async () => { - const res = await get("/extensions/v2/developers/no-such-developer", {}); + const res = await get("/v2/developers/no-such-developer", {}); expect(res.status).toBe(404); }); it("marks an unowned developer as unclaimed", async () => { await seedUnownedDeveloper("legacy-public"); - const res = await get("/extensions/v2/developers/legacy-public", {}); + const res = await get("/v2/developers/legacy-public", {}); expect(res.status).toBe(200); expect(await res.json()).toMatchObject({ result: { id: "legacy-public", unclaimed: true } @@ -2311,7 +2265,7 @@ describe("Extensions API v2", () => { .run(); const res = await get( - "/extensions/v2/developers/owned-dev", + "/v2/developers/owned-dev", await authHeaders("user-1") ); expect(res.status).toBe(200); diff --git a/test/services/extensions/v2/extension-writes.test.ts b/test/services/extensions/v2/extension-writes.test.ts index 73103c82..97dfb147 100644 --- a/test/services/extensions/v2/extension-writes.test.ts +++ b/test/services/extensions/v2/extension-writes.test.ts @@ -55,7 +55,7 @@ async function createExtension( overrides?: { extensionId?: string; name?: string } ) { return post( - "/extensions/v2/extensions", + "/v2/extensions", await authHeaders(user), sampleCreate(overrides) ); @@ -65,7 +65,7 @@ describe("Extensions API v2 writes", () => { describe("POST /extensions", () => { it("requires auth", async () => { const res = await post( - "/extensions/v2/extensions", + "/v2/extensions", { "Content-Type": "application/json" }, sampleCreate() ); @@ -73,11 +73,7 @@ describe("Extensions API v2 writes", () => { }); it("rejects an invalid body", async () => { - const res = await post( - "/extensions/v2/extensions", - await authHeaders("user-1"), - {} - ); + const res = await post("/v2/extensions", await authHeaders("user-1"), {}); expect(res.status).toBe(422); const data = (await res.json()) as { error: { code: string } }; expect(data.error.code).toBe("VALIDATION_ERROR"); @@ -85,38 +81,29 @@ describe("Extensions API v2 writes", () => { it("rejects the legacy license object", async () => { await seedDeveloper("new-developer", "user-1"); - const res = await post( - "/extensions/v2/extensions", - await authHeaders("user-1"), - { - ...sampleCreate(), - license: { name: "Not Real", spdx_id: "NOT-A-REAL-SPDX-ID" } - } - ); + const res = await post("/v2/extensions", await authHeaders("user-1"), { + ...sampleCreate(), + license: { name: "Not Real", spdx_id: "NOT-A-REAL-SPDX-ID" } + }); expect(res.status).toBe(422); expect(await countRevisions(db)).toBe(0); }); it("rejects characters outside the manifest license grammar", async () => { await seedDeveloper("new-developer", "user-1"); - const res = await post( - "/extensions/v2/extensions", - await authHeaders("user-1"), - { ...sampleCreate(), license: "invalid/license" } - ); + const res = await post("/v2/extensions", await authHeaders("user-1"), { + ...sampleCreate(), + license: "invalid/license" + }); expect(res.status).toBe(422); }); it("stores a manifest license expression", async () => { await seedDeveloper("new-developer", "user-1"); - const res = await post( - "/extensions/v2/extensions", - await authHeaders("user-1"), - { - ...sampleCreate(), - license: "Apache-2.0 OR MIT" - } - ); + const res = await post("/v2/extensions", await authHeaders("user-1"), { + ...sampleCreate(), + license: "Apache-2.0 OR MIT" + }); expect(res.status).toBe(201); const data = (await res.json()) as { result: { revision_id: string } }; const revision = await getRevision(db, data.result.revision_id); @@ -125,11 +112,10 @@ describe("Extensions API v2 writes", () => { it("accepts a custom manifest license", async () => { await seedDeveloper("new-developer", "user-1"); - const res = await post( - "/extensions/v2/extensions", - await authHeaders("user-1"), - { ...sampleCreate(), license: "LicenseRef-Acme" } - ); + const res = await post("/v2/extensions", await authHeaders("user-1"), { + ...sampleCreate(), + license: "LicenseRef-Acme" + }); expect(res.status).toBe(201); }); @@ -222,7 +208,7 @@ describe("Extensions API v2 writes", () => { // The readme's own bound, which fires at ~100 KB. Asserted by path so it // cannot quietly become the reason some other case passes. - const oversizedReadme = await post("/extensions/v2/extensions", headers, { + const oversizedReadme = await post("/v2/extensions", headers, { ...body, readme: "x".repeat(100_001) }); @@ -236,7 +222,7 @@ describe("Extensions API v2 writes", () => { ]) ); - const unknownField = await post("/extensions/v2/extensions", headers, { + const unknownField = await post("/v2/extensions", headers, { ...body, padding: "x" }); @@ -250,11 +236,10 @@ describe("Extensions API v2 writes", () => { ]) ); - const unknownReleaseField = await post( - "/extensions/v2/extensions", - headers, - { ...body, releases: [{ ...body.releases[0], padding: "x" }] } - ); + const unknownReleaseField = await post("/v2/extensions", headers, { + ...body, + releases: [{ ...body.releases[0], padding: "x" }] + }); expect(unknownReleaseField.status).toBe(422); const unknownReleaseBody = (await unknownReleaseField.json()) as { error: { details: Array<{ code: string; path: PropertyKey[] }> }; @@ -268,7 +253,7 @@ describe("Extensions API v2 writes", () => { ]) ); - const tooManyReleases = await post("/extensions/v2/extensions", headers, { + const tooManyReleases = await post("/v2/extensions", headers, { ...body, releases: Array.from({ length: 101 }, () => body.releases[0]) }); @@ -298,7 +283,7 @@ describe("Extensions API v2 writes", () => { new TextEncoder().encode(JSON.stringify(body)).byteLength ).toBeGreaterThan(256 * 1024); - const created = await post("/extensions/v2/extensions", headers, body); + const created = await post("/v2/extensions", headers, body); expect(created.status).toBe(422); const detail = (await created.json()) as { error: { details: Array<{ code: string; message: string }> }; @@ -317,7 +302,7 @@ describe("Extensions API v2 writes", () => { await seedOwnedExtension(); const { id: _id, ...content } = body; const edited = await put( - "/extensions/v2/extensions/existing-ext", + "/v2/extensions/existing-ext", await authHeaders("owner-1"), content ); @@ -361,7 +346,7 @@ describe("Extensions API v2 writes", () => { describe("PUT /extensions/{id}", () => { it("requires auth", async () => { const res = await put( - "/extensions/v2/extensions/existing-ext", + "/v2/extensions/existing-ext", { "Content-Type": "application/json" }, sampleContent() ); @@ -370,7 +355,7 @@ describe("Extensions API v2 writes", () => { it("reports an unknown extension as 404, not 403", async () => { const res = await put( - "/extensions/v2/extensions/no-such-ext", + "/v2/extensions/no-such-ext", await authHeaders("user-1"), sampleContent() ); @@ -380,7 +365,7 @@ describe("Extensions API v2 writes", () => { it("rejects an edit from someone who does not own the extension", async () => { await seedOwnedExtension(); const res = await put( - "/extensions/v2/extensions/existing-ext", + "/v2/extensions/existing-ext", await authHeaders("intruder"), sampleContent() ); @@ -392,7 +377,7 @@ describe("Extensions API v2 writes", () => { it("accepts an edit from the owner without changing the published content", async () => { await seedOwnedExtension(); const res = await put( - "/extensions/v2/extensions/existing-ext", + "/v2/extensions/existing-ext", await authHeaders("owner-1"), sampleContent({ name: "Renamed" }) ); @@ -415,17 +400,12 @@ describe("Extensions API v2 writes", () => { await seedOwnedExtension(); const headers = await authHeaders("owner-1"); expect( - ( - await put( - "/extensions/v2/extensions/existing-ext", - headers, - sampleContent() - ) - ).status + (await put("/v2/extensions/existing-ext", headers, sampleContent())) + .status ).toBe(202); const second = await put( - "/extensions/v2/extensions/existing-ext", + "/v2/extensions/existing-ext", headers, sampleContent({ name: "Again" }) ); @@ -436,7 +416,7 @@ describe("Extensions API v2 writes", () => { it("cannot rename an extension: the id comes from the path", async () => { await seedOwnedExtension(); const res = await put( - "/extensions/v2/extensions/existing-ext", + "/v2/extensions/existing-ext", await authHeaders("owner-1"), { ...sampleContent(), id: "renamed" } ); @@ -471,11 +451,7 @@ describe("Extensions API v2 writes", () => { const headers = await authHeaders("user-1"); deactivateBefore("INSERT INTO extensions", "user-1"); - const res = await post( - "/extensions/v2/extensions", - headers, - sampleCreate() - ); + const res = await post("/v2/extensions", headers, sampleCreate()); expect(res.status).toBe(403); await expect(res.json()).resolves.toMatchObject({ @@ -491,7 +467,7 @@ describe("Extensions API v2 writes", () => { deactivateBefore("extension_revisions", "owner-1"); const res = await put( - "/extensions/v2/extensions/existing-ext", + "/v2/extensions/existing-ext", headers, sampleContent() ); @@ -508,7 +484,7 @@ describe("Extensions API v2 writes", () => { it("accepts an edit addressed in lower case", async () => { await seedMixedCaseExtension(); const res = await put( - "/extensions/v2/extensions/existing-ext", + "/v2/extensions/existing-ext", await authHeaders("owner-1"), sampleContent({ name: "Renamed" }) ); @@ -522,13 +498,13 @@ describe("Extensions API v2 writes", () => { it("lists revisions addressed in lower case", async () => { await seedMixedCaseExtension(); await put( - "/extensions/v2/extensions/existing-ext", + "/v2/extensions/existing-ext", await authHeaders("owner-1"), sampleContent() ); const res = await get( - "/extensions/v2/extensions/existing-ext/revisions", + "/v2/extensions/existing-ext/revisions", await authHeaders("owner-1") ); expect(res.status).toBe(200); @@ -546,15 +522,13 @@ describe("Extensions API v2 writes", () => { const owner = await authHeaders("owner-1"); const mod = await authHeaders("mod-1"); - expect( - (await get("/extensions/v2/extensions/existing-EXT", {})).status - ).toBe(200); - expect( - (await get("/extensions/v2/extensions/EXISTING-ext", owner)).status - ).toBe(200); + expect((await get("/v2/extensions/existing-EXT", {})).status).toBe(200); + expect((await get("/v2/extensions/EXISTING-ext", owner)).status).toBe( + 200 + ); const edit = await put( - "/extensions/v2/extensions/existing-ext", + "/v2/extensions/existing-ext", owner, sampleContent({ name: "Renamed" }) ); @@ -562,13 +536,12 @@ describe("Extensions API v2 writes", () => { const first = (await edit.json()) as { result: { revision_id: string } }; expect( - (await get("/extensions/v2/extensions/EXISTING-EXT/revisions", owner)) - .status + (await get("/v2/extensions/EXISTING-EXT/revisions", owner)).status ).toBe(200); expect( ( await post( - `/extensions/v2/extensions/existing-EXT/revisions/${first.result.revision_id}/reject`, + `/v2/extensions/existing-EXT/revisions/${first.result.revision_id}/reject`, mod, { review_note: "no" } ) @@ -576,7 +549,7 @@ describe("Extensions API v2 writes", () => { ).toBe(200); const retry = await put( - "/extensions/v2/extensions/EXISTING-ext", + "/v2/extensions/EXISTING-ext", owner, sampleContent({ name: "Second" }) ); @@ -586,7 +559,7 @@ describe("Extensions API v2 writes", () => { expect( ( await post( - `/extensions/v2/extensions/existing-ext/revisions/${second.result.revision_id}/approve`, + `/v2/extensions/existing-ext/revisions/${second.result.revision_id}/approve`, mod, {} ) @@ -605,7 +578,7 @@ describe("Extensions API v2 writes", () => { await createExtension("user-1", { extensionId: "new-ext" }); const res = await del( - "/extensions/v2/extensions/NEW-EXT", + "/v2/extensions/NEW-EXT", await authHeaders("user-1") ); expect(res.status).toBe(200); @@ -619,7 +592,7 @@ describe("Extensions API v2 writes", () => { expect((await createExtension("user-1")).status).toBe(201); const res = await del( - "/extensions/v2/extensions/new-ext", + "/v2/extensions/new-ext", await authHeaders("user-1") ); @@ -634,7 +607,7 @@ describe("Extensions API v2 writes", () => { it("refuses to withdraw a published extension", async () => { await seedOwnedExtension(); const res = await del( - "/extensions/v2/extensions/existing-ext", + "/v2/extensions/existing-ext", await authHeaders("owner-1") ); @@ -660,7 +633,7 @@ describe("Extensions API v2 writes", () => { } }); - const res = await del("/extensions/v2/extensions/new-ext", headers); + const res = await del("/v2/extensions/new-ext", headers); expect(res.status).toBe(403); await expect(res.json()).resolves.toMatchObject({ @@ -690,7 +663,7 @@ describe("Extensions API v2 writes", () => { }); // Published *and* deactivated: the 409 branch would otherwise win. - const res = await del("/extensions/v2/extensions/existing-ext", headers); + const res = await del("/v2/extensions/existing-ext", headers); expect(res.status).toBe(403); await expect(res.json()).resolves.toMatchObject({ @@ -704,7 +677,7 @@ describe("Extensions API v2 writes", () => { await createExtension("user-1"); const res = await del( - "/extensions/v2/extensions/new-ext", + "/v2/extensions/new-ext", await authHeaders("intruder") ); @@ -719,13 +692,13 @@ describe("Extensions API v2 writes", () => { const headers = await authHeaders("owner-1"); expect( ( - await post("/extensions/v2/extensions", headers, { + await post("/v2/extensions", headers, { ...sampleCreate({ extensionId: "draft-ext" }) }) ).status ).toBe(201); - const res = await get("/extensions/v2/extensions?scope=mine", headers); + const res = await get("/v2/extensions?scope=mine", headers); expect(res.status).toBe(200); const data = (await res.json()) as { result: Array<{ @@ -752,12 +725,12 @@ describe("Extensions API v2 writes", () => { await seedOwnedExtension(); const headers = await authHeaders("owner-1"); await put( - "/extensions/v2/extensions/existing-ext", + "/v2/extensions/existing-ext", headers, sampleContent({ name: "Renamed" }) ); - const res = await get("/extensions/v2/extensions?scope=mine", headers); + const res = await get("/v2/extensions?scope=mine", headers); const data = (await res.json()) as { result: Array<{ published: { name: string } | null; @@ -774,7 +747,7 @@ describe("Extensions API v2 writes", () => { await seedOwnedExtension(); const res = await get( - "/extensions/v2/extensions?scope=mine", + "/v2/extensions?scope=mine", await authHeaders("owner-1") ); const data = (await res.json()) as { @@ -799,18 +772,18 @@ describe("Extensions API v2 writes", () => { result: { revision_id: string }; }; await post( - `/extensions/v2/extensions/new-ext/revisions/${result.revision_id}/reject`, + `/v2/extensions/new-ext/revisions/${result.revision_id}/reject`, await authHeaders("mod-1"), { review_note: "no" } ); await put( - "/extensions/v2/extensions/new-ext", + "/v2/extensions/new-ext", await authHeaders("user-1"), sampleContent({ name: "Fixed" }) ); const res = await get( - "/extensions/v2/extensions?scope=mine", + "/v2/extensions?scope=mine", await authHeaders("user-1") ); await expect(res.json()).resolves.toMatchObject({ @@ -830,7 +803,7 @@ describe("Extensions API v2 writes", () => { await createExtension("user-2", { extensionId: "other-ext" }); const res = await get( - "/extensions/v2/extensions?scope=mine", + "/v2/extensions?scope=mine", await authHeaders("owner-1") ); const data = (await res.json()) as { result: Array<{ id: string }> }; @@ -850,7 +823,7 @@ describe("Extensions API v2 writes", () => { result: { revision_id: string }; }; await post( - `/extensions/v2/extensions/live-ext/revisions/${liveIds.result.revision_id}/approve`, + `/v2/extensions/live-ext/revisions/${liveIds.result.revision_id}/approve`, await authHeaders("mod-1"), {} ); @@ -866,15 +839,12 @@ describe("Extensions API v2 writes", () => { result: { revision_id: string }; }; await post( - `/extensions/v2/extensions/rejected-ext/revisions/${rejectedIds.result.revision_id}/reject`, + `/v2/extensions/rejected-ext/revisions/${rejectedIds.result.revision_id}/reject`, await authHeaders("mod-1"), { review_note: "no" } ); - const res = await get( - "/extensions/v2/extensions?scope=mine&type=module", - headers - ); + const res = await get("/v2/extensions?scope=mine&type=module", headers); expect(res.status).toBe(200); const data = (await res.json()) as { result: Array<{ id: string }> }; expect(data.result.map((item) => item.id)).toEqual([ @@ -883,21 +853,18 @@ describe("Extensions API v2 writes", () => { "rejected-ext" ]); - const other = await get( - "/extensions/v2/extensions?scope=mine&type=theme", - headers - ); + const other = await get("/v2/extensions?scope=mine&type=theme", headers); await expect(other.json()).resolves.toMatchObject({ result: [] }); }); it("requires auth", async () => { - const res = await get("/extensions/v2/extensions?scope=mine", {}); + const res = await get("/v2/extensions?scope=mine", {}); expect(res.status).toBe(401); }); it("identifies invalid cursors", async () => { const res = await get( - "/extensions/v2/extensions?scope=mine&cursor=not-a-cursor", + "/v2/extensions?scope=mine&cursor=not-a-cursor", await authHeaders("user-1") ); expect(res.status).toBe(422); @@ -915,10 +882,7 @@ describe("Extensions API v2 writes", () => { ); } - const first = await get( - "/extensions/v2/extensions?scope=mine&limit=2", - headers - ); + const first = await get("/v2/extensions?scope=mine&limit=2", headers); const firstBody = (await first.json()) as { result: Array<{ id: string }>; pagination: { has_more: boolean; next_cursor: string }; @@ -930,7 +894,7 @@ describe("Extensions API v2 writes", () => { expect(firstBody.pagination.has_more).toBe(true); const second = await get( - `/extensions/v2/extensions?scope=mine&limit=2&cursor=${encodeURIComponent(firstBody.pagination.next_cursor)}`, + `/v2/extensions?scope=mine&limit=2&cursor=${encodeURIComponent(firstBody.pagination.next_cursor)}`, headers ); const secondBody = (await second.json()) as { @@ -951,7 +915,7 @@ describe("Extensions API v2 writes", () => { await createExtension("user-1"); const res = await get( - "/extensions/v2/extensions/new-ext", + "/v2/extensions/new-ext", await authHeaders("user-1") ); @@ -986,7 +950,7 @@ describe("Extensions API v2 writes", () => { }); const res = await get( - "/extensions/v2/extensions/adopted-ext", + "/v2/extensions/adopted-ext", await authHeaders("owner-1") ); @@ -1001,7 +965,7 @@ describe("Extensions API v2 writes", () => { it("shows the public projection for someone else's published extension", async () => { await seedOwnedExtension(); const res = await get( - "/extensions/v2/extensions/existing-ext", + "/v2/extensions/existing-ext", await authHeaders("intruder") ); expect(res.status).toBe(200); @@ -1017,7 +981,7 @@ describe("Extensions API v2 writes", () => { await seedDeveloper("new-developer", "user-1"); await createExtension("user-1"); const res = await get( - "/extensions/v2/extensions/new-ext", + "/v2/extensions/new-ext", await authHeaders("intruder") ); expect(res.status).toBe(404); @@ -1025,7 +989,7 @@ describe("Extensions API v2 writes", () => { it("404s an unknown id", async () => { const res = await get( - "/extensions/v2/extensions/no-such-ext", + "/v2/extensions/no-such-ext", await authHeaders("user-1") ); expect(res.status).toBe(404); @@ -1038,7 +1002,7 @@ describe("Extensions API v2 writes", () => { await createExtension("user-1", { extensionId: "mine" }); const res = await get( - "/extensions/v2/extensions/mine/revisions", + "/v2/extensions/mine/revisions", await authHeaders("user-1") ); expect(res.status).toBe(200); @@ -1056,10 +1020,7 @@ describe("Extensions API v2 writes", () => { await createExtension("user-1"); const headers = await authHeaders("user-1"); - const res = await get( - "/extensions/v2/extensions/new-ext/revisions", - headers - ); + const res = await get("/v2/extensions/new-ext/revisions", headers); expect(res.status).toBe(200); const data = (await res.json()) as { result: Array<{ extension_id: string; status: string }>; @@ -1074,7 +1035,7 @@ describe("Extensions API v2 writes", () => { it("refuses a caller who neither owns the extension nor moderates", async () => { await seedOwnedExtension(); const res = await get( - "/extensions/v2/extensions/existing-ext/revisions", + "/v2/extensions/existing-ext/revisions", await authHeaders("intruder") ); expect(res.status).toBe(403); diff --git a/test/services/extensions/v2/harness.ts b/test/services/extensions/v2/harness.ts index a66e0012..dbac45e4 100644 --- a/test/services/extensions/v2/harness.ts +++ b/test/services/extensions/v2/harness.ts @@ -118,11 +118,7 @@ export async function identityHeaders( } export async function syncIdentity(sub: string, body: unknown) { - return put( - "/extensions/v2/users/me/identity", - await identityHeaders(sub, body), - body - ); + return put("/v2/users/me/identity", await identityHeaders(sub, body), body); } // The PUT /extensions/{id} body: content only, no id and no developer. Both diff --git a/test/services/extensions/v2/index.test.ts b/test/services/extensions/v2/index.test.ts index 0ed985e7..3b702cd5 100644 --- a/test/services/extensions/v2/index.test.ts +++ b/test/services/extensions/v2/index.test.ts @@ -10,15 +10,24 @@ vi.mock("@octokit/request", async () => setupExtensionsV2Tests(); describe("Extensions API v2", () => { + it.each(["/extensions", "/docs/openapi.json"])( + "does not mount the old service base for %s", + async (path) => { + expect((await get(`/extensions/v2${path}`, {})).status).toBe(404); + } + ); + describe("OpenAPI docs", () => { it("serves a generated OpenAPI document", async () => { - const res = await get("/extensions/v2/docs/openapi.json", {}); + const res = await get("/v2/docs/openapi.json", {}); expect(res.status).toBe(200); const spec = (await res.json()) as { openapi: string; + servers: { url: string }[]; paths: Record; }; expect(spec.openapi).toBe("3.1.0"); + expect(spec.servers).toEqual([{ url: "/v2" }]); expect(Object.keys(spec.paths)).toEqual( expect.arrayContaining([ "/extensions", @@ -62,7 +71,7 @@ describe("Extensions API v2", () => { }); it("serves the Scalar API reference UI", async () => { - const res = await get("/extensions/v2/docs", {}); + const res = await get("/v2/docs", {}); expect(res.status).toBe(200); expect(res.headers.get("Content-Type")).toContain("text/html"); }); diff --git a/test/services/extensions/v2/manifest.test.ts b/test/services/extensions/v2/manifest.test.ts index cfc35fa8..8d6830bc 100644 --- a/test/services/extensions/v2/manifest.test.ts +++ b/test/services/extensions/v2/manifest.test.ts @@ -34,7 +34,7 @@ describe("manifest v1 publishing", () => { } }; const created = await post( - "/extensions/v2/extensions", + "/v2/extensions", await authHeaders("user-1"), content ); @@ -45,19 +45,19 @@ describe("manifest v1 publishing", () => { expect( ( await post( - `/extensions/v2/extensions/my_module/revisions/${result.revision_id}/approve`, + `/v2/extensions/my_module/revisions/${result.revision_id}/approve`, await authHeaders("moderator"), {} ) ).status ).toBe(200); - const detail = await get("/extensions/v2/extensions/my_module", {}); + const detail = await get("/v2/extensions/my_module", {}); const { result: extension } = (await detail.json()) as { result: Record; }; expect(extension).toMatchObject({ ...content, developer: { id: "owner" } }); expect(extension).not.toHaveProperty("website"); - const list = await get("/extensions/v2/extensions?type=module", {}); + const list = await get("/v2/extensions?type=module", {}); const { result: items } = (await list.json()) as { result: Record[]; }; @@ -68,7 +68,7 @@ describe("manifest v1 publishing", () => { requires: { fossbilling: ">=1.0.0", php: ">=8.4" } }; const edit = await put( - "/extensions/v2/extensions/my_module", + "/v2/extensions/my_module", await authHeaders("user-1"), updated ); @@ -79,14 +79,14 @@ describe("manifest v1 publishing", () => { expect( ( await post( - `/extensions/v2/extensions/my_module/revisions/${revision.revision_id}/approve`, + `/v2/extensions/my_module/revisions/${revision.revision_id}/approve`, await authHeaders("moderator"), {} ) ).status ).toBe(200); await expect( - (await get("/extensions/v2/extensions/my_module", {})).json() + (await get("/v2/extensions/my_module", {})).json() ).resolves.toMatchObject({ result: { requires: updated.requires, license: "MIT" } }); @@ -116,7 +116,7 @@ describe("manifest v1 publishing", () => { releases: fixture.releases.map((release) => ({ ...release, requires })) }; const created = await post( - "/extensions/v2/extensions", + "/v2/extensions", await authHeaders("user-1"), content ); @@ -127,14 +127,14 @@ describe("manifest v1 publishing", () => { expect( ( await post( - `/extensions/v2/extensions/${content.id}/revisions/${result.revision_id}/approve`, + `/v2/extensions/${content.id}/revisions/${result.revision_id}/approve`, await authHeaders("moderator"), {} ) ).status ).toBe(200); const published = (await ( - await get(`/extensions/v2/extensions/${content.id}`, {}) + await get(`/v2/extensions/${content.id}`, {}) ).json()) as { result: typeof content }; expect(published.result.requires).toEqual(requires); expect(published.result.releases[0].requires).toEqual(requires); @@ -167,11 +167,10 @@ describe("manifest v1 publishing", () => { it("rejects the retired website key specifically as an unknown root field", async () => { await seedDeveloper("owner", "user-1"); - const response = await post( - "/extensions/v2/extensions", - await authHeaders("user-1"), - { ...sampleCreate(), website: "https://example.com" } - ); + const response = await post("/v2/extensions", await authHeaders("user-1"), { + ...sampleCreate(), + website: "https://example.com" + }); expect(response.status).toBe(422); await expect(response.json()).resolves.toMatchObject({ error: { @@ -193,7 +192,7 @@ describe("manifest v1 publishing", () => { expect( ( await put( - `/extensions/v2/extensions/${id}`, + `/v2/extensions/${id}`, await authHeaders("user-1"), sampleContent() ) @@ -240,14 +239,12 @@ describe("manifest v1 publishing", () => { ]) expect( ( - await post("/extensions/v2/extensions", headers, { + await post("/v2/extensions", headers, { ...content, ...invalid }) ).status ).toBe(422); - expect( - (await post("/extensions/v2/extensions", headers, content)).status - ).toBe(201); + expect((await post("/v2/extensions", headers, content)).status).toBe(201); }); }); diff --git a/test/services/extensions/v2/moderation-correct.test.ts b/test/services/extensions/v2/moderation-correct.test.ts index 79f74c81..265e06dd 100644 --- a/test/services/extensions/v2/moderation-correct.test.ts +++ b/test/services/extensions/v2/moderation-correct.test.ts @@ -30,7 +30,7 @@ vi.mock("@octokit/request", async () => setupExtensionsV2Tests(); -const PATH = "/extensions/v2/extensions/live-ext/moderator-correct"; +const PATH = "/v2/extensions/live-ext/moderator-correct"; function correctBody(overrides?: Record) { return { @@ -155,7 +155,7 @@ describe("POST /extensions/{id}/moderator-correct (api#251)", () => { it("404s for an unknown extension", async () => { await insertUser(db, { id: "mod-1", is_moderator: 1 }); const res = await post( - "/extensions/v2/extensions/no-such-extension/moderator-correct", + "/v2/extensions/no-such-extension/moderator-correct", await authHeaders("mod-1"), correctBody() ); @@ -173,7 +173,7 @@ describe("POST /extensions/{id}/moderator-correct (api#251)", () => { developer_id: "new-developer" }); const res = await post( - "/extensions/v2/extensions/draft-ext/moderator-correct", + "/v2/extensions/draft-ext/moderator-correct", await authHeaders("mod-1"), correctBody() ); @@ -203,7 +203,7 @@ describe("POST /extensions/{id}/moderator-correct (api#251)", () => { it("409s when a pending revision exists", async () => { await seedPublished(); const edit = await put( - "/extensions/v2/extensions/live-ext", + "/v2/extensions/live-ext", await authHeaders("user-1"), sampleContent({ name: "Author Edit" }) ); @@ -309,7 +309,7 @@ describe("POST /extensions/{id}/moderator-correct (api#251)", () => { developer_id: "new-developer" }); const res = await post( - "/extensions/v2/extensions/live-EXT/moderator-correct", + "/v2/extensions/live-EXT/moderator-correct", await authHeaders("mod-1"), correctBody() ); @@ -323,7 +323,7 @@ describe("POST /extensions/{id}/moderator-correct (api#251)", () => { await insertUser(db, { id: "mod-1", is_moderator: 1 }); await seedDeveloper("new-developer", "user-1"); const created = await post( - "/extensions/v2/extensions", + "/v2/extensions", await authHeaders("user-1"), sampleCreate({ extensionId: "history-ext" }) ); @@ -334,7 +334,7 @@ describe("POST /extensions/{id}/moderator-correct (api#251)", () => { expect( ( await post( - `/extensions/v2/extensions/${pending.id}/revisions/${pending.revision_id}/approve?notify=false`, + `/v2/extensions/${pending.id}/revisions/${pending.revision_id}/approve?notify=false`, await authHeaders("mod-1"), {} ) @@ -342,7 +342,7 @@ describe("POST /extensions/{id}/moderator-correct (api#251)", () => { ).toBe(200); const res = await post( - "/extensions/v2/extensions/history-ext/moderator-correct", + "/v2/extensions/history-ext/moderator-correct", await authHeaders("mod-1"), { ...sampleContent(), readme: "# Corrected", correction_note: "typo" } ); diff --git a/test/services/extensions/v2/moderation-notify.test.ts b/test/services/extensions/v2/moderation-notify.test.ts index aa99770b..c8ba6d84 100644 --- a/test/services/extensions/v2/moderation-notify.test.ts +++ b/test/services/extensions/v2/moderation-notify.test.ts @@ -93,7 +93,7 @@ describe("moderation notification emails", () => { const calls = stubSmtpApi(); const res = await post( - "/extensions/v2/extensions/live-ext/delist", + "/v2/extensions/live-ext/delist", await authHeaders("mod-1"), { reason: "Upstream source removed" } ); @@ -118,7 +118,7 @@ describe("moderation notification emails", () => { await insertUser(db, { id: "mod-1", is_moderator: 1 }); await seedLiveExtension(); await post( - "/extensions/v2/extensions/live-ext/delist?notify=false", + "/v2/extensions/live-ext/delist?notify=false", await authHeaders("mod-1"), { reason: "Upstream source removed" } ); @@ -126,7 +126,7 @@ describe("moderation notification emails", () => { const calls = stubSmtpApi(); const res = await post( - "/extensions/v2/extensions/live-ext/relist", + "/v2/extensions/live-ext/relist", await authHeaders("mod-1"), { review_note: "Upstream is back" } ); @@ -156,7 +156,7 @@ describe("moderation notification emails", () => { const calls = stubSmtpApi(); const res = await post( - "/extensions/v2/extensions/LIVE-Ext/delist", + "/v2/extensions/LIVE-Ext/delist", await authHeaders("mod-1"), { reason: "Upstream source removed" } ); @@ -176,7 +176,7 @@ describe("moderation notification emails", () => { const calls = stubSmtpApi(); const res = await post( - "/extensions/v2/extensions/live-ext/delist", + "/v2/extensions/live-ext/delist", await authHeaders("mod-1"), { reason: "Upstream “gone” — domain lapsed" } ); @@ -201,7 +201,7 @@ describe("moderation notification emails", () => { const calls = stubSmtpApi(); const res = await post( - "/extensions/v2/extensions/live-ext/delist?notify=false", + "/v2/extensions/live-ext/delist?notify=false", await authHeaders("mod-1"), { reason: "Upstream source removed" } ); @@ -230,7 +230,7 @@ describe("moderation notification emails", () => { const calls = stubSmtpApi(); const res = await post( - "/extensions/v2/extensions/live-ext/delist", + "/v2/extensions/live-ext/delist", await authHeaders("mod-1"), { reason: "Upstream source removed" } ); @@ -252,7 +252,7 @@ describe("moderation notification emails", () => { }); const res = await post( - "/extensions/v2/extensions/live-ext/delist", + "/v2/extensions/live-ext/delist", await authHeaders("mod-1"), { reason: "Upstream source removed" } ); @@ -284,7 +284,7 @@ describe("moderation notification emails", () => { owner_user_id: "user-1" }); const created = await post( - "/extensions/v2/extensions", + "/v2/extensions", await authHeaders("user-1"), sampleCreate({ extensionId: "reject-me" }) ); @@ -298,7 +298,7 @@ describe("moderation notification emails", () => { const calls = stubSmtpApi(); const res = await post( - `/extensions/v2/extensions/${result.id}/revisions/${result.revision_id}/reject`, + `/v2/extensions/${result.id}/revisions/${result.revision_id}/reject`, await authHeaders("mod-1"), { review_note: "Needs a valid license URL" } ); @@ -317,7 +317,7 @@ describe("moderation notification emails", () => { const calls = stubSmtpApi(); const res = await post( - "/extensions/v2/developers/claims/claim-1/approve", + "/v2/developers/claims/claim-1/approve", await authHeaders("mod-1") ); expect(res.status).toBe(200); @@ -334,7 +334,7 @@ describe("moderation notification emails", () => { const calls = stubSmtpApi(); const res = await post( - "/extensions/v2/developers/claims/claim-1/reject", + "/v2/developers/claims/claim-1/reject", await authHeaders("mod-1"), { review_note: "Could not verify ownership" } ); @@ -364,7 +364,7 @@ describe("moderation notification emails", () => { const calls = stubSmtpApi(); const res = await post( - "/extensions/v2/extensions/live-ext/delist", + "/v2/extensions/live-ext/delist", await authHeaders("mod-1"), { reason: "Upstream source removed" } ); @@ -385,7 +385,7 @@ describe("moderation notification emails", () => { const calls = stubSmtpApi(); const res = await post( - "/extensions/v2/extensions/live-ext/delist", + "/v2/extensions/live-ext/delist", await authHeaders("mod-1"), { reason: "Upstream source removed" } ); @@ -418,7 +418,7 @@ describe("moderation notification emails", () => { const calls = stubSmtpApi(); const res = await post( - "/extensions/v2/extensions/live-ext/delist", + "/v2/extensions/live-ext/delist", await authHeaders("mod-1"), { reason: "Upstream source removed" } ); @@ -441,7 +441,7 @@ describe("moderation notification emails", () => { owner_user_id: "user-1" }); const created = await post( - "/extensions/v2/extensions", + "/v2/extensions", await authHeaders("user-1"), sampleCreate({ extensionId: "approve-me" }) ); @@ -453,7 +453,7 @@ describe("moderation notification emails", () => { const calls = stubSmtpApi(); const res = await post( - `/extensions/v2/extensions/${result.id}/revisions/${result.revision_id}/approve`, + `/v2/extensions/${result.id}/revisions/${result.revision_id}/approve`, await authHeaders("mod-1"), { review_note: " " } ); diff --git a/test/services/extensions/v2/moderation-revalidate.test.ts b/test/services/extensions/v2/moderation-revalidate.test.ts index fdbad7ee..3ad96c0c 100644 --- a/test/services/extensions/v2/moderation-revalidate.test.ts +++ b/test/services/extensions/v2/moderation-revalidate.test.ts @@ -77,7 +77,7 @@ async function seedModAndExtension(): Promise { // notification behaviour has its own suite (moderation-notify.test.ts). async function delist(as: string): Promise { return post( - "/extensions/v2/extensions/live-ext/delist?notify=false", + "/v2/extensions/live-ext/delist?notify=false", await authHeaders(as), { reason: "Upstream source removed" } ); @@ -108,17 +108,13 @@ describe("CDN cache revalidation on extension mutations", () => { const fetcher = stubFrontend(); const headers = await authHeaders("noop-revalidate-owner"); const profile = sampleDeveloper({ id: "noop-revalidate-profile" }); - expect( - (await put("/extensions/v2/developers/me", headers, profile)).status - ).toBe(200); + expect((await put("/v2/developers/me", headers, profile)).status).toBe(200); expect(fetcher.fetch).toHaveBeenCalledTimes(1); - expect( - (await put("/extensions/v2/developers/me", headers, profile)).status - ).toBe(200); + expect((await put("/v2/developers/me", headers, profile)).status).toBe(200); expect(fetcher.fetch).toHaveBeenCalledTimes(1); expect( ( - await put("/extensions/v2/developers/me", headers, { + await put("/v2/developers/me", headers, { ...profile, name: "Changed" }) @@ -155,7 +151,7 @@ describe("CDN cache revalidation on extension mutations", () => { const fetcher = stubFrontend(); const res = await post( - "/extensions/v2/extensions/live-ext/relist?notify=false", + "/v2/extensions/live-ext/relist?notify=false", await authHeaders("mod-1"), {} ); @@ -175,7 +171,7 @@ describe("CDN cache revalidation on extension mutations", () => { it("purges after a revision approval", async () => { await seedModAndExtension(); const created = await post( - "/extensions/v2/extensions", + "/v2/extensions", await authHeaders("user-1"), sampleCreate({ extensionId: "approve-rev" }) ); @@ -186,7 +182,7 @@ describe("CDN cache revalidation on extension mutations", () => { const fetcher = stubFrontend(); const res = await post( - `/extensions/v2/extensions/${result.id}/revisions/${result.revision_id}/approve?notify=false`, + `/v2/extensions/${result.id}/revisions/${result.revision_id}/approve?notify=false`, await authHeaders("mod-1"), {} ); @@ -199,7 +195,7 @@ describe("CDN cache revalidation on extension mutations", () => { const fetcher = stubFrontend(); const res = await post( - "/extensions/v2/extensions/live-ext/moderator-correct", + "/v2/extensions/live-ext/moderator-correct", await authHeaders("mod-1"), { ...sampleContent(), correction_note: "Fix truncated readme" } ); @@ -210,7 +206,7 @@ describe("CDN cache revalidation on extension mutations", () => { it("purges after a revision rejection", async () => { await seedModAndExtension(); const created = await post( - "/extensions/v2/extensions", + "/v2/extensions", await authHeaders("user-1"), sampleCreate({ extensionId: "reject-rev" }) ); @@ -221,7 +217,7 @@ describe("CDN cache revalidation on extension mutations", () => { const fetcher = stubFrontend(); const res = await post( - `/extensions/v2/extensions/${result.id}/revisions/${result.revision_id}/reject?notify=false`, + `/v2/extensions/${result.id}/revisions/${result.revision_id}/reject?notify=false`, await authHeaders("mod-1"), { review_note: "Needs work" } ); @@ -247,7 +243,7 @@ describe("CDN cache revalidation on extension mutations", () => { const fetcher = stubFrontend(); const res = await post( - "/extensions/v2/developers/claims/claim-1/approve?notify=false", + "/v2/developers/claims/claim-1/approve?notify=false", await authHeaders("mod-1") ); expect(res.status).toBe(200); @@ -270,7 +266,7 @@ describe("CDN cache revalidation on extension mutations", () => { const fetcher = stubFrontend(); const res = await del( - "/extensions/v2/extensions/draft-ext", + "/v2/extensions/draft-ext", await authHeaders("user-1") ); expect(res.status).toBe(200); @@ -281,7 +277,7 @@ describe("CDN cache revalidation on extension mutations", () => { const fetcher = stubFrontend(); const res = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); @@ -303,10 +299,7 @@ describe("CDN cache revalidation on extension mutations", () => { }); const fetcher = stubFrontend(); - const res = await del( - "/extensions/v2/developers/me", - await authHeaders("user-1") - ); + const res = await del("/v2/developers/me", await authHeaders("user-1")); expect(res.status).toBe(200); expect(fetchCalls(fetcher)).toHaveLength(1); }); diff --git a/test/services/extensions/v2/moderation-revocation.test.ts b/test/services/extensions/v2/moderation-revocation.test.ts index 4c45ff86..30866bed 100644 --- a/test/services/extensions/v2/moderation-revocation.test.ts +++ b/test/services/extensions/v2/moderation-revocation.test.ts @@ -111,12 +111,12 @@ describe("commit-time moderator authority", () => { } }); const path = action.startsWith("revision") - ? `/extensions/v2/extensions/review-extension/revisions/review-revision/${action.split(" ")[1]}` + ? `/v2/extensions/review-extension/revisions/review-revision/${action.split(" ")[1]}` : claimAction - ? `/extensions/v2/developers/claims/review-claim/${action.split(" ")[1]}` + ? `/v2/developers/claims/review-claim/${action.split(" ")[1]}` : action === "delist" - ? "/extensions/v2/extensions/review-extension/delist" - : "/extensions/v2/developers/review-developer/approve"; + ? "/v2/extensions/review-extension/delist" + : "/v2/developers/review-developer/approve"; const res = await post(path, headers, { ...(action.endsWith("reject") ? { review_note: "Review decision" } diff --git a/test/services/extensions/v2/moderation.test.ts b/test/services/extensions/v2/moderation.test.ts index e9f0a303..fdd779a3 100644 --- a/test/services/extensions/v2/moderation.test.ts +++ b/test/services/extensions/v2/moderation.test.ts @@ -42,7 +42,7 @@ async function createPending( overrides?: { extensionId?: string; name?: string } ): Promise<{ id: string; revisionId: string }> { const res = await post( - "/extensions/v2/extensions", + "/v2/extensions", await authHeaders(user), sampleCreate(overrides) ); @@ -58,23 +58,20 @@ function reviewPath( revisionId: string, action: "approve" | "reject" ): string { - return `/extensions/v2/extensions/${id}/revisions/${revisionId}/${action}`; + return `/v2/extensions/${id}/revisions/${revisionId}/${action}`; } describe("Extensions API v2", () => { describe("GET /revisions (queue)", () => { it("requires moderator access", async () => { - const res = await get( - "/extensions/v2/revisions", - await authHeaders("user-1") - ); + const res = await get("/v2/revisions", await authHeaders("user-1")); expect(res.status).toBe(403); }); it("identifies invalid cursors", async () => { await insertUser(db, { id: "mod-1", is_moderator: 1 }); const res = await get( - "/extensions/v2/revisions?cursor=not-a-cursor", + "/v2/revisions?cursor=not-a-cursor", await authHeaders("mod-1") ); expect(res.status).toBe(422); @@ -88,10 +85,7 @@ describe("Extensions API v2", () => { await seedDeveloper("new-developer", "user-1"); await createPending("user-1"); - const res = await get( - "/extensions/v2/revisions", - await authHeaders("mod-1") - ); + const res = await get("/v2/revisions", await authHeaders("mod-1")); expect(res.status).toBe(200); expect(res.headers.get("Vary")).toBe("Authorization"); const data = (await res.json()) as { @@ -114,7 +108,7 @@ describe("Extensions API v2", () => { }); const res = await get( - "/extensions/v2/extensions/live-ext", + "/v2/extensions/live-ext", await authHeaders("user-2") ); expect(res.status).toBe(200); @@ -129,7 +123,7 @@ describe("Extensions API v2", () => { await insertUser(db, { id: "mod-1", is_moderator: 1 }); const res = await get( - "/extensions/v2/extensions/no-such-extension", + "/v2/extensions/no-such-extension", await authHeaders("mod-1") ); expect(res.status).toBe(404); @@ -144,7 +138,7 @@ describe("Extensions API v2", () => { }); const res = await get( - "/extensions/v2/extensions/live-ext", + "/v2/extensions/live-ext", await authHeaders("mod-1") ); expect(res.status).toBe(200); @@ -162,14 +156,12 @@ describe("Extensions API v2", () => { id: "live-ext", developer_id: "new-developer" }); - await post( - "/extensions/v2/extensions/live-ext/delist", - await authHeaders("mod-1"), - { reason: "Upstream source removed" } - ); + await post("/v2/extensions/live-ext/delist", await authHeaders("mod-1"), { + reason: "Upstream source removed" + }); const res = await get( - "/extensions/v2/extensions/live-ext", + "/v2/extensions/live-ext", await authHeaders("mod-1") ); expect(res.status).toBe(200); @@ -183,7 +175,7 @@ describe("Extensions API v2", () => { it("requires moderator access", async () => { await seedDeveloper("new-developer", "user-1"); const res = await get( - "/extensions/v2/extensions?scope=all", + "/v2/extensions?scope=all", await authHeaders("user-1") ); expect(res.status).toBe(403); @@ -200,11 +192,9 @@ describe("Extensions API v2", () => { id: "draft-ext", developer_id: "new-developer" }); - await post( - "/extensions/v2/extensions/live-ext/delist", - await authHeaders("mod-1"), - { reason: "Upstream source removed" } - ); + await post("/v2/extensions/live-ext/delist", await authHeaders("mod-1"), { + reason: "Upstream source removed" + }); await seedDeveloper("other-developer", "user-2"); await insertExtension(db, { id: "other-ext", @@ -212,7 +202,7 @@ describe("Extensions API v2", () => { }); const res = await get( - "/extensions/v2/extensions?scope=all", + "/v2/extensions?scope=all", await authHeaders("mod-1") ); expect(res.status).toBe(200); @@ -240,13 +230,13 @@ describe("Extensions API v2", () => { developer_id: "new-developer" }); await post( - "/extensions/v2/extensions/delisted-ext/delist", + "/v2/extensions/delisted-ext/delist", await authHeaders("mod-1"), { reason: "Upstream source removed" } ); const published = await get( - "/extensions/v2/extensions?scope=all&status=published", + "/v2/extensions?scope=all&status=published", await authHeaders("mod-1") ); expect(await published.json()).toMatchObject({ @@ -254,7 +244,7 @@ describe("Extensions API v2", () => { }); const delisted = await get( - "/extensions/v2/extensions?scope=all&status=delisted", + "/v2/extensions?scope=all&status=delisted", await authHeaders("mod-1") ); expect(await delisted.json()).toMatchObject({ @@ -262,7 +252,7 @@ describe("Extensions API v2", () => { }); const unpublished = await get( - "/extensions/v2/extensions?scope=all&status=unpublished", + "/v2/extensions?scope=all&status=unpublished", await authHeaders("mod-1") ); expect(await unpublished.json()).toMatchObject({ @@ -287,7 +277,7 @@ describe("Extensions API v2", () => { }); const res = await get( - "/extensions/v2/extensions?scope=all&q=GATE", + "/v2/extensions?scope=all&q=GATE", await authHeaders("mod-1") ); expect(res.status).toBe(200); @@ -307,7 +297,7 @@ describe("Extensions API v2", () => { }); const percentRes = await get( - `/extensions/v2/extensions?scope=all&${new URLSearchParams({ q: "pay%gate" })}`, + `/v2/extensions?scope=all&${new URLSearchParams({ q: "pay%gate" })}`, await authHeaders("mod-1") ); expect(percentRes.status).toBe(200); @@ -316,7 +306,7 @@ describe("Extensions API v2", () => { // Unescaped, "_" is a single-character wildcard that would match the // "-" in "pay-gate" - this must not happen either. const underscoreRes = await get( - `/extensions/v2/extensions?scope=all&${new URLSearchParams({ q: "pay_gate" })}`, + `/v2/extensions?scope=all&${new URLSearchParams({ q: "pay_gate" })}`, await authHeaders("mod-1") ); expect(underscoreRes.status).toBe(200); @@ -407,9 +397,7 @@ describe("Extensions API v2", () => { // Until approval the extension exists but is in neither public listing. expect((await get("/extensions/v1/new-ext", {})).status).toBe(404); - expect((await get("/extensions/v2/extensions/new-ext", {})).status).toBe( - 404 - ); + expect((await get("/v2/extensions/new-ext", {})).status).toBe(404); const approved = await post( reviewPath(id, revisionId, "approve"), @@ -472,7 +460,7 @@ describe("Extensions API v2", () => { const afterFirst = await getExtension(db, first.id); const edit = await put( - `/extensions/v2/extensions/${first.id}`, + `/v2/extensions/${first.id}`, await authHeaders("user-1"), sampleContent({ name: "Second Version" }) ); @@ -565,7 +553,7 @@ describe("Extensions API v2", () => { // But it is still readable as history, with the empty releases intact. const history = await get( - "/extensions/v2/extensions/legacy-ext/revisions/legacy-revision", + "/v2/extensions/legacy-ext/revisions/legacy-revision", await authHeaders("user-1") ); expect(history.status).toBe(200); @@ -598,7 +586,7 @@ describe("Extensions API v2", () => { review_note: "first decision" }); const second = await put( - `/extensions/v2/extensions/${first.id}`, + `/v2/extensions/${first.id}`, await authHeaders("user-1"), sampleContent({ name: "Second" }) ); @@ -616,7 +604,7 @@ describe("Extensions API v2", () => { .run(); const mine = await get( - `/extensions/v2/extensions/${first.id}`, + `/v2/extensions/${first.id}`, await authHeaders("user-1") ); await expect(mine.json()).resolves.toMatchObject({ @@ -703,7 +691,7 @@ describe("Extensions API v2", () => { await insertUser(db, { id: "mod-1", is_moderator: 1 }); const edit = await put( - "/extensions/v2/extensions/Existing-Ext", + "/v2/extensions/Existing-Ext", await authHeaders("owner-1"), sampleContent() ); @@ -771,7 +759,7 @@ describe("Extensions API v2", () => { expect(stored?.published_at).toBeNull(); const mine = await get( - `/extensions/v2/extensions/${id}`, + `/v2/extensions/${id}`, await authHeaders("user-1") ); await expect(mine.json()).resolves.toMatchObject({ @@ -797,7 +785,7 @@ describe("Extensions API v2", () => { ); const retry = await put( - `/extensions/v2/extensions/${id}`, + `/v2/extensions/${id}`, await authHeaders("user-1"), sampleContent({ name: "Fixed" }) ); @@ -845,7 +833,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/extensions/live-ext/delist", + "/v2/extensions/live-ext/delist", await authHeaders("user-1"), { reason: "Upstream source removed" } ); @@ -856,7 +844,7 @@ describe("Extensions API v2", () => { await insertUser(db, { id: "mod-1", is_moderator: 1 }); const res = await post( - "/extensions/v2/extensions/no-such-extension/delist", + "/v2/extensions/no-such-extension/delist", await authHeaders("mod-1"), { reason: "Upstream source removed" } ); @@ -872,7 +860,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/extensions/draft-ext/delist", + "/v2/extensions/draft-ext/delist", await authHeaders("mod-1"), { reason: "Upstream source removed" } ); @@ -888,7 +876,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/extensions/live-ext/delist", + "/v2/extensions/live-ext/delist", await authHeaders("mod-1"), { reason: "" } ); @@ -904,7 +892,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/extensions/live-ext/delist", + "/v2/extensions/live-ext/delist", await authHeaders("mod-1"), { reason: " " } ); @@ -921,7 +909,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/extensions/live-ext/delist", + "/v2/extensions/live-ext/delist", await authHeaders("mod-1"), { reason: "Upstream source removed" } ); @@ -938,17 +926,15 @@ describe("Extensions API v2", () => { "2020-01-01T00:00:00.000Z" ); - expect((await get("/extensions/v2/extensions", {})).status).toBe(200); + expect((await get("/v2/extensions", {})).status).toBe(200); await expect( - (await get("/extensions/v2/extensions", {})).json() + (await get("/v2/extensions", {})).json() ).resolves.toMatchObject({ result: [] }); - expect((await get("/extensions/v2/extensions/live-ext", {})).status).toBe( - 404 - ); + expect((await get("/v2/extensions/live-ext", {})).status).toBe(404); // The owner can still see it, plus why it was pulled. const mine = await get( - "/extensions/v2/extensions/live-ext", + "/v2/extensions/live-ext", await authHeaders("user-1") ); await expect(mine.json()).resolves.toMatchObject({ @@ -967,11 +953,11 @@ describe("Extensions API v2", () => { developer_id: "new-developer" }); const mod = await authHeaders("mod-1"); - await post("/extensions/v2/extensions/live-ext/delist", mod, { + await post("/v2/extensions/live-ext/delist", mod, { reason: "First reason" }); - const res = await post("/extensions/v2/extensions/live-ext/delist", mod, { + const res = await post("/v2/extensions/live-ext/delist", mod, { reason: "Second reason" }); expect(res.status).toBe(409); @@ -992,16 +978,12 @@ describe("Extensions API v2", () => { id: "live-ext", developer_id: "new-developer" }); - await post( - "/extensions/v2/extensions/live-ext/delist", - await authHeaders("mod-1"), - { - reason: "Upstream source removed" - } - ); + await post("/v2/extensions/live-ext/delist", await authHeaders("mod-1"), { + reason: "Upstream source removed" + }); const owner = await get( - "/extensions/v2/extensions/live-ext", + "/v2/extensions/live-ext", await authHeaders("user-1") ); expect(owner.status).toBe(200); @@ -1011,7 +993,7 @@ describe("Extensions API v2", () => { }); const moderator = await get( - "/extensions/v2/extensions/live-ext", + "/v2/extensions/live-ext", await authHeaders("mod-1") ); expect(moderator.status).toBe(200); @@ -1021,16 +1003,16 @@ describe("Extensions API v2", () => { }); const stranger = await get( - "/extensions/v2/extensions/live-ext", + "/v2/extensions/live-ext", await authHeaders("user-2") ); expect(stranger.status).toBe(404); - const anonymous = await get("/extensions/v2/extensions/live-ext", {}); + const anonymous = await get("/v2/extensions/live-ext", {}); expect(anonymous.status).toBe(404); const strangerRevisions = await get( - "/extensions/v2/extensions/live-ext/revisions", + "/v2/extensions/live-ext/revisions", await authHeaders("user-2") ); expect(strangerRevisions.status).toBe(403); @@ -1046,7 +1028,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/extensions/live-ext/relist", + "/v2/extensions/live-ext/relist", await authHeaders("user-1"), {} ); @@ -1057,7 +1039,7 @@ describe("Extensions API v2", () => { await insertUser(db, { id: "mod-1", is_moderator: 1 }); const res = await post( - "/extensions/v2/extensions/no-such-extension/relist", + "/v2/extensions/no-such-extension/relist", await authHeaders("mod-1"), {} ); @@ -1073,7 +1055,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/extensions/live-ext/relist", + "/v2/extensions/live-ext/relist", await authHeaders("mod-1"), {} ); @@ -1093,15 +1075,12 @@ describe("Extensions API v2", () => { developer_id: "new-developer" }); const mod = await authHeaders("mod-1"); - await post("/extensions/v2/extensions/live-ext/delist", mod, { + await post("/v2/extensions/live-ext/delist", mod, { reason: "Upstream source removed" }); const { "Content-Type": _dropped, ...noContentType } = mod; - const res = await post( - "/extensions/v2/extensions/live-ext/relist", - noContentType - ); + const res = await post("/v2/extensions/live-ext/relist", noContentType); expect(res.status).toBe(200); await expect(res.json()).resolves.toEqual({ result: { id: "live-ext", status: "relisted", notified: false } @@ -1116,15 +1095,15 @@ describe("Extensions API v2", () => { developer_id: "new-developer" }); const mod = await authHeaders("mod-1"); - await post("/extensions/v2/extensions/live-ext/delist", mod, { + await post("/v2/extensions/live-ext/delist", mod, { reason: "Upstream source removed" }); - expect((await get("/extensions/v2/extensions", {})).status).toBe(200); + expect((await get("/v2/extensions", {})).status).toBe(200); await expect( - (await get("/extensions/v2/extensions", {})).json() + (await get("/v2/extensions", {})).json() ).resolves.toMatchObject({ result: [] }); - const res = await post("/extensions/v2/extensions/live-ext/relist", mod, { + const res = await post("/v2/extensions/live-ext/relist", mod, { review_note: "Upstream is back" }); expect(res.status).toBe(200); @@ -1136,7 +1115,7 @@ describe("Extensions API v2", () => { delisted_at: null }); await expect( - (await get("/extensions/v2/extensions", {})).json() + (await get("/v2/extensions", {})).json() ).resolves.toMatchObject({ result: [{ id: "live-ext" }] }); }); @@ -1148,15 +1127,11 @@ describe("Extensions API v2", () => { developer_id: "new-developer" }); const mod = await authHeaders("mod-1"); - await post("/extensions/v2/extensions/live-ext/delist", mod, { + await post("/v2/extensions/live-ext/delist", mod, { reason: "Upstream source removed" }); - const res = await post( - "/extensions/v2/extensions/Live-EXT/relist", - mod, - {} - ); + const res = await post("/v2/extensions/Live-EXT/relist", mod, {}); expect(res.status).toBe(200); await expect(res.json()).resolves.toEqual({ result: { id: "LIVE-ext", status: "relisted", notified: false } @@ -1171,7 +1146,7 @@ describe("Extensions API v2", () => { developer_id: "new-developer" }); const mod = await authHeaders("mod-1"); - await post("/extensions/v2/extensions/live-ext/delist", mod, { + await post("/v2/extensions/live-ext/delist", mod, { reason: "Upstream source removed" }); @@ -1180,11 +1155,7 @@ describe("Extensions API v2", () => { .bind("mod-1") .run(); - const res = await post( - "/extensions/v2/extensions/live-ext/relist", - mod, - {} - ); + const res = await post("/v2/extensions/live-ext/relist", mod, {}); expect(res.status).toBe(403); expect(await getExtension(db, "live-ext")).toMatchObject({ delisted_at: expect.any(String) @@ -1195,26 +1166,26 @@ describe("Extensions API v2", () => { describe("developer moderation", () => { it("binds approval to the exact profile revision reviewed", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper({ name: "Revision two" }) ); await insertUser(db, { id: "mod-1", is_moderator: 1 }); const stale = await post( - "/extensions/v2/developers/dev-developer/approve", + "/v2/developers/dev-developer/approve", await authHeaders("mod-1"), await developerApproval("dev-developer", 1) ); expect(stale.status).toBe(409); const current = await post( - "/extensions/v2/developers/dev-developer/approve", + "/v2/developers/dev-developer/approve", await authHeaders("mod-1"), await developerApproval("dev-developer", 2) ); @@ -1223,7 +1194,7 @@ describe("Extensions API v2", () => { it("does not turn an approval diagnosis database failure into not found", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); @@ -1236,7 +1207,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/dev-developer/approve", + "/v2/developers/dev-developer/approve", await authHeaders("mod-1"), await developerApproval("dev-developer", 999) ); @@ -1250,7 +1221,7 @@ describe("Extensions API v2", () => { it("reports an inactive moderator when the account is deactivated during approval", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); @@ -1272,7 +1243,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/dev-developer/approve", + "/v2/developers/dev-developer/approve", await authHeaders("mod-1"), await developerApproval("dev-developer", 1) ); @@ -1288,14 +1259,14 @@ describe("Extensions API v2", () => { it("approves a developer and removes it from the unapproved list", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); await insertUser(db, { id: "mod-1", is_moderator: 1 }); const approve = await post( - "/extensions/v2/developers/dev-developer/approve", + "/v2/developers/dev-developer/approve", await authHeaders("mod-1"), await developerApproval("dev-developer", 1) ); @@ -1310,7 +1281,7 @@ describe("Extensions API v2", () => { }); const unapproved = await get( - "/extensions/v2/developers?status=unapproved", + "/v2/developers?status=unapproved", await authHeaders("mod-1") ); expect(unapproved.status).toBe(200); @@ -1326,7 +1297,7 @@ describe("Extensions API v2", () => { await insertUser(db, { id: "mod-1", is_moderator: 1 }); const res = await post( - "/extensions/v2/developers/no-such-developer/approve", + "/v2/developers/no-such-developer/approve", await authHeaders("mod-1"), { expected_revision: 1, expected_generation: "0".repeat(32) } ); @@ -1335,7 +1306,7 @@ describe("Extensions API v2", () => { it("blocks non-moderators from listing unapproved developers", async () => { const res = await get( - "/extensions/v2/developers?status=unapproved", + "/v2/developers?status=unapproved", await authHeaders("user-1") ); expect(res.status).toBe(403); @@ -1343,26 +1314,23 @@ describe("Extensions API v2", () => { it("lists every developer, approved and unapproved", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-2"), sampleDeveloper({ id: "other-developer", name: "Other Developer" }) ); await insertUser(db, { id: "mod-1", is_moderator: 1 }); await post( - "/extensions/v2/developers/dev-developer/approve", + "/v2/developers/dev-developer/approve", await authHeaders("mod-1"), await developerApproval("dev-developer", 1) ); - const res = await get( - "/extensions/v2/developers", - await authHeaders("mod-1") - ); + const res = await get("/v2/developers", await authHeaders("mod-1")); expect(res.status).toBe(200); const body = (await res.json()) as { result: Array<{ id: string; approved: boolean }>; @@ -1380,26 +1348,20 @@ describe("Extensions API v2", () => { }); it("blocks non-moderators from listing all developers", async () => { - const res = await get( - "/extensions/v2/developers", - await authHeaders("user-1") - ); + const res = await get("/v2/developers", await authHeaders("user-1")); expect(res.status).toBe(403); }); it("lists by scope and rejects a conflicting scope/status pair", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); await insertUser(db, { id: "mod-1", is_moderator: 1 }); const mod = await authHeaders("mod-1"); - const scoped = await get( - "/extensions/v2/developers?scope=unapproved", - mod - ); + const scoped = await get("/v2/developers?scope=unapproved", mod); expect(scoped.status).toBe(200); await expect(scoped.json()).resolves.toMatchObject({ result: [{ id: "dev-developer" }], @@ -1407,7 +1369,7 @@ describe("Extensions API v2", () => { }); const conflict = await get( - "/extensions/v2/developers?scope=all&status=unapproved", + "/v2/developers?scope=all&status=unapproved", mod ); expect(conflict.status).toBe(422); @@ -1415,19 +1377,19 @@ describe("Extensions API v2", () => { it("walks the developer list by cursor", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper({ id: "aaa-developer", name: "Aaa Developer" }) ); await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-2"), sampleDeveloper({ id: "zzz-developer", name: "Zzz Developer" }) ); await insertUser(db, { id: "mod-1", is_moderator: 1 }); const mod = await authHeaders("mod-1"); - const first = await get("/extensions/v2/developers?limit=1", mod); + const first = await get("/v2/developers?limit=1", mod); expect(first.status).toBe(200); const firstBody = (await first.json()) as { result: Array<{ id: string }>; @@ -1438,7 +1400,7 @@ describe("Extensions API v2", () => { expect(firstBody.pagination.next_cursor).toBeTruthy(); const second = await get( - `/extensions/v2/developers?limit=1&cursor=${encodeURIComponent(firstBody.pagination.next_cursor!)}`, + `/v2/developers?limit=1&cursor=${encodeURIComponent(firstBody.pagination.next_cursor!)}`, mod ); expect(second.status).toBe(200); @@ -1452,22 +1414,19 @@ describe("Extensions API v2", () => { has_more: false }); - const bad = await get( - "/extensions/v2/developers?cursor=not-a-cursor", - mod - ); + const bad = await get("/v2/developers?cursor=not-a-cursor", mod); expect(bad.status).toBe(422); }); it("blocks non-moderators from approving developers", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); const res = await post( - "/extensions/v2/developers/dev-developer/approve", + "/v2/developers/dev-developer/approve", await authHeaders("user-1"), await developerApproval("dev-developer", 1) ); @@ -1478,14 +1437,14 @@ describe("Extensions API v2", () => { describe("GET /developers/{id}/history", () => { it("records a history entry for a newly created profile", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); await insertUser(db, { id: "mod-1", is_moderator: 1 }); const res = await get( - "/extensions/v2/developers/dev-developer/history", + "/v2/developers/dev-developer/history", await authHeaders("mod-1") ); @@ -1506,14 +1465,14 @@ describe("Extensions API v2", () => { // Params omitted fall back to a bounded cursor window (default limit 50). it("applies the default pagination window when params are omitted", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); await insertUser(db, { id: "mod-1", is_moderator: 1 }); const res = await get( - "/extensions/v2/developers/dev-developer/history", + "/v2/developers/dev-developer/history", await authHeaders("mod-1") ); @@ -1530,12 +1489,12 @@ describe("Extensions API v2", () => { it("bounds history pages to the requested limit", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper({ name: "Original Name" }) ); await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper({ name: "Edited Name" }) ); @@ -1543,7 +1502,7 @@ describe("Extensions API v2", () => { const mod = await authHeaders("mod-1"); const first = await get( - "/extensions/v2/developers/dev-developer/history?limit=1", + "/v2/developers/dev-developer/history?limit=1", mod ); expect(first.status).toBe(200); @@ -1556,7 +1515,7 @@ describe("Extensions API v2", () => { expect(firstBody.pagination.next_cursor).toBeTruthy(); const second = await get( - `/extensions/v2/developers/dev-developer/history?limit=1&cursor=${encodeURIComponent(firstBody.pagination.next_cursor as string)}`, + `/v2/developers/dev-developer/history?limit=1&cursor=${encodeURIComponent(firstBody.pagination.next_cursor as string)}`, mod ); const secondBody = (await second.json()) as { @@ -1574,7 +1533,7 @@ describe("Extensions API v2", () => { await insertUser(db, { id: "mod-1", is_moderator: 1 }); const res = await get( - "/extensions/v2/developers/dev-developer/history?cursor=not-a-cursor", + "/v2/developers/dev-developer/history?cursor=not-a-cursor", await authHeaders("mod-1") ); expect(res.status).toBe(422); @@ -1582,19 +1541,19 @@ describe("Extensions API v2", () => { it("orders entries newest-first and snapshots each write", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper({ name: "Original Name" }) ); await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper({ name: "Edited Name" }) ); await insertUser(db, { id: "mod-1", is_moderator: 1 }); const res = await get( - "/extensions/v2/developers/dev-developer/history", + "/v2/developers/dev-developer/history", await authHeaders("mod-1") ); @@ -1611,7 +1570,7 @@ describe("Extensions API v2", () => { await insertUser(db, { id: "mod-1", is_moderator: 1 }); const res = await get( - "/extensions/v2/developers/no-such-developer/history", + "/v2/developers/no-such-developer/history", await authHeaders("mod-1") ); @@ -1622,13 +1581,13 @@ describe("Extensions API v2", () => { it("blocks non-moderators", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); const res = await get( - "/extensions/v2/developers/dev-developer/history", + "/v2/developers/dev-developer/history", await authHeaders("user-1") ); expect(res.status).toBe(403); @@ -1636,13 +1595,13 @@ describe("Extensions API v2", () => { it("does not record history for a rejected write (id already taken)", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); const res = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-2"), sampleDeveloper() ); @@ -1650,7 +1609,7 @@ describe("Extensions API v2", () => { await insertUser(db, { id: "mod-1", is_moderator: 1 }); const history = await get( - "/extensions/v2/developers/dev-developer/history", + "/v2/developers/dev-developer/history", await authHeaders("mod-1") ); const data = (await history.json()) as { result: unknown[] }; @@ -1661,7 +1620,7 @@ describe("Extensions API v2", () => { describe("GET /moderation/counts", () => { it("requires moderator access", async () => { const res = await get( - "/extensions/v2/moderation/counts", + "/v2/moderation/counts", await authHeaders("user-1") ); expect(res.status).toBe(403); @@ -1712,7 +1671,7 @@ describe("Extensions API v2", () => { }); const res = await get( - "/extensions/v2/moderation/counts", + "/v2/moderation/counts", await authHeaders("mod-1") ); expect(res.status).toBe(200); diff --git a/test/services/extensions/v2/ownership.test.ts b/test/services/extensions/v2/ownership.test.ts index a7ee40ea..21e3563a 100644 --- a/test/services/extensions/v2/ownership.test.ts +++ b/test/services/extensions/v2/ownership.test.ts @@ -43,7 +43,7 @@ describe("Extensions API v2", () => { describe("developer transfers", () => { it("does not accept transfer capabilities in URL paths", async () => { const res = await post( - "/extensions/v2/developers/transfers/secret-token/accept", + "/v2/developers/transfers/secret-token/accept", await authHeaders("user-2") ); expect(res.status).toBe(404); @@ -51,13 +51,13 @@ describe("Extensions API v2", () => { it("initiating a second transfer revokes the first token", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); const first = await post( - "/extensions/v2/developers/dev-developer/transfer", + "/v2/developers/dev-developer/transfer", await authHeaders("user-1") ); expect(first.status).toBe(200); @@ -65,13 +65,13 @@ describe("Extensions API v2", () => { .result.token; const second = await post( - "/extensions/v2/developers/dev-developer/transfer", + "/v2/developers/dev-developer/transfer", await authHeaders("user-1") ); expect(second.status).toBe(200); const acceptFirst = await post( - "/extensions/v2/developers/transfers/accept", + "/v2/developers/transfers/accept", await authHeaders("user-2"), { token: firstToken } ); @@ -80,26 +80,26 @@ describe("Extensions API v2", () => { it("accepts a valid token, transferring ownership and clearing approval", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); await insertUser(db, { id: "mod-1", is_moderator: 1 }); await post( - "/extensions/v2/developers/dev-developer/approve", + "/v2/developers/dev-developer/approve", await authHeaders("mod-1"), await developerApproval("dev-developer", 1) ); const initiate = await post( - "/extensions/v2/developers/dev-developer/transfer", + "/v2/developers/dev-developer/transfer", await authHeaders("user-1") ); const token = ((await initiate.json()) as { result: { token: string } }) .result.token; const accept = await post( - "/extensions/v2/developers/transfers/accept", + "/v2/developers/transfers/accept", await authHeaders("user-2"), { token } ); @@ -114,7 +114,7 @@ describe("Extensions API v2", () => { ); const acceptAgain = await post( - "/extensions/v2/developers/transfers/accept", + "/v2/developers/transfers/accept", await authHeaders("user-3"), { token } ); @@ -126,12 +126,12 @@ describe("Extensions API v2", () => { it("does not turn a committed transfer into a database error if the profile is deleted before the response lookup", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); const initiate = await post( - "/extensions/v2/developers/dev-developer/transfer", + "/v2/developers/dev-developer/transfer", await authHeaders("user-1") ); const token = ((await initiate.json()) as { result: { token: string } }) @@ -157,7 +157,7 @@ describe("Extensions API v2", () => { }); const accept = await post( - "/extensions/v2/developers/transfers/accept", + "/v2/developers/transfers/accept", await authHeaders("user-2"), { token } ); @@ -172,7 +172,7 @@ describe("Extensions API v2", () => { it("rejects pending revisions and claims when ownership changes", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); @@ -194,14 +194,14 @@ describe("Extensions API v2", () => { }); const initiate = await post( - "/extensions/v2/developers/dev-developer/transfer", + "/v2/developers/dev-developer/transfer", await authHeaders("user-1") ); const token = ((await initiate.json()) as { result: { token: string } }) .result.token; const accept = await post( - "/extensions/v2/developers/transfers/accept", + "/v2/developers/transfers/accept", await authHeaders("user-2"), { token } ); @@ -221,7 +221,7 @@ describe("Extensions API v2", () => { it("reports an inactive owner when the account is deactivated during initiation", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); @@ -238,7 +238,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/dev-developer/transfer", + "/v2/developers/dev-developer/transfer", await authHeaders("user-1") ); env.DB_EXTENSIONS = db; @@ -255,13 +255,13 @@ describe("Extensions API v2", () => { it("reports an inactive recipient when the account is deactivated during acceptance", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); const initiate = await post( - "/extensions/v2/developers/dev-developer/transfer", + "/v2/developers/dev-developer/transfer", await authHeaders("user-1") ); const token = ((await initiate.json()) as { result: { token: string } }) @@ -279,7 +279,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/transfers/accept", + "/v2/developers/transfers/accept", await authHeaders("user-2"), { token } ); @@ -302,25 +302,25 @@ describe("Extensions API v2", () => { it("doesn't inherit the previous owner's check_url cooldown after a transfer", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); const usedCooldown = await post( - "/extensions/v2/developers/me/reverify?check_url=true", + "/v2/developers/me/reverify?check_url=true", await authHeaders("user-1") ); expect(usedCooldown.status).toBe(200); const initiate = await post( - "/extensions/v2/developers/dev-developer/transfer", + "/v2/developers/dev-developer/transfer", await authHeaders("user-1") ); const token = ((await initiate.json()) as { result: { token: string } }) .result.token; const accept = await post( - "/extensions/v2/developers/transfers/accept", + "/v2/developers/transfers/accept", await authHeaders("user-2"), { token } ); @@ -329,7 +329,7 @@ describe("Extensions API v2", () => { // user-2 has never called check_url themselves — the previous // owner's still-active cooldown must not carry over onto them. const res = await post( - "/extensions/v2/developers/me/reverify?check_url=true", + "/v2/developers/me/reverify?check_url=true", await authHeaders("user-2") ); expect(res.status).toBe(200); @@ -343,7 +343,7 @@ describe("Extensions API v2", () => { github_orgs: JSON.stringify([]) }); const created = await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), { ...sampleDeveloper(), URL: "https://acme.example" } ); @@ -357,13 +357,13 @@ describe("Extensions API v2", () => { expect(createdBody.result.github_url_verified).toBe(true); const initiate = await post( - "/extensions/v2/developers/dev-developer/transfer", + "/v2/developers/dev-developer/transfer", await authHeaders("user-1") ); const token = ((await initiate.json()) as { result: { token: string } }) .result.token; const accept = await post( - "/extensions/v2/developers/transfers/accept", + "/v2/developers/transfers/accept", await authHeaders("user-2"), { token } ); @@ -380,20 +380,20 @@ describe("Extensions API v2", () => { it("does not let replaying an already-used token reassign ownership away from a later owner", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); const initiate1 = await post( - "/extensions/v2/developers/dev-developer/transfer", + "/v2/developers/dev-developer/transfer", await authHeaders("user-1") ); const token1 = ((await initiate1.json()) as { result: { token: string } }) .result.token; const accept1 = await post( - "/extensions/v2/developers/transfers/accept", + "/v2/developers/transfers/accept", await authHeaders("user-2"), { token: token1 } ); @@ -404,13 +404,13 @@ describe("Extensions API v2", () => { // dev-developer is legitimately handed off again, to a third user. const initiate2 = await post( - "/extensions/v2/developers/dev-developer/transfer", + "/v2/developers/dev-developer/transfer", await authHeaders("user-2") ); const token2 = ((await initiate2.json()) as { result: { token: string } }) .result.token; const accept2 = await post( - "/extensions/v2/developers/transfers/accept", + "/v2/developers/transfers/accept", await authHeaders("user-3"), { token: token2 } ); @@ -422,7 +422,7 @@ describe("Extensions API v2", () => { // Replaying the *first* (already-used) token, by the same user who // originally accepted it, must not silently reassign ownership back. const replay = await post( - "/extensions/v2/developers/transfers/accept", + "/v2/developers/transfers/accept", await authHeaders("user-2"), { token: token1 } ); @@ -434,13 +434,13 @@ describe("Extensions API v2", () => { it("rejects an expired token", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); const initiate = await post( - "/extensions/v2/developers/dev-developer/transfer", + "/v2/developers/dev-developer/transfer", await authHeaders("user-1") ); const token = ((await initiate.json()) as { result: { token: string } }) @@ -449,7 +449,7 @@ describe("Extensions API v2", () => { await expireAllDeveloperTransfers(db); const accept = await post( - "/extensions/v2/developers/transfers/accept", + "/v2/developers/transfers/accept", await authHeaders("user-2"), { token } ); @@ -458,26 +458,26 @@ describe("Extensions API v2", () => { it("rejects a revoked token", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); const initiate = await post( - "/extensions/v2/developers/dev-developer/transfer", + "/v2/developers/dev-developer/transfer", await authHeaders("user-1") ); const token = ((await initiate.json()) as { result: { token: string } }) .result.token; const revoke = await post( - "/extensions/v2/developers/dev-developer/transfer/revoke", + "/v2/developers/dev-developer/transfer/revoke", await authHeaders("user-1") ); expect(revoke.status).toBe(200); const accept = await post( - "/extensions/v2/developers/transfers/accept", + "/v2/developers/transfers/accept", await authHeaders("user-2"), { token } ); @@ -486,25 +486,25 @@ describe("Extensions API v2", () => { it("rejects acceptance by a user who already owns a different profile", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-2"), sampleDeveloper({ id: "other-developer", name: "Other Developer" }) ); const initiate = await post( - "/extensions/v2/developers/dev-developer/transfer", + "/v2/developers/dev-developer/transfer", await authHeaders("user-1") ); const token = ((await initiate.json()) as { result: { token: string } }) .result.token; const accept = await post( - "/extensions/v2/developers/transfers/accept", + "/v2/developers/transfers/accept", await authHeaders("user-2"), { token } ); @@ -516,20 +516,20 @@ describe("Extensions API v2", () => { it("rejects the current owner accepting their own transfer link", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); const initiate = await post( - "/extensions/v2/developers/dev-developer/transfer", + "/v2/developers/dev-developer/transfer", await authHeaders("user-1") ); const token = ((await initiate.json()) as { result: { token: string } }) .result.token; const accept = await post( - "/extensions/v2/developers/transfers/accept", + "/v2/developers/transfers/accept", await authHeaders("user-1"), { token } ); @@ -541,13 +541,13 @@ describe("Extensions API v2", () => { it("blocks a non-owner from initiating a transfer", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); const res = await post( - "/extensions/v2/developers/dev-developer/transfer", + "/v2/developers/dev-developer/transfer", await authHeaders("intruder") ); expect(res.status).toBe(403); @@ -555,17 +555,17 @@ describe("Extensions API v2", () => { it("blocks a non-owner from revoking a transfer", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); await post( - "/extensions/v2/developers/dev-developer/transfer", + "/v2/developers/dev-developer/transfer", await authHeaders("user-1") ); const res = await post( - "/extensions/v2/developers/dev-developer/transfer/revoke", + "/v2/developers/dev-developer/transfer/revoke", await authHeaders("intruder") ); expect(res.status).toBe(403); @@ -580,7 +580,7 @@ describe("Extensions API v2", () => { await seedUnownedDeveloper("legacy-developer"); const res = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), { note: "mine", github_org_verified: true } ); @@ -604,7 +604,7 @@ describe("Extensions API v2", () => { await seedUnownedDeveloper("legacy-developer"); const res = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), { note: "I'm the maintainer, see github.com/x" } ); @@ -612,7 +612,7 @@ describe("Extensions API v2", () => { const created = (await res.json()) as { result: { id: string } }; const mine = await get( - "/extensions/v2/developers/claims?scope=mine", + "/v2/developers/claims?scope=mine", await authHeaders("user-1") ); expect(mine.status).toBe(200); @@ -624,7 +624,7 @@ describe("Extensions API v2", () => { await insertUser(db, { id: "mod-1", is_moderator: 1 }); const pending = await get( - "/extensions/v2/developers/claims?scope=pending", + "/v2/developers/claims?scope=pending", await authHeaders("mod-1") ); expect(pending.status).toBe(200); @@ -638,7 +638,7 @@ describe("Extensions API v2", () => { it("rejects a status filter that disagrees with the pending queue", async () => { await seedUnownedDeveloper("legacy-developer"); await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); @@ -647,13 +647,13 @@ describe("Extensions API v2", () => { // status only narrows scope=mine; on scope=pending anything but // pending/all is a 422 rather than a silently ignored filter. const rejected = await get( - "/extensions/v2/developers/claims?scope=pending&status=approved", + "/v2/developers/claims?scope=pending&status=approved", await authHeaders("mod-1") ); expect(rejected.status).toBe(422); const pending = await get( - "/extensions/v2/developers/claims?scope=pending&status=pending", + "/v2/developers/claims?scope=pending&status=pending", await authHeaders("mod-1") ); expect(pending.status).toBe(200); @@ -664,13 +664,13 @@ describe("Extensions API v2", () => { it("rejects claiming a developer that already has an owner", async () => { await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); const res = await post( - "/extensions/v2/developers/dev-developer/claim", + "/v2/developers/dev-developer/claim", await authHeaders("user-2"), {} ); @@ -703,7 +703,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/acme-org/claim", + "/v2/developers/acme-org/claim", await authHeaders("user-1"), {} ); @@ -734,14 +734,14 @@ describe("Extensions API v2", () => { await seedUnownedDeveloper("legacy-developer"); const first = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); expect(first.status).toBe(201); const second = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); @@ -753,7 +753,7 @@ describe("Extensions API v2", () => { await seedUnownedDeveloper("legacy-developer"); await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); @@ -761,7 +761,7 @@ describe("Extensions API v2", () => { expect(callsAfterFirst).toBeGreaterThan(0); const second = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); @@ -775,14 +775,14 @@ describe("Extensions API v2", () => { await insertUser(db, { id: "mod-1", is_moderator: 1 }); const first = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); const claimId = ((await first.json()) as { result: { id: string } }) .result.id; await post( - `/extensions/v2/developers/claims/${claimId}/reject`, + `/v2/developers/claims/${claimId}/reject`, await authHeaders("mod-1"), { review_note: "Not enough evidence" } ); @@ -799,7 +799,7 @@ describe("Extensions API v2", () => { const claimCountBefore = await countDeveloperClaims(db); const retry = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); @@ -813,13 +813,13 @@ describe("Extensions API v2", () => { it("rejects a claim from a user who already owns a different profile", async () => { await seedUnownedDeveloper("legacy-developer"); await put( - "/extensions/v2/developers/me", + "/v2/developers/me", await authHeaders("user-1"), sampleDeveloper() ); const res = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); @@ -845,7 +845,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", headers, {} ); @@ -864,14 +864,14 @@ describe("Extensions API v2", () => { await insertUser(db, { id: "mod-1", is_moderator: 1 }); const claim1 = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); const claim1Id = ((await claim1.json()) as { result: { id: string } }) .result.id; const claim2 = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-2"), {} ); @@ -892,7 +892,7 @@ describe("Extensions API v2", () => { }); const approve = await post( - `/extensions/v2/developers/claims/${claim1Id}/approve`, + `/v2/developers/claims/${claim1Id}/approve`, await authHeaders("mod-1") ); expect(approve.status).toBe(500); @@ -910,7 +910,7 @@ describe("Extensions API v2", () => { await insertUser(db, { id: "mod-1", is_moderator: 1 }); const claim1 = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); @@ -918,7 +918,7 @@ describe("Extensions API v2", () => { .result.id; const claim2 = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-2"), {} ); @@ -926,7 +926,7 @@ describe("Extensions API v2", () => { .result.id; const approve = await post( - `/extensions/v2/developers/claims/${claim1Id}/approve`, + `/v2/developers/claims/${claim1Id}/approve`, await authHeaders("mod-1") ); expect(approve.status).toBe(200); @@ -951,14 +951,14 @@ describe("Extensions API v2", () => { await insertUser(db, { id: "mod-1", is_moderator: 1 }); const first = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); const firstId = ((await first.json()) as { result: { id: string } }) .result.id; const second = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-2"), {} ); @@ -967,8 +967,8 @@ describe("Extensions API v2", () => { const headers = await authHeaders("mod-1"); const approvals = await Promise.all([ - post(`/extensions/v2/developers/claims/${firstId}/approve`, headers), - post(`/extensions/v2/developers/claims/${secondId}/approve`, headers) + post(`/v2/developers/claims/${firstId}/approve`, headers), + post(`/v2/developers/claims/${secondId}/approve`, headers) ]); expect(approvals.map(({ status }) => status).sort()).toEqual([200, 409]); @@ -998,7 +998,7 @@ describe("Extensions API v2", () => { }); const claim = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); @@ -1008,7 +1008,7 @@ describe("Extensions API v2", () => { expect(claimRow?.github_org_verified).toBe(1); const approve = await post( - `/extensions/v2/developers/claims/${claimId}/approve`, + `/v2/developers/claims/${claimId}/approve`, await authHeaders("mod-1") ); expect(approve.status).toBe(200); @@ -1026,7 +1026,7 @@ describe("Extensions API v2", () => { await insertUser(db, { id: "mod-1", is_moderator: 1 }); const claim = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); @@ -1034,7 +1034,7 @@ describe("Extensions API v2", () => { .result.id; const reject = await post( - `/extensions/v2/developers/claims/${claimId}/reject`, + `/v2/developers/claims/${claimId}/reject`, await authHeaders("mod-1"), { review_note: "Not enough evidence of maintainership" } ); @@ -1056,7 +1056,7 @@ describe("Extensions API v2", () => { await insertUser(db, { id: "mod-1", is_moderator: 1 }); const claim = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); @@ -1064,7 +1064,7 @@ describe("Extensions API v2", () => { .result.id; const reject = await post( - `/extensions/v2/developers/claims/${claimId}/reject`, + `/v2/developers/claims/${claimId}/reject`, await authHeaders("mod-1"), { review_note: " " } ); @@ -1087,7 +1087,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); @@ -1114,7 +1114,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/legacy-user/claim", + "/v2/developers/legacy-user/claim", await authHeaders("user-1"), {} ); @@ -1141,7 +1141,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); @@ -1157,7 +1157,7 @@ describe("Extensions API v2", () => { // No row in users for user-1 — never linked GitHub. const res = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); @@ -1185,7 +1185,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); @@ -1213,7 +1213,7 @@ describe("Extensions API v2", () => { }); const res = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); @@ -1229,7 +1229,7 @@ describe("Extensions API v2", () => { mockGithubEntityNotFound(); const res = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); @@ -1243,7 +1243,7 @@ describe("Extensions API v2", () => { it("blocks non-moderators from the claims queue and review routes", async () => { await seedUnownedDeveloper("legacy-developer"); const claim = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); @@ -1251,19 +1251,19 @@ describe("Extensions API v2", () => { .result.id; const queue = await get( - "/extensions/v2/developers/claims?scope=pending", + "/v2/developers/claims?scope=pending", await authHeaders("intruder") ); expect(queue.status).toBe(403); const approve = await post( - `/extensions/v2/developers/claims/${claimId}/approve`, + `/v2/developers/claims/${claimId}/approve`, await authHeaders("intruder") ); expect(approve.status).toBe(403); const reject = await post( - `/extensions/v2/developers/claims/${claimId}/reject`, + `/v2/developers/claims/${claimId}/reject`, await authHeaders("intruder"), { review_note: "no" } ); @@ -1273,7 +1273,7 @@ describe("Extensions API v2", () => { it("lets a claimant cancel their own pending claim", async () => { await seedUnownedDeveloper("legacy-developer"); const claim = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); @@ -1281,7 +1281,7 @@ describe("Extensions API v2", () => { .result.id; const cancel = await post( - `/extensions/v2/developers/claims/${claimId}/cancel`, + `/v2/developers/claims/${claimId}/cancel`, await authHeaders("user-1") ); expect(cancel.status).toBe(200); @@ -1295,7 +1295,7 @@ describe("Extensions API v2", () => { it("rejects cancelling a claim that belongs to someone else", async () => { await seedUnownedDeveloper("legacy-developer"); const claim = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); @@ -1303,7 +1303,7 @@ describe("Extensions API v2", () => { .result.id; const cancel = await post( - `/extensions/v2/developers/claims/${claimId}/cancel`, + `/v2/developers/claims/${claimId}/cancel`, await authHeaders("user-2") ); expect(cancel.status).toBe(404); @@ -1314,20 +1314,20 @@ describe("Extensions API v2", () => { await seedUnownedDeveloper("legacy-developer"); await insertUser(db, { id: "mod-1", is_moderator: 1 }); const claim = await post( - "/extensions/v2/developers/legacy-developer/claim", + "/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); const claimId = ((await claim.json()) as { result: { id: string } }) .result.id; await post( - `/extensions/v2/developers/claims/${claimId}/reject`, + `/v2/developers/claims/${claimId}/reject`, await authHeaders("mod-1"), { review_note: "no" } ); const cancel = await post( - `/extensions/v2/developers/claims/${claimId}/cancel`, + `/v2/developers/claims/${claimId}/cancel`, await authHeaders("user-1") ); expect(cancel.status).toBe(404); diff --git a/test/services/extensions/v2/pending-edits.test.ts b/test/services/extensions/v2/pending-edits.test.ts index 41c53f6c..74a9860c 100644 --- a/test/services/extensions/v2/pending-edits.test.ts +++ b/test/services/extensions/v2/pending-edits.test.ts @@ -24,11 +24,11 @@ vi.mock("@octokit/request", async () => (await import("../../../mocks/octokit")).octokitRequestMock() ); setupExtensionsV2Tests(); -const root = "/extensions/v2/extensions/new-ext"; +const root = "/v2/extensions/new-ext"; async function pending() { await seedDeveloper("dev", "owner"); const response = await post( - "/extensions/v2/extensions", + "/v2/extensions", await authHeaders("owner"), sampleCreate() ); @@ -118,7 +118,7 @@ describe("pending submission replacement", () => { content: JSON.stringify(sampleContent()) }); const response = await put( - "/extensions/v2/extensions/NEW-EXT/revisions/canonical-revision", + "/v2/extensions/NEW-EXT/revisions/canonical-revision", await authHeaders("owner"), sampleContent({ name: "Updated" }) ); diff --git a/test/services/extensions/v2/public-extensions.test.ts b/test/services/extensions/v2/public-extensions.test.ts index 0c3e4302..4deab907 100644 --- a/test/services/extensions/v2/public-extensions.test.ts +++ b/test/services/extensions/v2/public-extensions.test.ts @@ -42,7 +42,7 @@ describe("Extensions API v2", () => { it("lists published extensions with the developer embedded", async () => { await seedOwnedExtension(); - const res = await get("/extensions/v2/extensions", {}); + const res = await get("/v2/extensions", {}); expect(res.status).toBe(200); const body = (await res.json()) as { result: Array<{ @@ -63,7 +63,7 @@ describe("Extensions API v2", () => { it("marks unowned public developers as unclaimed", async () => { await seedExtensions(["legacy-extension"]); - const res = await get("/extensions/v2/extensions", {}); + const res = await get("/v2/extensions", {}); expect(res.status).toBe(200); const body = (await res.json()) as { result: Array<{ developer: { unclaimed: boolean } }>; @@ -74,11 +74,11 @@ describe("Extensions API v2", () => { it("filters by type", async () => { await seedOwnedExtension(); - const matching = await get("/extensions/v2/extensions?type=module", {}); + const matching = await get("/v2/extensions?type=module", {}); const matchingBody = (await matching.json()) as { result: unknown[] }; expect(matchingBody.result).toHaveLength(1); - const nonMatching = await get("/extensions/v2/extensions?type=theme", {}); + const nonMatching = await get("/v2/extensions?type=theme", {}); const nonMatchingBody = (await nonMatching.json()) as { result: unknown[]; }; @@ -86,7 +86,7 @@ describe("Extensions API v2", () => { }); it("422s on an invalid type filter", async () => { - const res = await get("/extensions/v2/extensions?type=not-a-type", {}); + const res = await get("/v2/extensions?type=not-a-type", {}); expect(res.status).toBe(422); }); @@ -94,14 +94,14 @@ describe("Extensions API v2", () => { await seedOwnedExtension(); const matching = await get( - "/extensions/v2/extensions?developer_id=owner-developer", + "/v2/extensions?developer_id=owner-developer", {} ); const matchingBody = (await matching.json()) as { result: unknown[] }; expect(matchingBody.result).toHaveLength(1); const nonMatching = await get( - "/extensions/v2/extensions?developer_id=someone-else", + "/v2/extensions?developer_id=someone-else", {} ); const nonMatchingBody = (await nonMatching.json()) as { @@ -113,7 +113,7 @@ describe("Extensions API v2", () => { it("returns deterministic first, middle, and final pages", async () => { await seedExtensions(["charlie", "Alpha", "bravo", "delta", "echo"]); - const first = await get("/extensions/v2/extensions?limit=2", {}); + const first = await get("/v2/extensions?limit=2", {}); const firstBody = (await first.json()) as { result: Array<{ id: string }>; pagination: { next_cursor: string | null; has_more: boolean }; @@ -122,7 +122,7 @@ describe("Extensions API v2", () => { expect(firstBody.pagination.has_more).toBe(true); const middle = await get( - `/extensions/v2/extensions?limit=2&cursor=${encodeURIComponent(firstBody.pagination.next_cursor!)}`, + `/v2/extensions?limit=2&cursor=${encodeURIComponent(firstBody.pagination.next_cursor!)}`, {} ); const middleBody = (await middle.json()) as typeof firstBody; @@ -133,7 +133,7 @@ describe("Extensions API v2", () => { expect(middleBody.pagination.has_more).toBe(true); const final = await get( - `/extensions/v2/extensions?limit=2&cursor=${encodeURIComponent(middleBody.pagination.next_cursor!)}`, + `/v2/extensions?limit=2&cursor=${encodeURIComponent(middleBody.pagination.next_cursor!)}`, {} ); const finalBody = (await final.json()) as typeof firstBody; @@ -145,23 +145,20 @@ describe("Extensions API v2", () => { }); it("rejects invalid cursors", async () => { - const res = await get( - "/extensions/v2/extensions?cursor=not-a-cursor", - {} - ); + const res = await get("/v2/extensions?cursor=not-a-cursor", {}); expect(res.status).toBe(422); expect(await res.json()).toMatchObject({ error: { code: "INVALID_CURSOR" } }); - const blank = await get("/extensions/v2/extensions?cursor=", {}); + const blank = await get("/v2/extensions?cursor=", {}); expect(blank.status).toBe(422); }); it("supports UTF-8 extension ids in cursors", async () => { await seedExtensions(["alpha", "zulu", "éclair", "😀"]); - const first = await get("/extensions/v2/extensions?limit=3", {}); + const first = await get("/v2/extensions?limit=3", {}); const firstBody = (await first.json()) as { pagination: { next_cursor: string | null }; }; @@ -169,7 +166,7 @@ describe("Extensions API v2", () => { expect(firstBody.pagination.next_cursor).not.toBeNull(); const second = await get( - `/extensions/v2/extensions?limit=3&cursor=${encodeURIComponent(firstBody.pagination.next_cursor!)}`, + `/v2/extensions?limit=3&cursor=${encodeURIComponent(firstBody.pagination.next_cursor!)}`, {} ); expect(second.status).toBe(200); @@ -181,12 +178,8 @@ describe("Extensions API v2", () => { it("accepts the maximum limit and rejects values above it", async () => { await seedExtensions(["one"]); - expect( - (await get("/extensions/v2/extensions?limit=100", {})).status - ).toBe(200); - expect( - (await get("/extensions/v2/extensions?limit=101", {})).status - ).toBe(422); + expect((await get("/v2/extensions?limit=100", {})).status).toBe(200); + expect((await get("/v2/extensions?limit=101", {})).status).toBe(422); }); }); @@ -194,7 +187,7 @@ describe("Extensions API v2", () => { it("gets a single extension, case-insensitively", async () => { await seedOwnedExtension(); - const res = await get("/extensions/v2/extensions/EXISTING-EXT", {}); + const res = await get("/v2/extensions/EXISTING-EXT", {}); expect(res.status).toBe(200); const body = (await res.json()) as { result: { @@ -218,7 +211,7 @@ describe("Extensions API v2", () => { }); it("404s for an unknown extension", async () => { - const res = await get("/extensions/v2/extensions/no-such-extension", {}); + const res = await get("/v2/extensions/no-such-extension", {}); expect(res.status).toBe(404); }); @@ -237,11 +230,9 @@ describe("Extensions API v2", () => { delist_reason: "Upstream source removed" }); - expect( - (await get("/extensions/v2/extensions/delisted-ext", {})).status - ).toBe(404); + expect((await get("/v2/extensions/delisted-ext", {})).status).toBe(404); - const list = await get("/extensions/v2/extensions", {}); + const list = await get("/v2/extensions", {}); await expect(list.json()).resolves.toMatchObject({ result: [] }); }); }); diff --git a/test/services/extensions/v2/publisher.test.ts b/test/services/extensions/v2/publisher.test.ts index 51c4b565..836ff815 100644 --- a/test/services/extensions/v2/publisher.test.ts +++ b/test/services/extensions/v2/publisher.test.ts @@ -51,11 +51,10 @@ async function m2mHeaders( } async function registerPublisher(canPublish = true): Promise { - return put( - `/extensions/v2/m2m/clients/${CLIENT}`, - await authHeaders(MODERATOR), - { developer_id: DEVELOPER, can_publish: canPublish } - ); + return put(`/v2/m2m/clients/${CLIENT}`, await authHeaders(MODERATOR), { + developer_id: DEVELOPER, + can_publish: canPublish + }); } async function revisionRow(id: string) { @@ -78,7 +77,7 @@ describe("m2m publisher registration", () => { it("requires a moderator", async () => { await insertUser(db, { id: OWNER }); const res = await put( - `/extensions/v2/m2m/clients/${CLIENT}`, + `/v2/m2m/clients/${CLIENT}`, await authHeaders(OWNER), { developer_id: DEVELOPER, can_publish: true } ); @@ -115,13 +114,13 @@ describe("m2m publisher registration", () => { await insertUser(db, { id: MODERATOR, is_moderator: 1 }); await seedUnownedDeveloper("unclaimed-dev"); const missing = await put( - `/extensions/v2/m2m/clients/${CLIENT}`, + `/v2/m2m/clients/${CLIENT}`, await authHeaders(MODERATOR), { developer_id: "no-such-developer", can_publish: true } ); expect(missing.status).toBe(404); const unclaimed = await put( - `/extensions/v2/m2m/clients/${CLIENT}`, + `/v2/m2m/clients/${CLIENT}`, await authHeaders(MODERATOR), { developer_id: "unclaimed-dev", can_publish: true } ); @@ -132,10 +131,7 @@ describe("m2m publisher registration", () => { await insertUser(db, { id: MODERATOR, is_moderator: 1 }); await seedDeveloper(DEVELOPER, OWNER); await registerPublisher(); - const res = await get( - "/extensions/v2/m2m/clients", - await authHeaders(MODERATOR) - ); + const res = await get("/v2/m2m/clients", await authHeaders(MODERATOR)); expect(res.status).toBe(200); const body = (await res.json()) as { result: Array<{ client_id: string }>; @@ -158,7 +154,7 @@ describe("m2m publisher writes", () => { it("creates an extension under the mapped developer", async () => { await registerPublisher(); const res = await post( - "/extensions/v2/extensions", + "/v2/extensions", await m2mHeaders(["extensions:push"]), sampleCreate({ extensionId: "published-ext" }) ); @@ -185,7 +181,7 @@ describe("m2m publisher writes", () => { source: '{"type":"github","repo":"example/existing"}' }); const res = await put( - "/extensions/v2/extensions/existing-ext", + "/v2/extensions/existing-ext", await m2mHeaders(["extensions:push"]), sampleContent({ name: "Edited" }) ); @@ -205,7 +201,7 @@ describe("m2m publisher writes", () => { source: '{"type":"github","repo":"example/existing"}' }); const res = await put( - "/extensions/v2/extensions/existing-ext", + "/v2/extensions/existing-ext", await m2mHeaders(["extensions:push"]), sampleContent({ name: "Edited" }) ); @@ -219,7 +215,7 @@ describe("m2m publisher writes", () => { it("rejects writes without the required scope", async () => { await registerPublisher(); const res = await post( - "/extensions/v2/extensions", + "/v2/extensions", await m2mHeaders(["extensions:publish"]), sampleCreate({ extensionId: "no-push-scope" }) ); @@ -232,12 +228,9 @@ describe("m2m publisher writes", () => { it("rejects a revoked mapping without waiting out the token", async () => { await registerPublisher(); - await del( - `/extensions/v2/m2m/clients/${CLIENT}`, - await authHeaders(MODERATOR) - ); + await del(`/v2/m2m/clients/${CLIENT}`, await authHeaders(MODERATOR)); const res = await post( - "/extensions/v2/extensions", + "/v2/extensions", await m2mHeaders(["extensions:push"]), sampleCreate({ extensionId: "revoked-client" }) ); @@ -249,17 +242,17 @@ describe("m2m publisher writes", () => { // Machine tokens are not a recognized user credential: role-aware reads // fall back to the anonymous projection, and mine requires a bearer. const mine = await get( - "/extensions/v2/extensions?scope=mine", + "/v2/extensions?scope=mine", await m2mHeaders(["extensions:push"]) ); expect(mine.status).toBe(401); const profile = await get( - "/extensions/v2/users/me", + "/v2/users/me", await m2mHeaders(["extensions:push"]) ); expect(profile.status).toBe(403); const queue = await get( - "/extensions/v2/revisions", + "/v2/revisions", await m2mHeaders(["extensions:push"]) ); expect(queue.status).toBe(403); @@ -267,7 +260,7 @@ describe("m2m publisher writes", () => { it("answers 401 for a token bound to another audience", async () => { const res = await post( - "/extensions/v2/extensions", + "/v2/extensions", await m2mHeaders(["extensions:push"], { aud: "https://other.example" }), sampleCreate({ extensionId: "wrong-aud" }) ); @@ -280,7 +273,7 @@ describe("m2m publisher writes", () => { await bumpDeveloperOwnership(db, DEVELOPER, "new-owner-1"); const stale = await post( - "/extensions/v2/extensions", + "/v2/extensions", await m2mHeaders(["extensions:push"]), sampleCreate({ extensionId: "stale-after-transfer" }) ); @@ -292,7 +285,7 @@ describe("m2m publisher writes", () => { const reregistered = await registerPublisher(); expect(reregistered.status).toBe(200); const res = await post( - "/extensions/v2/extensions", + "/v2/extensions", await m2mHeaders(["extensions:push"]), sampleCreate({ extensionId: "rebound-after-transfer" }) ); @@ -313,7 +306,7 @@ describe("m2m publisher auto-approval", () => { it("publishes a pending revision when can_publish is set", async () => { await registerPublisher(true); const created = await post( - "/extensions/v2/extensions", + "/v2/extensions", await m2mHeaders(["extensions:push", "extensions:publish"]), sampleCreate({ extensionId: "auto-published" }) ); @@ -324,7 +317,7 @@ describe("m2m publisher auto-approval", () => { const revisionId = createdBody.result.revision_id; const approved = await post( - `/extensions/v2/extensions/auto-published/revisions/${revisionId}/approve?notify=false`, + `/v2/extensions/auto-published/revisions/${revisionId}/approve?notify=false`, await m2mHeaders(["extensions:publish"]), {} ); @@ -349,7 +342,7 @@ describe("m2m publisher auto-approval", () => { it("rejects approval when the mapping lacks can_publish", async () => { await registerPublisher(false); const created = await post( - "/extensions/v2/extensions", + "/v2/extensions", await m2mHeaders(["extensions:push"]), sampleCreate({ extensionId: "no-auto-publish" }) ); @@ -360,7 +353,7 @@ describe("m2m publisher auto-approval", () => { const revisionId = createdBody.result.revision_id; const approved = await post( - `/extensions/v2/extensions/no-auto-publish/revisions/${revisionId}/approve?notify=false`, + `/v2/extensions/no-auto-publish/revisions/${revisionId}/approve?notify=false`, await m2mHeaders(["extensions:publish"]), {} ); @@ -374,7 +367,7 @@ describe("m2m publisher auto-approval", () => { it("rejects approval without the publish scope", async () => { await registerPublisher(true); const created = await post( - "/extensions/v2/extensions", + "/v2/extensions", await m2mHeaders(["extensions:push"]), sampleCreate({ extensionId: "scope-gate" }) ); @@ -385,7 +378,7 @@ describe("m2m publisher auto-approval", () => { const revisionId = createdBody.result.revision_id; const approved = await post( - `/extensions/v2/extensions/scope-gate/revisions/${revisionId}/approve?notify=false`, + `/v2/extensions/scope-gate/revisions/${revisionId}/approve?notify=false`, await m2mHeaders(["extensions:push"]), {} ); @@ -399,12 +392,12 @@ describe("m2m publisher auto-approval", () => { it("keeps moderator approval working alongside the carve-out", async () => { await registerPublisher(true); await post( - "/extensions/v2/extensions", + "/v2/extensions", await m2mHeaders(["extensions:push"]), sampleCreate({ extensionId: "mod-approved" }) ); const pending = await get( - "/extensions/v2/revisions?status=pending", + "/v2/revisions?status=pending", await authHeaders(MODERATOR) ); expect(pending.status).toBe(200); @@ -413,7 +406,7 @@ describe("m2m publisher auto-approval", () => { }; expect(result).toHaveLength(1); const approved = await post( - `/extensions/v2/extensions/mod-approved/revisions/${result[0].id}/approve?notify=false`, + `/v2/extensions/mod-approved/revisions/${result[0].id}/approve?notify=false`, await authHeaders(MODERATOR), {} ); @@ -444,7 +437,7 @@ describe("m2m write pacing", () => { } }; await post( - "/extensions/v2/extensions", + "/v2/extensions", await m2mHeaders(["extensions:push"]), sampleCreate({ extensionId: "paced-ext" }) ); diff --git a/test/services/extensions/v2/resource-bounds.test.ts b/test/services/extensions/v2/resource-bounds.test.ts index 6d031ced..2a98e6b0 100644 --- a/test/services/extensions/v2/resource-bounds.test.ts +++ b/test/services/extensions/v2/resource-bounds.test.ts @@ -52,7 +52,7 @@ setupExtensionsV2Tests(); async function withdraw(id: string, user = "owner") { const ctx = createExecutionContext(); const result = await app.request( - `/extensions/v2/extensions/${id}`, + `/v2/extensions/${id}`, { method: "DELETE", headers: await authHeaders(user) }, env, ctx @@ -61,7 +61,7 @@ async function withdraw(id: string, user = "owner") { return result; } async function create(id: string) { - return post("/extensions/v2/extensions", await authHeaders("owner"), { + return post("/v2/extensions", await authHeaders("owner"), { ...sampleCreate(), id }); @@ -283,7 +283,7 @@ describe("Extension resource admission", () => { } }); const response = await app.request( - "/extensions/v2/extensions", + "/v2/extensions", { method: "POST", headers: await authHeaders("owner"), body: stream }, env ); @@ -309,7 +309,7 @@ describe("Extension resource admission", () => { } }); const response = await app.request( - "/extensions/v2/extensions", + "/v2/extensions", { method: "POST", headers: await authHeaders("owner"), body: stream }, env ); @@ -321,11 +321,11 @@ describe("Extension resource admission", () => { const id = "e".repeat(201); expect((await create(id)).status).toBe(422); await insertExtension(db, { id, developer_id: "developer" }); - expect((await get(`/extensions/v2/extensions/${id}`, {})).status).toBe(200); + expect((await get(`/v2/extensions/${id}`, {})).status).toBe(200); expect( ( await put( - `/extensions/v2/extensions/${id}`, + `/v2/extensions/${id}`, await authHeaders("owner"), sampleContent() ) @@ -370,7 +370,7 @@ describe("Extension resource admission", () => { } }); const res = await app.request( - "/extensions/v2/extensions", + "/v2/extensions", { method: "POST", headers, body: stream }, env ); @@ -390,7 +390,7 @@ describe("Extension resource admission", () => { expect( ( await app.request( - "/extensions/v2/extensions", + "/v2/extensions", { method: "POST", headers, body: padded }, env ) @@ -399,7 +399,7 @@ describe("Extension resource admission", () => { expect( ( await app.request( - "/extensions/v2/extensions", + "/v2/extensions", { method: "POST", headers, body: padded + " " }, env ) @@ -408,8 +408,8 @@ describe("Extension resource admission", () => { }); it("bounds raw moderator corrections and rejected unknown-field padding", async () => { for (const path of [ - "/extensions/v2/extensions", - "/extensions/v2/extensions/live/moderator-correct" + "/v2/extensions", + "/v2/extensions/live/moderator-correct" ]) { const response = await app.request( path, @@ -445,11 +445,9 @@ describe("Extension resource admission", () => { env.EXTENSION_WRITE_RATE_LIMITER = { limit: async ({ key }) => ({ success: !key.startsWith("account:") }) }; - const res = await post( - "/extensions/v2/extensions", - await authHeaders("owner"), - { invalid: true } - ); + const res = await post("/v2/extensions", await authHeaders("owner"), { + invalid: true + }); expect(res.status).toBe(429); expect(res.headers.get("Retry-After")).toBe("60"); env.EXTENSION_WRITE_RATE_LIMITER = { @@ -476,12 +474,12 @@ describe("Extension resource admission", () => { ? await create("unavailable") : operation === "edit" ? await put( - "/extensions/v2/extensions/live", + "/v2/extensions/live", await authHeaders("owner"), sampleContent() ) : await post( - "/extensions/v2/extensions/live/moderator-correct", + "/v2/extensions/live/moderator-correct", await authHeaders("mod"), { ...sampleContent(), correction_note: "Fix" } ); @@ -503,11 +501,7 @@ describe("Extension resource admission", () => { await owned(); const results = await Promise.all([ ...Array.from({ length: 6 }, (_, i) => create(`concurrent-${i}`)), - put( - "/extensions/v2/extensions/live", - await authHeaders("owner"), - sampleContent() - ) + put("/v2/extensions/live", await authHeaders("owner"), sampleContent()) ]); expect( results.filter((r) => r.status === 201 || r.status === 202) @@ -645,7 +639,7 @@ describe("Extension resource admission", () => { .bind(MAX_ACCOUNT_BYTES) .run(); const response = await post( - `/extensions/v2/extensions/publication/revisions/${revision}/approve?notify=false`, + `/v2/extensions/publication/revisions/${revision}/approve?notify=false`, await authHeaders("mod"), {} ); @@ -670,13 +664,7 @@ describe("Extension resource admission", () => { const content = { ...sampleCreate(), id: "multibyte", readme: "😀é" }; const { id: _id, ...stored } = content; expect( - ( - await post( - "/extensions/v2/extensions", - await authHeaders("owner"), - content - ) - ).status + (await post("/v2/extensions", await authHeaders("owner"), content)).status ).toBe(201); const bytes = new TextEncoder().encode(JSON.stringify(stored)).byteLength; expect( @@ -720,10 +708,7 @@ describe("Bounded revision reads and maintenance", () => { sql.push(query); }); const headers = await authHeaders("mod"); - const first = await get( - "/extensions/v2/revisions?status=rejected&limit=2", - headers - ); + const first = await get("/v2/revisions?status=rejected&limit=2", headers); const page = (await first.json()) as { result: Array<{ id: string; content?: unknown }>; pagination: { next_cursor: string }; @@ -731,7 +716,7 @@ describe("Bounded revision reads and maintenance", () => { expect(page.result.map((r) => r.id)).toEqual(["a", "b"]); expect(page.result.every((r) => !("content" in r))).toBe(true); const second = await get( - `/extensions/v2/revisions?status=rejected&limit=2&cursor=${encodeURIComponent(page.pagination.next_cursor)}`, + `/v2/revisions?status=rejected&limit=2&cursor=${encodeURIComponent(page.pagination.next_cursor)}`, headers ); await expect(second.json()).resolves.toMatchObject({ @@ -746,13 +731,13 @@ describe("Bounded revision reads and maintenance", () => { expect( ( await get( - "/extensions/v2/extensions/live/revisions/a", + "/v2/extensions/live/revisions/a", await authHeaders("intruder") ) ).status ).toBe(403); const detail = await get( - "/extensions/v2/extensions/live/revisions/a", + "/v2/extensions/live/revisions/a", await authHeaders("owner") ); await expect(detail.json()).resolves.toMatchObject({ @@ -782,7 +767,7 @@ describe("Bounded revision reads and maintenance", () => { created_at: "2000-01-01" }); const detail = await get( - `/extensions/v2/extensions/live/revisions/${id}`, + `/v2/extensions/live/revisions/${id}`, await authHeaders("owner") ); expect(detail.status).toBe(409); @@ -790,7 +775,7 @@ describe("Bounded revision reads and maintenance", () => { error: { code: "CONTENT_UNAVAILABLE" } }); const list = await get( - "/extensions/v2/extensions/live/revisions", + "/v2/extensions/live/revisions", await authHeaders("owner") ); const body = (await list.json()) as { @@ -814,7 +799,7 @@ describe("Bounded revision reads and maintenance", () => { status: "rejected" }); const detail = await get( - "/extensions/v2/extensions/live/revisions/unicode-tag", + "/v2/extensions/live/revisions/unicode-tag", await authHeaders("owner") ); expect(detail.status).toBe(200); @@ -893,7 +878,7 @@ describe("Bounded revision reads and maintenance", () => { .first("bytes") ).toBe(after); const detail = await get( - "/extensions/v2/extensions/live/revisions/old", + "/v2/extensions/live/revisions/old", await authHeaders("owner") ); await expect(detail.json()).resolves.toMatchObject({ @@ -956,10 +941,7 @@ describe("Bounded revision reads and maintenance", () => { reviewed_at: "2000-01-01" }); const headers = await authHeaders("mod"); - const queue = await get( - "/extensions/v2/revisions?status=rejected&limit=100", - headers - ); + const queue = await get("/v2/revisions?status=rejected&limit=100", headers); const text = await queue.text(); expect(queue.status).toBe(200); const responseBytes = new TextEncoder().encode(text).byteLength; @@ -970,10 +952,7 @@ describe("Bounded revision reads and maintenance", () => { expect(result.every((row) => row.description === content.description)).toBe( true ); - const detail = await get( - "/extensions/v2/extensions/live/revisions/max-0", - headers - ); + const detail = await get("/v2/extensions/live/revisions/max-0", headers); const detailBytes = new TextEncoder().encode( await detail.text() ).byteLength; @@ -1206,7 +1185,7 @@ describe("Bounded revision reads and maintenance", () => { db.prepare(trigger!) ]); const list = await get( - "/extensions/v2/extensions/live/revisions", + "/v2/extensions/live/revisions", await authHeaders("owner") ); await expect(list.json()).resolves.toMatchObject({ @@ -1219,7 +1198,7 @@ describe("Bounded revision reads and maintenance", () => { ] }); const detail = await get( - "/extensions/v2/extensions/live/revisions/legacy", + "/v2/extensions/live/revisions/legacy", await authHeaders("owner") ); expect(detail.status).toBe(409); From 40f537d8b585830787757c6b4f717ea2965a6797 Mon Sep 17 00:00:00 2001 From: Adam Daley Date: Thu, 5 Nov 2026 18:04:07 +0000 Subject: [PATCH 2/2] Retain the extension API reference title --- src/services/extensions/v2/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/extensions/v2/index.ts b/src/services/extensions/v2/index.ts index 60af455e..c1bcfa29 100644 --- a/src/services/extensions/v2/index.ts +++ b/src/services/extensions/v2/index.ts @@ -104,14 +104,14 @@ extensionsV2.route( (cachedOpenApiDocument ??= extensionsV2.getOpenAPI31Document({ openapi: "3.1.0", info: { - title: "FOSSBilling API (extensionsV2)", + title: "FOSSBilling Extensions API (v2)", version: "2.0.0", description: "Self-service extension publishing, ownership, moderation, and public browsing. v1 (/extensions/v1) remains available for existing integrations." }, servers: [{ url: "/v2" }] })), - pageTitle: "FOSSBilling API (extensionsV2)", + pageTitle: "FOSSBilling Extensions API (v2)", agent: { disabled: true }, documentDownloadType: "none", hideClientButton: true,