Files
STCourses/packages/feature-studio/test/feature.test.ts
T

81 lines
5.2 KiB
TypeScript

import { afterEach, expect, test } from "bun:test";
import { setupApp } from "./app-helpers";
let env: Awaited<ReturnType<typeof setupApp>>;
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 = [["sqlite", "sqlite://:memory:"], ...(process.env.STC_TEST_PG_URL ? [["postgres", process.env.STC_TEST_PG_URL]] : [])] as const;
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(200);
expect((await env.call("ada", "GET", "/api/studio/courses/c1")).json.status.remote).toBe("/tmp/nowhere.git");
expect((await env.call("ada", "POST", "/api/studio/courses/c1/sync")).status).toBe(502);
});