| 1 | import { afterEach, describe, expect, it, vi } from "vitest"; |
| 2 | import registryApp from "../app"; |
| 3 | import type { Bindings } from "../env"; |
| 4 | import type { PackageRow } from "../types"; |
| 5 | |
| 6 | const oldRevision: PackageRow = { |
| 7 | id: 42, |
| 8 | kind: "plugin", |
| 9 | scope_handle: "publisher", |
| 10 | name: "devkit", |
| 11 | slug: "publisher/devkit", |
| 12 | summary: "Developer tools", |
| 13 | description: "", |
| 14 | source: "https://github.com/o/r", |
| 15 | install_kind: "plugin", |
| 16 | homepage: "", |
| 17 | repo_url: "https://github.com/o/r", |
| 18 | tags: "tool", |
| 19 | latest_version: "2.7.1", |
| 20 | install_count: 0, |
| 21 | star_count: 0, |
| 22 | verified: 0, |
| 23 | status: "pending", |
| 24 | publisher_id: 7, |
| 25 | created_at: "2026-07-22T00:00:00.000Z", |
| 26 | updated_at: "2026-07-22T00:30:00.000Z", |
| 27 | }; |
| 28 | |
| 29 | function approvalDB(current: PackageRow, approved: PackageRow | null) { |
| 30 | const statements: { sql: string; values: unknown[] }[] = []; |
| 31 | const db = { |
| 32 | prepare(sql: string) { |
| 33 | let values: unknown[] = []; |
| 34 | const statement = { |
| 35 | bind(...bound: unknown[]) { |
| 36 | values = bound; |
| 37 | return statement; |
| 38 | }, |
| 39 | async first<T>() { |
| 40 | statements.push({ sql, values }); |
| 41 | if (sql.startsWith("UPDATE packages SET status")) return approved as T | null; |
| 42 | if (sql.startsWith("SELECT * FROM packages")) return current as T; |
| 43 | return null; |
| 44 | }, |
| 45 | async run() { |
| 46 | statements.push({ sql, values }); |
| 47 | return { meta: { changes: 1 } }; |
| 48 | }, |
| 49 | }; |
| 50 | return statement; |
| 51 | }, |
| 52 | }; |
| 53 | return { db: db as unknown as D1Database, statements }; |
| 54 | } |
| 55 | |
| 56 | function bindings(db: D1Database): Bindings { |
| 57 | return { |
| 58 | DB: db, |
| 59 | ACCOUNTS_ORIGIN: "https://id.reasonix.test", |
| 60 | APP_ORIGIN: "https://reasonix.test", |
| 61 | ALLOWED_ORIGINS: "https://reasonix.test", |
| 62 | }; |
| 63 | } |
| 64 | |
| 65 | function approvalRequest(body: object): Request { |
| 66 | return new Request("https://registry.reasonix.test/v1/admin/packages/publisher/devkit/approve", { |
| 67 | method: "POST", |
| 68 | headers: { cookie: "rxid=test", "content-type": "application/json" }, |
| 69 | body: JSON.stringify(body), |
| 70 | }); |
| 71 | } |
| 72 | |
| 73 | afterEach(() => vi.unstubAllGlobals()); |
| 74 | |
| 75 | describe("admin package approval", () => { |
| 76 | it("fails closed when an older review page omits the revision", async () => { |
| 77 | vi.stubGlobal( |
| 78 | "fetch", |
| 79 | vi.fn(async () => |
| 80 | Response.json({ user: { id: 1, handle: "admin", role: "admin", emailVerified: true } }), |
| 81 | ), |
| 82 | ); |
| 83 | const { db, statements } = approvalDB(oldRevision, null); |
| 84 | |
| 85 | const response = await registryApp.fetch(approvalRequest({}), bindings(db)); |
| 86 | |
| 87 | expect(response.status).toBe(400); |
| 88 | await expect(response.json()).resolves.toEqual({ |
| 89 | error: { |
| 90 | code: "invalid_review_revision", |
| 91 | message: "Approval requires the reviewed package revision.", |
| 92 | }, |
| 93 | }); |
| 94 | expect(statements).toHaveLength(0); |
| 95 | }); |
| 96 | |
| 97 | it("rejects a stale review after the publisher submits a newer version", async () => { |
| 98 | vi.stubGlobal( |
| 99 | "fetch", |
| 100 | vi.fn(async () => |
| 101 | Response.json({ user: { id: 1, handle: "admin", role: "admin", emailVerified: true } }), |
| 102 | ), |
| 103 | ); |
| 104 | const current = { ...oldRevision, latest_version: "2.7.2", updated_at: "2026-07-22T00:45:00.000Z" }; |
| 105 | const { db, statements } = approvalDB(current, null); |
| 106 | |
| 107 | const response = await registryApp.fetch( |
| 108 | approvalRequest({ |
| 109 | expectedVersion: oldRevision.latest_version, |
| 110 | expectedUpdatedAt: oldRevision.updated_at, |
| 111 | expectedStatus: oldRevision.status, |
| 112 | }), |
| 113 | bindings(db), |
| 114 | ); |
| 115 | |
| 116 | expect(response.status).toBe(409); |
| 117 | await expect(response.json()).resolves.toEqual({ |
| 118 | error: { |
| 119 | code: "stale_review", |
| 120 | message: "Package changed since it was reviewed. Refresh and review the latest version.", |
| 121 | }, |
| 122 | }); |
| 123 | expect(statements.some(({ sql }) => sql.startsWith("INSERT INTO events"))).toBe(false); |
| 124 | }); |
| 125 | |
| 126 | it("publishes when the submitted review revision still matches", async () => { |
| 127 | vi.stubGlobal( |
| 128 | "fetch", |
| 129 | vi.fn(async () => |
| 130 | Response.json({ user: { id: 1, handle: "admin", role: "admin", emailVerified: true } }), |
| 131 | ), |
| 132 | ); |
| 133 | const approved = { ...oldRevision, status: "active", updated_at: "2026-07-22T01:00:00.000Z" }; |
| 134 | const { db, statements } = approvalDB(oldRevision, approved); |
| 135 | |
| 136 | const response = await registryApp.fetch( |
| 137 | approvalRequest({ |
| 138 | expectedVersion: oldRevision.latest_version, |
| 139 | expectedUpdatedAt: oldRevision.updated_at, |
| 140 | expectedStatus: oldRevision.status, |
| 141 | }), |
| 142 | bindings(db), |
| 143 | ); |
| 144 | |
| 145 | expect(response.status).toBe(200); |
| 146 | const body = (await response.json()) as { package: { status: string; latestVersion: string } }; |
| 147 | expect(body.package).toMatchObject({ status: "active", latestVersion: "2.7.1" }); |
| 148 | expect(statements.some(({ sql }) => sql.startsWith("INSERT INTO events"))).toBe(true); |
| 149 | }); |
| 150 | }); |
| 151 |