Files
STCourses/packages/feature-learner/test/catalog.test.ts
T

62 lines
3.6 KiB
TypeScript

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("[email protected]");
const learner = await t.signUp("[email protected]");
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 = '[email protected]'`;
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("[email protected]");
const learner = await t.signUp("[email protected]");
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);