import { expect } from "bun:test"; import { forEachDb } from "./helpers"; import { FIXTURES, makeApp } from "./app"; import { extraCourses } from "./courses"; forEachDb("library shows only visible courses, with formats and lengths", async (db) => { const t = await makeApp(db, [FIXTURES, await extraCourses()]); const anon = await (await t.call("GET", "/api/library")).json(); expect(anon.signedIn).toBe(false); expect(anon.courses.map((c: { id: string }) => c.id)).toEqual(["sample"]); const sample = anon.courses[0]; expect(sample.formats).toEqual(["text", "video", "slides", "exercises"]); expect(sample.lessonCount).toBe(5); expect(sample.minutes).toBeGreaterThan(14); expect(anon.featured.id).toBe("sample"); const owner = await t.signUp("owner@example.com"); const learner = await t.signUp("l@example.com"); const asOwner = await (await t.call("GET", "/api/library", { cookie: owner })).json(); expect(asOwner.courses.map((c: { id: string }) => c.id).sort()).toEqual(["gated", "sample", "secret"]); const asLearner = await (await t.call("GET", "/api/library", { cookie: learner })).json(); expect(asLearner.courses.map((c: { id: string }) => c.id).sort()).toEqual(["gated", "sample"]); // manifest collections become rows; restricted courses do not leak into them expect(asLearner.collections).toEqual([ { id: "devops", title: "DevOps", courseIds: ["gated"] }, { id: "functional-programming", title: "Functional programming", courseIds: ["gated"] }, ]); expect((await t.call("GET", "/api/courses/secret", { cookie: learner })).status).toBe(404); expect((await t.call("GET", "/api/courses/secret/lessons/s1", { cookie: learner })).status).toBe(404); // continue row: after touching a lesson await db.sql`INSERT INTO progress (user_id, course_id, lesson_id, state, position_seconds, updated_at) SELECT id, 'gated', 'g1a', 'done', NULL, '2026-10-30T00:00:00Z' FROM auth_user WHERE email = 'l@example.com'`; const again = await (await t.call("GET", "/api/library", { cookie: learner })).json(); expect(again.continue).toEqual([{ courseId: "gated", lessonId: "g1b", lessonTitle: "Then this", moduleTitle: "First", moduleIndex: 0, moduleDone: 1, moduleTotal: 2, progress: 1 / 3 }]); expect(again.featured.id).toBe("gated"); }, 30_000); forEachDb("search matches titles, pitch and lesson titles", async (db) => { const t = await makeApp(db, [FIXTURES, await extraCourses()]); const hits = await (await t.call("GET", "/api/library/search?q=clip")).json(); expect(hits).toEqual([{ courseId: "sample", courseTitle: "Sample course", course: false, lessons: [{ id: "m1-video", title: "Watch the clip", moduleTitle: "First steps" }] }]); const tiny = await (await t.call("GET", "/api/library/search?q=TINY")).json(); expect(tiny[0].course).toBe(true); expect(await (await t.call("GET", "/api/library/search?q=")).json()).toEqual([]); }, 30_000); forEachDb("course outline carries gating locks and progress", async (db) => { const t = await makeApp(db, [FIXTURES, await extraCourses()]); await t.signUp("owner@example.com"); const learner = await t.signUp("l@example.com"); const c = await (await t.call("GET", "/api/courses/gated", { cookie: learner })).json(); expect(c.modules.map((m: { locked: boolean }) => m.locked)).toEqual([false, true]); expect(c.modules[1].lessons[0].hasDeck).toBe(true); expect(c.canEdit).toBe(false); expect(c.diagnostics).toEqual([]); expect((await t.call("GET", "/api/courses/gated")).status).toBe(404); // signed-in only const locked = await (await t.call("GET", "/api/courses/gated/lessons/g2a", { cookie: learner })).json(); expect(locked.locked).toBe(true); expect(locked.markdown).toBeNull(); }, 30_000);