Skip to content

test: permission system test coverage #153

Description

@droarty

Context

Steps 142–148 added a full permission system: group-role-based access (read/write/admin) computed from group membership + ancestor chains, an optional per-user ACL, two permission manager modes (owner vs group_admin), enforcement inside WorkflowEngine (requiredAccess) and DatabasePersistor (write guard), and 10-minute caching in EventProcessorWorker. No tests exist yet for any of this logic. This issue covers the files needed to give the system full coverage.


Files to Create

1. apps/api/src/app/services/permission.service.spec.ts — Unit tests

Uses MongoMemoryServer + Mongoose models directly. No HTTP layer.

getEffectiveGroupIds

  • User with no memberships → []
  • User in one group with no ancestors → [groupId]
  • User in group with a parent → includes both IDs
  • User in group with grandparent chain → includes all three IDs
  • User in two disjoint groups → includes all IDs from both trees

computeAccessLevel — owner mode (permissionManagerMode: 'owner')

  • doc.userId === userId → short-circuits to 'admin'
  • Non-owner, empty ACL, no group perms → 'none'
  • Non-owner with userPermissions entry of 'read''read'
  • Non-owner with userPermissions entry of 'write''write'
  • Non-owner with userPermissions entry of 'admin''admin'
  • User in a group that has 'read' in doc.permissions, no user ACL → 'read'
  • User ACL 'read', group 'write''write' (max wins)
  • User ACL 'write', group 'read''write' (max wins)
  • User in child group; parent group in doc.permissions with 'write''write' (ancestor inheritance)

computeAccessLevel — group_admin mode (permissionManagerMode: 'group_admin')

  • doc.userId === userId but mode is group_admin → NOT auto-admin; falls through to ACL check
  • Owner with no ACL entry → 'none'
  • Owner with 'write' in userPermissions'write'
  • Unrelated user in doc's group with 'admin''admin'

canManagePermissions — owner mode

  • doc.userId === userIdtrue
  • Different user, no doc.groupIdfalse
  • Different user, in doc.groupId group with role 'member'false
  • Different user, in doc.groupId group with role 'admin'true
  • Different user, in doc.groupId group with role 'owner'true

canManagePermissions — group_admin mode

  • Caller in doc.groupId with 'admin'true
  • Caller in doc.groupId with 'owner'true
  • Caller in doc.groupId with 'member'false
  • doc.userId === userId but caller not in group → false (NOT auto-manage)
  • doc.groupId is null → false

2. apps/api-e2e/src/permissions.spec.ts — E2E HTTP tests

Follows the same MongoMemoryServer + supertest pattern as users.spec.ts. Set up multiple users (owner, otherUser, groupAdmin) and real Group/Membership documents in beforeEach.

GET /api/documents/:id

  • Owner → 200
  • User with 'read' user ACL → 200
  • User with group 'read' access → 200
  • User with no permissions → 403
  • Unauthenticated → 401

GET /api/documents — filtering

  • Returns owned documents
  • Returns documents where user has a user ACL entry
  • Returns documents where user has group access
  • Does NOT return documents where user has no access

POST /api/documents — group admin flow

  • Group admin + groupId + targetUserId → 201; response has permissionManagerMode: 'group_admin', group gets 'admin', targetUser gets 'write' in userPermissions
  • Ancestor group gets 'read' in permissions
  • Non-member with targetUserId → 403
  • Group member (role 'member') with targetUserId → 403
  • Missing groupId with targetUserId → 400

PATCH /api/documents/:id/user-permissions — grant permission

  • Owner grants 'read' → 200, userPermissions updated
  • Owner grants 'write' → 200
  • Owner grants 'admin' → 200
  • Granting same userId twice upserts (no duplicate) → 200
  • Non-owner without manage rights → 403
  • Owner cannot grant 'admin' when own level is only 'write' → 403 (Cannot grant access level higher than your own)
  • Invalid access value → 400

DELETE /api/documents/:id/user-permissions/:userId — revoke permission

  • Owner removes existing entry → 204
  • Non-owner without manage rights → 403
  • Removing non-existent userId → 404

3. apps/api/src/app/websocket/access-level-cache.ts + access-level-cache.spec.ts

Extract the ACCESS_LEVEL_CACHE map and TTL logic from EventProcessorWorker.ts into a createAccessLevelCache(ttlMs) factory. No behavioral change — purely structural.

Cache tests use jest.useFakeTimers():

  • First call invokes compute once and returns the result
  • Second call within TTL returns cached level, compute NOT called again
  • Call after TTL expires re-invokes compute
  • Different userId or channel uses separate cache entries

Files to Modify

4. apps/api/src/app/websocket/WorkflowEngine.spec.ts — requiredAccess guard

Add handlers with requiredAccess: 'write' and requiredAccess: 'admin' to the test config. Extend makeContext to accept optional permissionLevel.

  • permissionLevel: 'read', requires 'write' → NOT executed
  • permissionLevel: 'write', requires 'write' → executed
  • permissionLevel: 'admin', requires 'write' → executed (admin ≥ write)
  • permissionLevel: 'write', requires 'admin' → NOT executed
  • permissionLevel absent, requires 'write' → NOT executed
  • No requiredAccess, any level → executed

5. apps/api/src/app/websocket/DatabasePersistor.spec.ts — write guard

  • permissionLevel: 'none' → DB write skipped
  • permissionLevel: 'read' → DB write skipped
  • permissionLevel: 'write' → DB write proceeds
  • permissionLevel: 'admin' → DB write proceeds

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions