import { env } from "cloudflare:workers"; import { beforeAll, describe, expect, it } from "vitest"; import { DEFAULT_PASSWORD, ISSUER, SELF, signInForCookie, signUpAdmin, signUpTestUser, testHeaders, type JsonErrorBody, type TestCredential, } from "./helpers"; interface LauncherAppApiShape { id: string; name: string; url: string; icon: string | null; tint: string | null; enabled: boolean; } describe("admin launcher-apps CRUD", () => { const adminCred = { email: "admin-launcher@example.com", password: DEFAULT_PASSWORD, name: "Admin Launcher Tester", } satisfies TestCredential; const userCred = { email: "user-launcher@example.com", password: DEFAULT_PASSWORD, name: "Regular Launcher User", } satisfies TestCredential; beforeAll(async () => { await signUpAdmin(adminCred); await signUpTestUser(userCred); }); it("rejects unauthenticated requests with 401 on every method", async () => { const get = await SELF.fetch(`${ISSUER}/api/admin/launcher-apps`); expect(get.status).toBe(401); const post = await SELF.fetch(`${ISSUER}/api/admin/launcher-apps`, { method: "POST", headers: testHeaders(), body: JSON.stringify({ name: "anvil", url: "https://anvil.limic.dev" }), }); expect(post.status).toBe(401); const patch = await SELF.fetch(`${ISSUER}/api/admin/launcher-apps/abc`, { method: "PATCH", headers: testHeaders(), body: JSON.stringify({ name: "anvil" }), }); expect(patch.status).toBe(401); const del = await SELF.fetch(`${ISSUER}/api/admin/launcher-apps/abc`, { method: "DELETE", headers: testHeaders(), }); expect(del.status).toBe(401); }); it("rejects non-admin users with 403", async () => { const cookie = await signInForCookie(userCred.email, userCred.password, "10.40.0.1"); const list = await SELF.fetch(`${ISSUER}/api/admin/launcher-apps`, { headers: { cookie } }); expect(list.status).toBe(403); const create = await SELF.fetch(`${ISSUER}/api/admin/launcher-apps`, { method: "POST", headers: testHeaders({ cookie }), body: JSON.stringify({ name: "anvil", url: "https://anvil.limic.dev" }), }); expect(create.status).toBe(403); }); it("creates, lists (including disabled), updates, and deletes a launcher app", async () => { const cookie = await signInForCookie(adminCred.email, adminCred.password, "10.40.0.1"); const createRes = await SELF.fetch(`${ISSUER}/api/admin/launcher-apps`, { method: "POST", headers: testHeaders({ cookie }), body: JSON.stringify({ name: "anvil", url: "https://anvil.limic.dev", icon: "Hammer", tint: "sky", }), }); expect(createRes.status).toBe(201); const created = (await createRes.json()) as LauncherAppApiShape; expect(created.id).toBeTruthy(); expect(created.enabled).toBe(true); expect(created.icon).toBe("Hammer"); expect(created.tint).toBe("sky"); const disableRes = await SELF.fetch(`${ISSUER}/api/admin/launcher-apps/${encodeURIComponent(created.id)}`, { method: "PATCH", headers: testHeaders({ cookie }), body: JSON.stringify({ enabled: false }), }); expect(disableRes.status).toBe(200); const disabled = (await disableRes.json()) as LauncherAppApiShape; expect(disabled.enabled).toBe(false); const listRes = await SELF.fetch(`${ISSUER}/api/admin/launcher-apps`, { headers: { cookie } }); expect(listRes.status).toBe(200); const listed = (await listRes.json()) as LauncherAppApiShape[]; const ours = listed.find((row) => row.id === created.id); expect(ours).toBeDefined(); expect(ours?.enabled).toBe(false); const renameRes = await SELF.fetch(`${ISSUER}/api/admin/launcher-apps/${encodeURIComponent(created.id)}`, { method: "PATCH", headers: testHeaders({ cookie }), body: JSON.stringify({ name: "anvil-renamed", icon: null, enabled: true }), }); expect(renameRes.status).toBe(200); const renamed = (await renameRes.json()) as LauncherAppApiShape; expect(renamed.name).toBe("anvil-renamed"); expect(renamed.icon).toBeNull(); expect(renamed.enabled).toBe(true); // tint not in PATCH body -> unchanged expect(renamed.tint).toBe("sky"); const delRes = await SELF.fetch(`${ISSUER}/api/admin/launcher-apps/${encodeURIComponent(created.id)}`, { method: "DELETE", headers: testHeaders({ cookie }), }); expect(delRes.status).toBe(204); const listAfter = (await ( await SELF.fetch(`${ISSUER}/api/admin/launcher-apps`, { headers: { cookie } }) ).json()) as LauncherAppApiShape[]; expect(listAfter.some((r) => r.id === created.id)).toBe(false); }); it("accepts localhost subdomain URLs with ports", async () => { const cookie = await signInForCookie(adminCred.email, adminCred.password, "10.40.0.2"); const createRes = await SELF.fetch(`${ISSUER}/api/admin/launcher-apps`, { method: "POST", headers: testHeaders({ cookie }), body: JSON.stringify({ name: "local acme", url: "http://acme.localhost:5176" }), }); expect(createRes.status).toBe(201); const created = (await createRes.json()) as LauncherAppApiShape; expect(created.url).toBe("http://acme.localhost:5176"); const patchRes = await SELF.fetch(`${ISSUER}/api/admin/launcher-apps/${encodeURIComponent(created.id)}`, { method: "PATCH", headers: testHeaders({ cookie }), body: JSON.stringify({ url: "https://acme.localhost:5176" }), }); expect(patchRes.status).toBe(200); const patched = (await patchRes.json()) as LauncherAppApiShape; expect(patched.url).toBe("https://acme.localhost:5176"); await env.DB.prepare("DELETE FROM launcher_apps WHERE id = ?").bind(created.id).run(); }); it("rejects unsafe URL schemes with 400", async () => { const cookie = await signInForCookie(adminCred.email, adminCred.password, "10.40.0.1"); const res = await SELF.fetch(`${ISSUER}/api/admin/launcher-apps`, { method: "POST", headers: testHeaders({ cookie }), body: JSON.stringify({ name: "xss", url: "javascript:alert(1)" }), }); expect(res.status).toBe(400); expect(((await res.json()) as JsonErrorBody).error).toBe("invalid_url"); }); it("rejects unknown icon and tint names with 400", async () => { const cookie = await signInForCookie(adminCred.email, adminCred.password, "10.40.0.1"); const badIcon = await SELF.fetch(`${ISSUER}/api/admin/launcher-apps`, { method: "POST", headers: testHeaders({ cookie }), body: JSON.stringify({ name: "bad-icon", url: "https://example.com", icon: "NotAnIcon" }), }); expect(badIcon.status).toBe(400); expect(((await badIcon.json()) as JsonErrorBody).error).toBe("invalid_icon"); const badTint = await SELF.fetch(`${ISSUER}/api/admin/launcher-apps`, { method: "POST", headers: testHeaders({ cookie }), body: JSON.stringify({ name: "bad-tint", url: "https://example.com", tint: "neon" }), }); expect(badTint.status).toBe(400); expect(((await badTint.json()) as JsonErrorBody).error).toBe("invalid_tint"); }); it("rejects non-boolean enabled values with 400", async () => { const cookie = await signInForCookie(adminCred.email, adminCred.password, "10.40.0.1"); const badCreate = await SELF.fetch(`${ISSUER}/api/admin/launcher-apps`, { method: "POST", headers: testHeaders({ cookie }), body: JSON.stringify({ name: "bad-enabled", url: "https://example.com", enabled: "false" }), }); expect(badCreate.status).toBe(400); expect(((await badCreate.json()) as JsonErrorBody).error).toBe("invalid_enabled"); // Set up a valid row, then try to PATCH it with a non-boolean // `enabled`. The truthy-string regression would coerce "false" to // true; the strict check rejects with 400 instead. const okCreate = await SELF.fetch(`${ISSUER}/api/admin/launcher-apps`, { method: "POST", headers: testHeaders({ cookie }), body: JSON.stringify({ name: "patch-bad-enabled", url: "https://example.com" }), }); expect(okCreate.status).toBe(201); const created = (await okCreate.json()) as LauncherAppApiShape; const badPatch = await SELF.fetch(`${ISSUER}/api/admin/launcher-apps/${encodeURIComponent(created.id)}`, { method: "PATCH", headers: testHeaders({ cookie }), body: JSON.stringify({ enabled: "false" }), }); expect(badPatch.status).toBe(400); expect(((await badPatch.json()) as JsonErrorBody).error).toBe("invalid_enabled"); // cleanup await env.DB.prepare("DELETE FROM launcher_apps WHERE id = ?").bind(created.id).run(); }); it("rejects empty PATCH bodies with 400", async () => { const cookie = await signInForCookie(adminCred.email, adminCred.password, "10.40.0.1"); const createRes = await SELF.fetch(`${ISSUER}/api/admin/launcher-apps`, { method: "POST", headers: testHeaders({ cookie }), body: JSON.stringify({ name: "empty-patch", url: "https://example.com" }), }); expect(createRes.status).toBe(201); const created = (await createRes.json()) as LauncherAppApiShape; const res = await SELF.fetch(`${ISSUER}/api/admin/launcher-apps/${encodeURIComponent(created.id)}`, { method: "PATCH", headers: testHeaders({ cookie }), body: JSON.stringify({}), }); expect(res.status).toBe(400); expect(((await res.json()) as JsonErrorBody).error).toBe("invalid_body"); // cleanup await env.DB.prepare("DELETE FROM launcher_apps WHERE id = ?").bind(created.id).run(); }); it("returns 404 when updating a non-existent launcher app", async () => { const cookie = await signInForCookie(adminCred.email, adminCred.password, "10.40.0.1"); const res = await SELF.fetch(`${ISSUER}/api/admin/launcher-apps/nope-not-real`, { method: "PATCH", headers: testHeaders({ cookie }), body: JSON.stringify({ name: "ghost" }), }); expect(res.status).toBe(404); expect(((await res.json()) as JsonErrorBody).error).toBe("not_found"); }); });