import { expect } from "bun:test"; import { forEachDb } from "./helpers"; import { FIXTURES, makeApp } from "./app"; import { extraCourses } from "./courses"; forEachDb("progress: mark done, positions never undo done, gating blocks writes", async (db) => { const t = await makeApp(db, [FIXTURES, await extraCourses()]); await t.signUp("owner@example.com"); const me = await t.signUp("l@example.com"); const put = (course: string, lesson: string, body: unknown, cookie = me) => t.call("PUT", `/api/courses/${course}/progress/${lesson}`, { cookie, body }); expect((await put("sample", "m1-intro", { state: "done" }, "")).status).toBe(401); expect((await put("sample", "m1-intro", { state: "finished" })).status).toBe(400); expect((await (await put("sample", "m1-video", { state: "in-progress", positionSeconds: 1.5 })).json()).positionSeconds).toBe(1.5); await put("sample", "m1-video", { state: "done" }); const after = await (await put("sample", "m1-video", { state: "in-progress", positionSeconds: 0.5 })).json(); expect(after).toMatchObject({ state: "done", positionSeconds: 0.5 }); expect((await put("sample", "nope", { state: "done" })).status).toBe(404); expect((await put("gated", "g2a", { state: "done" })).status).toBe(403); await put("gated", "g1a", { state: "done" }); await put("gated", "g1b", { state: "done" }); expect((await put("gated", "g2a", { state: "done" })).status).toBe(200); const all = await (await t.call("GET", "/api/courses/gated/progress", { cookie: me })).json(); expect(Object.keys(all).sort()).toEqual(["g1a", "g1b", "g2a"]); }, 30_000); forEachDb("notes persist per lesson with slide and video anchors", async (db) => { const t = await makeApp(db); const me = await t.signUp("l@example.com"); const other = await t.signUp("o@example.com"); const post = (b: unknown, cookie = me) => t.call("POST", "/api/courses/sample/notes", { cookie, body: b }); expect((await post({ lessonId: "m1-video", anchorKind: "slide", anchorValue: 2, body: "x" })).status).toBe(400); // no deck id expect((await post({ lessonId: "m1-video", anchorKind: "video", body: "x" })).status).toBe(400); const s10 = await (await post({ lessonId: "m1-video", anchorKind: "slide", anchorValue: 10, deckId: "d1", body: "slide ten" })).json(); await post({ lessonId: "m1-video", anchorKind: "slide", anchorValue: 2, deckId: "d1", body: "slide two" }); await post({ lessonId: "m1-video", anchorKind: "video", anchorValue: 1.25, body: "at 1.25s" }); await post({ lessonId: "m1-intro", body: "general" }); const list = await (await t.call("GET", "/api/courses/sample/notes?lesson=m1-video", { cookie: me })).json(); expect(list.map((n: { body: string }) => n.body)).toEqual(["slide two", "slide ten", "at 1.25s"]); expect(list[0]).toMatchObject({ anchorKind: "slide", anchorValue: "2", deckId: "d1" }); expect(list[0].userId).toBeUndefined(); expect((await (await t.call("GET", "/api/courses/sample/notes", { cookie: me })).json()).length).toBe(4); expect(await (await t.call("GET", "/api/courses/sample/notes", { cookie: other })).json()).toEqual([]); expect((await t.call("PATCH", `/api/notes/${s10.id}`, { cookie: other, body: { body: "hijack" } })).status).toBe(404); expect((await (await t.call("PATCH", `/api/notes/${s10.id}`, { cookie: me, body: { body: "edited" } })).json()).body).toBe("edited"); expect((await t.call("DELETE", `/api/notes/${s10.id}`, { cookie: other })).status).toBe(404); expect((await t.call("DELETE", `/api/notes/${s10.id}`, { cookie: me })).status).toBe(200); expect((await (await t.call("GET", "/api/courses/sample/notes?lesson=m1-video", { cookie: me })).json()).length).toBe(2); }, 30_000); forEachDb("exercise attempts save code and a pass marks the lesson done", async (db) => { const t = await makeApp(db); const me = await t.signUp("l@example.com"); const url = "/api/courses/sample/lessons/m2-ex-add/attempt"; expect(await (await t.call("GET", `${url}?track=js`, { cookie: me })).json()).toBeNull(); const r1 = await (await t.call("PUT", url, { cookie: me, body: { track: "js", code: "draft", passed: false, result: [{ name: "a", passed: false }] } })).json(); expect(r1).toEqual({ ok: true, passed: false, state: "in-progress" }); const r2 = await (await t.call("PUT", url, { cookie: me, body: { track: "js", code: "export const add=(a,b)=>a+b", passed: true, result: [] } })).json(); expect(r2.state).toBe("done"); const got = await (await t.call("GET", `${url}?track=js`, { cookie: me })).json(); expect(got).toMatchObject({ code: "export const add=(a,b)=>a+b", passed: true }); expect((await t.call("PUT", "/api/courses/sample/lessons/m1-intro/attempt", { cookie: me, body: { code: "x" } })).status).toBe(400); }, 30_000);