Files
pocketpaw/tests/cloud/test_models.py
Rohit Kushwaha d1b5870278 feat(ee-cloud): workspace RBAC consolidation + invite flow + visibility defaults
Consolidates the two parallel RBAC frameworks into a single route-level
authorization system and adds the missing pieces needed for a managed-onboarding
pilot: an endpoint to list pending invites, sane visibility defaults for
pockets/groups so invited teammates actually see shared work, and audit
logging on every denial.

RBAC consolidation
- New ACTIONS table (src/pocketpaw/ee/guards/actions.py) is the single source
  of truth for every guarded action → (minimum role, stable deny code).
  Covers workspace, group, message, pocket, agent, session, KB, invite,
  and billing — 32 rows total.
- New audit helpers (src/pocketpaw/ee/guards/audit.py): log_denial() +
  log_privileged_action() backed by the existing append-only audit log.
- New FastAPI deps: require_action(), require_action_any_workspace(),
  require_group_action(), require_membership(), require_agent_owner_or_admin(),
  require_pocket_edit(), require_pocket_owner().
- Route-level enforcement across workspace/chat/pockets/agents/sessions/kb
  routers; permission checks removed from service bodies where now redundant.
- Group role model extended to 3 tiers (owner > admin > member), with per-member
  override via Group.member_roles. Admin tier enforced via resolve_group_role()
  and the updated _require_group_admin helper.
- Legacy ee/cloud/shared/permissions.py deleted; legacy require_role shim
  removed from ee/cloud/shared/deps.py.
- Matrix test (tests/cloud/test_rbac_matrix.py) iterates every ACTIONS entry
  across every peer role, verifying both allow and deny paths and the exact
  Forbidden.code. Meta-test enforces coverage.
- Redundant tests/cloud/test_permissions.py removed.

Invite list endpoint
- GET /workspaces/{workspace_id}/invites returns all pending invites,
  admin-only via require_action("invite.create"). Wires the admin UX for
  listing + copy-link + revoke.

Visibility defaults
- Pocket.visibility default flipped "private" → "workspace" so new pockets
  are visible to all workspace members out of the box. Owners can tighten
  per-pocket via the Share tab.
- Group.type default flipped "public" → "private" so new groups are
  invite-only. Public channels remain explicit via type="channel" or "public".
- CreateGroupRequest and UpdateGroupRequest updated; GroupService.update_group
  now supports type changes. list_groups() treats "channel" same as "public"
  for workspace-wide readability.
- Existing tests updated to match new defaults.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 10:21:16 +05:30

107 lines
3.4 KiB
Python

"""Tests for cloud model changes — pure Pydantic validation, no DB needed.
Uses model_construct() to bypass Beanie's __init__ (which requires a live
MongoDB collection). We then verify default values and field acceptance via
Pydantic's model_validate (construct=True).
"""
from __future__ import annotations
from ee.cloud.models.group import Group
from ee.cloud.models.invite import Invite
from ee.cloud.models.message import Message
from ee.cloud.models.notification import Notification
from ee.cloud.models.pocket import Pocket
from ee.cloud.models.session import Session
from ee.cloud.models.workspace import Workspace
# ---------------------------------------------------------------------------
# Group
# ---------------------------------------------------------------------------
def test_group_supports_dm_type():
g = Group.model_construct(
workspace="w1", name="DM", type="dm", owner="u1", members=["u1", "u2"]
)
assert g.type == "dm"
def test_group_has_last_message_at():
g = Group.model_construct(workspace="w1", name="test", owner="u1")
assert g.last_message_at is None
def test_group_has_message_count():
g = Group.model_construct(workspace="w1", name="test", owner="u1")
assert g.message_count == 0
# ---------------------------------------------------------------------------
# Message
# ---------------------------------------------------------------------------
def test_message_has_edited_at():
m = Message.model_construct(group="g1", sender="u1", content="hello")
assert m.edited_at is None
# ---------------------------------------------------------------------------
# Pocket
# ---------------------------------------------------------------------------
def test_pocket_sharing_fields():
p = Pocket.model_construct(workspace="w1", name="test", owner="u1")
assert p.share_link_token is None
assert p.share_link_access == "view"
assert p.visibility == "workspace"
assert p.shared_with == []
def test_pocket_visibility_values():
for v in ("private", "workspace", "public"):
p = Pocket.model_construct(workspace="w1", name="test", owner="u1", visibility=v)
assert p.visibility == v
# ---------------------------------------------------------------------------
# Invite
# ---------------------------------------------------------------------------
def test_invite_has_revoked():
i = Invite.model_construct(workspace="w1", email="a@b.com", invited_by="u1", token="tok1")
assert i.revoked is False
# ---------------------------------------------------------------------------
# Workspace
# ---------------------------------------------------------------------------
def test_workspace_has_deleted_at():
w = Workspace.model_construct(name="test", slug="test", owner="u1")
assert w.deleted_at is None
# ---------------------------------------------------------------------------
# Session
# ---------------------------------------------------------------------------
def test_session_has_deleted_at():
s = Session.model_construct(sessionId="s1", workspace="w1", owner="u1")
assert s.deleted_at is None
# ---------------------------------------------------------------------------
# Notification
# ---------------------------------------------------------------------------
def test_notification_has_expires_at():
n = Notification.model_construct(workspace="w1", recipient="u1", type="mention", title="test")
assert n.expires_at is None