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

93 lines
5.1 KiB
TypeScript

import { afterEach, expect, test } from "bun:test";
import { writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { extGit } from "./helpers";
import { REPO, setupApp } from "./app-helpers";
let env: Awaited<ReturnType<typeof setupApp>>;
afterEach(() => env?.stop());
const lesson = (path: string) => `/api/studio/courses/c1/file?path=${encodeURIComponent(path)}`;
test("acceptance: create a course, add and edit a lesson, publish, a learner sees it", async () => {
env = await setupApp();
const { call, ctx } = env;
expect((await call("ada", "POST", "/api/studio/courses", { id: "c1", title: "Course One" })).status).toBe(201);
expect(ctx.courses.get("c1")?.status).toBe("ok");
expect(ctx.courses.get("c1")?.root).toBe(join(env.dataDir, "worktrees/c1/published"));
// add a lesson (new file + manifest entry, one commit) and make the course public
const st = (await call("ada", "GET", "/api/studio/courses/c1")).json;
expect(st.readOnly).toBe(false);
const m = structuredClone(st.manifestRaw);
m.visibility = "public";
m.modules[0].lessons.push({ id: "second", title: "Second lesson", type: "text", file: "lessons/second.md" });
let r = await call("ada", "PUT", "/api/studio/courses/c1/files", {
message: "Add second lesson",
manifest: m, manifestBaseHash: st.manifestHash,
files: [{ path: "lessons/second.md", content: "# Second\n\nFirst draft.\n", baseHash: null }],
});
expect(r.status).toBe(200);
expect(r.json.commit).toBeTruthy();
// edit it
const f = (await call("ada", "GET", lesson("lessons/second.md"))).json;
expect(f.content).toContain("First draft.");
r = await call("ada", "PUT", "/api/studio/courses/c1/files", { message: "Edit second", files: [{ path: "lessons/second.md", content: "# Second\n\nEdited text.\n", baseHash: f.hash }] });
expect(r.status).toBe(200);
// drafts never reach learners
expect(ctx.courses.get("c1")?.lessonIndex.has("second")).toBe(false);
expect((await call(null, "GET", "/api/media/c1/lessons/second.md")).status).toBe(404);
// publish
r = await call("ada", "POST", "/api/studio/courses/c1/publish");
expect(r.status).toBe(200);
expect(r.json).toMatchObject({ ok: true, published: true });
expect(await ctx.courses.readLessonMarkdown("c1", "second")).toBe("# Second\n\nEdited text.\n");
const media = await call(null, "GET", "/api/media/c1/lessons/second.md");
expect(media.status).toBe(200);
expect(media.json).toContain("Edited text.");
});
test("acceptance: a conflicting external commit is shown, not overwritten", async () => {
env = await setupApp();
const { call, ctx, feature } = env;
await call("ada", "POST", "/api/studio/courses", { id: "c1", title: "Course One" });
const f = (await call("ada", "GET", lesson("lessons/welcome.md"))).json;
await call("ada", "PUT", "/api/studio/courses/c1/files", { message: "Studio edit", files: [{ path: "lessons/welcome.md", content: "# Studio\n", baseHash: f.hash }] });
const pub = join(env.dataDir, "worktrees/c1/published");
writeFileSync(join(pub, "lessons/welcome.md"), "# External\n");
await extGit(pub, "commit", "-qam", "Edited in VS Code");
expect(await feature.watcher.check()).toEqual(["c1"]);
expect(await ctx.courses.readLessonMarkdown("c1", "welcome")).toBe("# External\n");
const st = (await call("ada", "GET", "/api/studio/courses/c1")).json;
expect(st.status.behind).toBe(1);
const r = await call("ada", "POST", "/api/studio/courses/c1/publish");
expect(r.status).toBe(409);
expect(r.json).toEqual({ ok: false, reason: "conflict", conflicts: ["lessons/welcome.md"] });
expect(await ctx.courses.readLessonMarkdown("c1", "welcome")).toBe("# External\n");
const u = await call("ada", "POST", "/api/studio/courses/c1/update-draft", { from: "published" });
expect(u.status).toBe(409);
const c = (await call("ada", "GET", "/api/studio/courses/c1/conflicts")).json;
expect(c.files[0]).toMatchObject({ path: "lessons/welcome.md", ours: "# Studio\n", theirs: "# External\n" });
expect((await call("ada", "POST", "/api/studio/courses/c1/resolve", { files: [{ path: "lessons/welcome.md", content: "# Studio + External\n" }] })).json.remaining).toEqual([]);
expect((await call("ada", "POST", "/api/studio/courses/c1/publish")).status).toBe(200);
expect(await ctx.courses.readLessonMarkdown("c1", "welcome")).toBe("# Studio + External\n");
});
test("overlay and other outside courses are read-only", async () => {
env = await setupApp([resolve(REPO, "fixtures/courses")]);
const list = (await env.call("ada", "GET", "/api/studio/courses")).json;
expect(list.courses).toContainEqual(expect.objectContaining({ id: "sample", managed: false, message: "This course is managed outside STCourses" }));
const one = await env.call("ada", "GET", "/api/studio/courses/sample");
expect(one.json).toMatchObject({ readOnly: true, message: "This course is managed outside STCourses" });
const w = await env.call("ada", "PUT", "/api/studio/courses/sample/files", { message: "x", files: [{ path: "a.md", content: "x" }] });
expect(w.status).toBe(403);
expect((await env.call("ada", "POST", "/api/studio/courses", { id: "sample", title: "Dup" })).status).toBe(409);
});