Implement team-based organization for projects with role-based access control: Backend: - Add teams and team_memberships database tables (migrations 009, 009b) - Add Team and TeamMembership ORM models with relationships - Implement TeamAuthorizationService for team-level access control - Add team CRUD, membership, and projects API endpoints - Update project creation to support team assignment Frontend: - Add TeamContext for managing team state with localStorage persistence - Add TeamSelector component for switching between teams - Add TeamsPage, TeamDashboardPage, TeamSettingsPage, TeamMembersPage - Add team API client functions - Update navigation with Teams link Security: - Team role hierarchy: owner > admin > member - Membership checked before system admin fallback - Self-modification prevention for role changes - Email visibility restricted to team admins/owners - Slug validation rejects consecutive hyphens Tests: - Unit tests for TeamAuthorizationService - Integration tests for all team API endpoints
214 lines
8.1 KiB
Python
214 lines
8.1 KiB
Python
"""
|
|
Unit tests for TeamAuthorizationService.
|
|
"""
|
|
|
|
import pytest
|
|
from unittest.mock import MagicMock, patch
|
|
import uuid
|
|
|
|
|
|
class TestTeamRoleHierarchy:
|
|
"""Tests for team role hierarchy functions."""
|
|
|
|
def test_get_team_role_rank(self):
|
|
"""Test role ranking."""
|
|
from app.auth import get_team_role_rank
|
|
|
|
assert get_team_role_rank("member") == 0
|
|
assert get_team_role_rank("admin") == 1
|
|
assert get_team_role_rank("owner") == 2
|
|
assert get_team_role_rank("invalid") == -1
|
|
|
|
def test_has_sufficient_team_role(self):
|
|
"""Test role sufficiency checks."""
|
|
from app.auth import has_sufficient_team_role
|
|
|
|
# Same role should be sufficient
|
|
assert has_sufficient_team_role("member", "member") is True
|
|
assert has_sufficient_team_role("admin", "admin") is True
|
|
assert has_sufficient_team_role("owner", "owner") is True
|
|
|
|
# Higher role should be sufficient for lower requirements
|
|
assert has_sufficient_team_role("admin", "member") is True
|
|
assert has_sufficient_team_role("owner", "member") is True
|
|
assert has_sufficient_team_role("owner", "admin") is True
|
|
|
|
# Lower role should NOT be sufficient for higher requirements
|
|
assert has_sufficient_team_role("member", "admin") is False
|
|
assert has_sufficient_team_role("member", "owner") is False
|
|
assert has_sufficient_team_role("admin", "owner") is False
|
|
|
|
|
|
class TestTeamAuthorizationService:
|
|
"""Tests for TeamAuthorizationService class."""
|
|
|
|
@pytest.fixture
|
|
def mock_db(self):
|
|
"""Create a mock database session."""
|
|
return MagicMock()
|
|
|
|
@pytest.fixture
|
|
def mock_user(self):
|
|
"""Create a mock user."""
|
|
user = MagicMock()
|
|
user.id = uuid.uuid4()
|
|
user.username = "testuser"
|
|
user.is_admin = False
|
|
return user
|
|
|
|
@pytest.fixture
|
|
def mock_admin_user(self):
|
|
"""Create a mock admin user."""
|
|
user = MagicMock()
|
|
user.id = uuid.uuid4()
|
|
user.username = "adminuser"
|
|
user.is_admin = True
|
|
return user
|
|
|
|
def test_get_user_team_role_no_user(self, mock_db):
|
|
"""Test that None is returned for anonymous users."""
|
|
from app.auth import TeamAuthorizationService
|
|
|
|
service = TeamAuthorizationService(mock_db)
|
|
result = service.get_user_team_role("team-id", None)
|
|
assert result is None
|
|
|
|
def test_get_user_team_role_admin_user(self, mock_db, mock_admin_user):
|
|
"""Test that system admins who are not members get admin role."""
|
|
from app.auth import TeamAuthorizationService
|
|
|
|
# Mock no membership found
|
|
mock_db.query.return_value.filter.return_value.first.return_value = None
|
|
|
|
service = TeamAuthorizationService(mock_db)
|
|
result = service.get_user_team_role("team-id", mock_admin_user)
|
|
assert result == "admin"
|
|
|
|
def test_get_user_team_role_member(self, mock_db, mock_user):
|
|
"""Test getting role for a team member."""
|
|
from app.auth import TeamAuthorizationService
|
|
|
|
# Mock the membership query
|
|
mock_membership = MagicMock()
|
|
mock_membership.role = "member"
|
|
mock_db.query.return_value.filter.return_value.first.return_value = mock_membership
|
|
|
|
service = TeamAuthorizationService(mock_db)
|
|
result = service.get_user_team_role("team-id", mock_user)
|
|
assert result == "member"
|
|
|
|
def test_get_user_team_role_not_member(self, mock_db, mock_user):
|
|
"""Test getting role for a non-member."""
|
|
from app.auth import TeamAuthorizationService
|
|
|
|
# Mock no membership found
|
|
mock_db.query.return_value.filter.return_value.first.return_value = None
|
|
|
|
service = TeamAuthorizationService(mock_db)
|
|
result = service.get_user_team_role("team-id", mock_user)
|
|
assert result is None
|
|
|
|
def test_check_team_access_member(self, mock_db, mock_user):
|
|
"""Test access check for member requiring member role."""
|
|
from app.auth import TeamAuthorizationService
|
|
|
|
# Mock the membership query
|
|
mock_membership = MagicMock()
|
|
mock_membership.role = "member"
|
|
mock_db.query.return_value.filter.return_value.first.return_value = mock_membership
|
|
|
|
service = TeamAuthorizationService(mock_db)
|
|
|
|
# Member should have member access
|
|
assert service.check_team_access("team-id", mock_user, "member") is True
|
|
# Member should not have admin access
|
|
assert service.check_team_access("team-id", mock_user, "admin") is False
|
|
# Member should not have owner access
|
|
assert service.check_team_access("team-id", mock_user, "owner") is False
|
|
|
|
def test_check_team_access_admin(self, mock_db, mock_user):
|
|
"""Test access check for admin role."""
|
|
from app.auth import TeamAuthorizationService
|
|
|
|
# Mock admin membership
|
|
mock_membership = MagicMock()
|
|
mock_membership.role = "admin"
|
|
mock_db.query.return_value.filter.return_value.first.return_value = mock_membership
|
|
|
|
service = TeamAuthorizationService(mock_db)
|
|
|
|
assert service.check_team_access("team-id", mock_user, "member") is True
|
|
assert service.check_team_access("team-id", mock_user, "admin") is True
|
|
assert service.check_team_access("team-id", mock_user, "owner") is False
|
|
|
|
def test_check_team_access_owner(self, mock_db, mock_user):
|
|
"""Test access check for owner role."""
|
|
from app.auth import TeamAuthorizationService
|
|
|
|
# Mock owner membership
|
|
mock_membership = MagicMock()
|
|
mock_membership.role = "owner"
|
|
mock_db.query.return_value.filter.return_value.first.return_value = mock_membership
|
|
|
|
service = TeamAuthorizationService(mock_db)
|
|
|
|
assert service.check_team_access("team-id", mock_user, "member") is True
|
|
assert service.check_team_access("team-id", mock_user, "admin") is True
|
|
assert service.check_team_access("team-id", mock_user, "owner") is True
|
|
|
|
def test_can_create_project(self, mock_db, mock_user):
|
|
"""Test can_create_project requires admin role."""
|
|
from app.auth import TeamAuthorizationService
|
|
|
|
service = TeamAuthorizationService(mock_db)
|
|
|
|
# Member cannot create projects
|
|
mock_membership = MagicMock()
|
|
mock_membership.role = "member"
|
|
mock_db.query.return_value.filter.return_value.first.return_value = mock_membership
|
|
assert service.can_create_project("team-id", mock_user) is False
|
|
|
|
# Admin can create projects
|
|
mock_membership.role = "admin"
|
|
assert service.can_create_project("team-id", mock_user) is True
|
|
|
|
# Owner can create projects
|
|
mock_membership.role = "owner"
|
|
assert service.can_create_project("team-id", mock_user) is True
|
|
|
|
def test_can_manage_members(self, mock_db, mock_user):
|
|
"""Test can_manage_members requires admin role."""
|
|
from app.auth import TeamAuthorizationService
|
|
|
|
service = TeamAuthorizationService(mock_db)
|
|
|
|
# Member cannot manage members
|
|
mock_membership = MagicMock()
|
|
mock_membership.role = "member"
|
|
mock_db.query.return_value.filter.return_value.first.return_value = mock_membership
|
|
assert service.can_manage_members("team-id", mock_user) is False
|
|
|
|
# Admin can manage members
|
|
mock_membership.role = "admin"
|
|
assert service.can_manage_members("team-id", mock_user) is True
|
|
|
|
def test_can_delete_team(self, mock_db, mock_user):
|
|
"""Test can_delete_team requires owner role."""
|
|
from app.auth import TeamAuthorizationService
|
|
|
|
service = TeamAuthorizationService(mock_db)
|
|
|
|
# Member cannot delete team
|
|
mock_membership = MagicMock()
|
|
mock_membership.role = "member"
|
|
mock_db.query.return_value.filter.return_value.first.return_value = mock_membership
|
|
assert service.can_delete_team("team-id", mock_user) is False
|
|
|
|
# Admin cannot delete team
|
|
mock_membership.role = "admin"
|
|
assert service.can_delete_team("team-id", mock_user) is False
|
|
|
|
# Only owner can delete team
|
|
mock_membership.role = "owner"
|
|
assert service.can_delete_team("team-id", mock_user) is True
|