Skip to content

LMS mutation test sweep

Problem

The LMS mutation surface is under-tested relative to its security rules. Audit findings:

  • Server actions: ~8 actions.ts files in apps/lms/src/app/**, but only 2 indirect test files (apps/lms/tests/unit/app/admin/courses/featureFlagActions.test.ts and a similar tenants one). No dedicated tests for enroll, quiz submit, sign-up, account, invite, or roster actions.
  • Route handlers: only 1 of 9 is tested — apps/lms/src/app/api/cms/revalidate/route.test.ts. Untested: api/admin/impersonate/start, api/admin/impersonate/stop, CMS asset proxies, NextAuth handler.

This is the largest test gap in the monorepo and shadows several .cursor/rules/120-security.mdc requirements (rate limits on enroll/quiz/sign-up are documented but not unit-asserted at the action boundary).

Proposal

  1. Server-action unit tests under apps/lms/tests/unit/app/** (mirroring source paths):
    • learn/enroll/actions.test.ts — happy path, unauthenticated denial, rate-limit reached, cross-tenant denial.
    • learn/quiz/actions.test.ts — submission persists; idempotency; rate-limit; auth required.
    • (auth)/sign-up/actions.test.ts — happy path, duplicate email, rate-limit, invalid input (Zod).
    • account/actions.test.ts — profile/password updates require auth and bump token_version.
    • admin/users/actions.test.ts — invite, role change, deactivation; all require tenant_admin or super_admin.
    • Use the existing apps/lms/tests/unit/db/setup.ts#createTestDb + tests/unit/db/fixtures.ts patterns; mock @/auth/guards and rate-limit driver where appropriate.
  2. Route handler tests under apps/lms/src/app/api/**:
    • admin/impersonate/start/route.test.ts — super_admin only; audit event written; one-time proof generated; non-super_admin → 403.
    • admin/impersonate/stop/route.test.ts — restores original session; audit event written.
    • Asset proxy routes — happy path + auth/tenant denial.
    • NextAuth handler smoke test (importable, exports GET/POST).
  3. Re-run after T-003 lands so assertions can target the standardized error shape.

Acceptance criteria

  • Every actions.ts under apps/lms/src/app/** has at least one happy-path and one auth-denied test.
  • Every route handler under apps/lms/src/app/api/** has at least one happy-path and one auth-denied test (or documented exception in this ticket).
  • Rate-limited actions assert the RATE_LIMITED code path via the in-memory driver.
  • npm run check passes; coverage report (T-004) shows measurable bump on src/app/**.

Out of scope

  • E2e additions (Playwright). Those follow once unit coverage is solid.
  • Changing rate-limit driver implementation (covered by T-014, deferred).
  • Refactoring action signatures (only assertion-level changes here).

Notes / decisions log

  • 2026-05-24: Ticket created from monorepo audit.
  • 2026-05-25: Implemented full mutation sweep (574 LMS unit tests). Harness: tests/unit/mutations/_harness.ts, assertions.ts; extended tests/unit/db/fixtures.ts. Auth actions covered under tests/unit/auth/actions/* (not only src/app/**/actions.ts). Route exceptions: public cms/site/assets/* (invalid id / upstream only); test/oauth/google (non-production); NextAuth handler export smoke via mocked @/auth.handlers. Course asset routes delegate auth to courseAssetAccessErrorResponse (unit-tested separately). Rate-limit tests use useMemoryRateLimitDriver() + exhaustRateLimitKey() without mocking @/lib/rate-limit.