Add multi-tenancy with Teams feature
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
This commit is contained in:
213
backend/tests/unit/test_team_auth.py
Normal file
213
backend/tests/unit/test_team_auth.py
Normal file
@@ -0,0 +1,213 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user