93 lines
5.1 KiB
TypeScript
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);
|
|
});
|