feat(learner): owner admin API for roles, groups, course access and library curation

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-30 03:20:49 +01:00
co-authored by Claude Opus 5.5
parent 668ae379bf
commit a8a8e32bcb
3 changed files with 195 additions and 0 deletions
@@ -0,0 +1,43 @@
import { expect } from "bun:test";
import { forEachDb } from "./helpers";
import { makeApp } from "./app";
forEachDb("admin API is owner-only and manages roles, groups, access and library", async (db) => {
const t = await makeApp(db);
const owner = await t.signUp("[email protected]");
const learner = await t.signUp("[email protected]");
expect((await t.call("GET", "/api/admin/users")).status).toBe(401);
expect((await t.call("GET", "/api/admin/users", { cookie: learner })).status).toBe(403);
const users = await (await t.call("GET", "/api/admin/users", { cookie: owner })).json();
expect(users.map((u: { email: string }) => u.email)).toEqual(["[email protected]", "[email protected]"]);
const [o, l] = users;
// the last owner keeps the role
expect((await t.call("PUT", `/api/admin/users/${o.id}/roles`, { cookie: owner, body: { roles: ["learner"] } })).status).toBe(409);
expect((await t.call("PUT", `/api/admin/users/${l.id}/roles`, { cookie: owner, body: { roles: ["bogus"] } })).status).toBe(400);
const r = await t.call("PUT", `/api/admin/users/${l.id}/roles`, { cookie: owner, body: { roles: ["author", "learner"] } });
expect((await r.json()).roles).toEqual(["author", "learner"]);
expect((await (await t.call("GET", "/api/me", { cookie: learner })).json()).user.roles).toEqual(["author", "learner"]);
const g = await (await t.call("POST", "/api/admin/groups", { cookie: owner, body: { name: "DevOps Team" } })).json();
expect(g.id).toBe("devops-team");
await t.call("PUT", `/api/admin/groups/${g.id}/members`, { cookie: owner, body: { userIds: [l.id] } });
const groups = await (await t.call("GET", "/api/admin/groups", { cookie: owner })).json();
expect(groups).toEqual([{ id: "devops-team", name: "DevOps Team", members: [l.id] }]);
expect((await (await t.call("GET", "/api/me", { cookie: learner })).json()).user.groups).toEqual(["devops-team"]);
await t.call("PUT", "/api/admin/courses/sample/access", { cookie: owner, body: { acl: [{ principal_type: "group", principal_id: g.id }], authors: [l.id] } });
const access = await (await t.call("GET", "/api/admin/courses/sample/access", { cookie: owner })).json();
expect(access).toEqual({ visibility: "public", acl: [{ principal_type: "group", principal_id: "devops-team" }], authors: [l.id] });
expect((await t.call("GET", "/api/admin/courses/nope/access", { cookie: owner })).status).toBe(404);
const libr = await (await t.call("PUT", "/api/admin/library", { cookie: owner, body: { featured: "sample", collections: [{ title: "Getting started", courseIds: ["sample"] }] } })).json();
expect(libr.featured).toBe("sample");
expect(libr.collections).toEqual([{ id: "getting-started", title: "Getting started", sort: 0, courseIds: ["sample"] }]);
const cleared = await (await t.call("PUT", "/api/admin/library", { cookie: owner, body: { collections: [] } })).json();
expect(cleared.collections).toEqual([]);
await t.call("DELETE", `/api/admin/groups/${g.id}`, { cookie: owner });
expect(await (await t.call("GET", "/api/admin/groups", { cookie: owner })).json()).toEqual([]);
}, 30_000);