import { env, exports } from "cloudflare:workers"; import { beforeAll, describe, expect, it } from "vitest"; const SELF = exports.default; import { makeAuth } from "@/worker/auth"; import { ISSUER, signInForCookie, testHeaders } from "./helpers"; // auth/index.ts has a `before` hook on /admin/set-role / ban-user / // remove-user that returns FORBIDDEN if the caller targets themselves // (set-role only allows the no-op of keeping role=admin; ban/remove // always 403 self). This test exercises the load-bearing case: an // admin demoting themselves to user via the raw /api/auth/admin/set-role // path; without the guard, an operator could lock themselves out. describe("admin self-modify guard", () => { const cred = { email: "self-modify-admin@example.com", password: "correct-horse-battery-staple", name: "Self Modify Admin", }; let cookie: string; let userId: string; beforeAll(async () => { const auth = makeAuth(env, { baseURL: ISSUER, issuer: ISSUER }); await auth.api.signUpEmail({ body: cred, asResponse: false }); await env.DB.prepare("UPDATE users SET role = ? WHERE email = ?").bind("admin", cred.email).run(); cookie = await signInForCookie(cred.email, cred.password, "10.50.0.1"); const row = await env.DB.prepare("SELECT id FROM users WHERE email = ?").bind(cred.email).first<{ id: string }>(); if (!row) throw new Error("seeded admin missing"); userId = row.id; }); it("blocks self-demotion via /admin/set-role with 403", async () => { // `origin` is required by Better Auth's CSRF gate for state-changing // auth endpoints; without it we get 403 MISSING_OR_NULL_ORIGIN before // the self-modify hook runs. const res = await SELF.fetch(`${ISSUER}/api/auth/admin/set-role`, { method: "POST", headers: testHeaders({ cookie }), body: JSON.stringify({ userId, role: "user" }), }); expect(res.status).toBe(403); const body = (await res.json()) as { code?: string }; expect(body.code).toBe("CANNOT_SELF_DEMOTE"); // Confirm the role didn't actually change. const row = await env.DB.prepare("SELECT role FROM users WHERE id = ?").bind(userId).first<{ role: string }>(); expect(row?.role).toBe("admin"); }); it("allows self set-role to admin (no-op) without 403", async () => { const res = await SELF.fetch(`${ISSUER}/api/auth/admin/set-role`, { method: "POST", headers: testHeaders({ cookie }), body: JSON.stringify({ userId, role: "admin" }), }); expect([200, 204]).toContain(res.status); }); it('blocks self-demotion via array role body (`role: ["user"]`)', async () => { // Better Auth's set-role schema accepts `role: string | string[]`. // The self-demote guard normalizes both shapes so an admin posting // an array form against their own user id still hits CANNOT_SELF_DEMOTE. const res = await SELF.fetch(`${ISSUER}/api/auth/admin/set-role`, { method: "POST", headers: testHeaders({ cookie }), body: JSON.stringify({ userId, role: ["user"] }), }); expect(res.status).toBe(403); const body = (await res.json()) as { code?: string }; expect(body.code).toBe("CANNOT_SELF_DEMOTE"); const row = await env.DB.prepare("SELECT role FROM users WHERE id = ?").bind(userId).first<{ role: string }>(); expect(row?.role).toBe("admin"); }); });