import { afterEach, expect, test } from "bun:test"; import { setupApp } from "./app-helpers"; let env: Awaited>; afterEach(() => env?.stop()); test("anonymous gets 401, learners 403", async () => { env = await setupApp(); expect((await env.call(null, "GET", "/api/studio/courses")).status).toBe(401); expect((await env.call("lea", "GET", "/api/studio/courses")).status).toBe(403); expect((await env.call("lea", "POST", "/api/studio/courses", { id: "x", title: "X" })).status).toBe(403); }); const DBS: [string, string][] = [["sqlite", "sqlite://:memory:"], ...(process.env.STC_TEST_PG_URL ? [["postgres", process.env.STC_TEST_PG_URL] as [string, string]] : [])]; test.each(DBS)("creating a course records the creator in course_authors; a per-course author without the role can edit it [%s]", async (_name, url) => { env = await setupApp([], url); expect((await env.call("ada", "POST", "/api/studio/courses", { id: "c1", title: "C" })).status).toBe(201); const rows = await env.db.sql`SELECT course_id, user_id FROM course_authors`; expect(rows.map((r: any) => [r.course_id, r.user_id])).toEqual([["c1", "u-ada"]]); await env.db.sql`INSERT INTO course_authors (course_id, user_id) VALUES (${"c1"}, ${"u-lea"})`; const list = (await env.call("lea", "GET", "/api/studio/courses")).json; expect(list.courses.map((c: any) => c.id)).toEqual(["c1"]); expect((await env.call("lea", "GET", "/api/studio/courses/c1")).status).toBe(200); expect((await env.call("lea", "POST", "/api/studio/courses", { id: "c2", title: "C2" })).status).toBe(403); }); test("bad input: invalid id, missing title, bad JSON, bad paths, stale base hash", async () => { env = await setupApp(); expect((await env.call("ada", "POST", "/api/studio/courses", { id: "Bad Id", title: "x" })).status).toBe(400); expect((await env.call("ada", "POST", "/api/studio/courses", { id: "c1" })).status).toBe(400); await env.call("ada", "POST", "/api/studio/courses", { id: "c1", title: "C" }); expect((await env.call("ada", "PUT", "/api/studio/courses/c1/files", { message: "x", files: [{ path: "../x", content: "x" }] })).status).toBe(400); const r = await env.call("ada", "PUT", "/api/studio/courses/c1/files", { message: "x", files: [{ path: "lessons/welcome.md", content: "x", baseHash: "stale" }] }); expect(r.status).toBe(409); expect(r.json.conflicts[0].path).toBe("lessons/welcome.md"); expect((await env.call("ada", "GET", "/api/studio/courses/nope")).status).toBe(404); expect((await env.call("ada", "GET", "/api/studio/courses/c1/file?path=../../etc/passwd")).status).toBe(404); }); test("manifest validation errors come from @stc/course-format", async () => { env = await setupApp(); await env.call("ada", "POST", "/api/studio/courses", { id: "c1", title: "C" }); const st = (await env.call("ada", "GET", "/api/studio/courses/c1")).json; expect(st.manifestErrors).toEqual([]); const bad = { ...st.manifestRaw, accent: "orange", modules: [{ id: "m", title: "M", lessons: [{ id: "x", title: "X", type: "text" }] }] }; const v = (await env.call("ada", "POST", "/api/studio/courses/c1/validate", { manifest: bad })).json; expect(v.errors).toEqual(expect.arrayContaining([expect.stringContaining("accent"), expect.stringContaining("file is required")])); // saving an invalid draft is allowed (it is a draft) but publishing is refused with diagnostics await env.call("ada", "PUT", "/api/studio/courses/c1/files", { message: "wip", manifest: bad, manifestBaseHash: st.manifestHash, files: [] }); const st2 = (await env.call("ada", "GET", "/api/studio/courses/c1")).json; expect(st2.manifestErrors.length).toBeGreaterThan(0); const p = await env.call("ada", "POST", "/api/studio/courses/c1/publish"); expect(p.status).toBe(422); expect(p.json.reason).toBe("invalid"); }); test("upload an attachment and serve it back from the draft", async () => { env = await setupApp(); await env.call("ada", "POST", "/api/studio/courses", { id: "c1", title: "C" }); const fd = new FormData(); fd.set("file", new File([new Uint8Array([37, 80, 68, 70])], "Æble slides.pdf", { type: "application/pdf" })); fd.set("dir", "media"); const r = await env.call("ada", "POST", "/api/studio/courses/c1/upload", fd); expect(r.status).toBe(200); expect(r.json.path).toBe("media/Æble slides.pdf"); const raw = await env.app.fetch(new Request(`http://x/api/studio/courses/c1/raw/${encodeURIComponent("media/Æble slides.pdf")}`, { headers: { "x-user": "ada" } })); expect(raw.status).toBe(200); expect(raw.headers.get("content-type")).toBe("application/pdf"); expect((await env.call("lea", "GET", `/api/studio/courses/c1/raw/media/x`)).status).toBe(403); }); test("remote config and sync without a remote", async () => { env = await setupApp(); await env.call("ada", "POST", "/api/studio/courses", { id: "c1", title: "C" }); expect((await env.call("ada", "POST", "/api/studio/courses/c1/sync")).status).toBe(400); expect((await env.call("ada", "PUT", "/api/studio/courses/c1/remote", { url: "/tmp/nowhere.git" })).status).toBe(400); expect((await env.call("ada", "PUT", "/api/studio/courses/c1/remote", { url: "https://nowhere.invalid/x.git" })).status).toBe(200); expect((await env.call("ada", "GET", "/api/studio/courses/c1")).json.status.remote).toBe("https://nowhere.invalid/x.git"); expect((await env.call("ada", "POST", "/api/studio/courses/c1/sync")).status).toBe(502); });