Add multi-tenancy with Teams feature
This commit is contained in:
316
backend/tests/integration/test_teams_api.py
Normal file
316
backend/tests/integration/test_teams_api.py
Normal file
@@ -0,0 +1,316 @@
|
||||
"""
|
||||
Integration tests for Teams API endpoints.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestTeamsCRUD:
|
||||
"""Tests for team creation, listing, updating, and deletion."""
|
||||
|
||||
def test_create_team(self, integration_client, unique_test_id):
|
||||
"""Test creating a new team."""
|
||||
team_name = f"Test Team {unique_test_id}"
|
||||
team_slug = f"test-team-{unique_test_id}"
|
||||
|
||||
response = integration_client.post(
|
||||
"/api/v1/teams",
|
||||
json={
|
||||
"name": team_name,
|
||||
"slug": team_slug,
|
||||
"description": "A test team",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 201, f"Failed to create team: {response.text}"
|
||||
|
||||
data = response.json()
|
||||
assert data["name"] == team_name
|
||||
assert data["slug"] == team_slug
|
||||
assert data["description"] == "A test team"
|
||||
assert data["user_role"] == "owner"
|
||||
assert data["member_count"] == 1
|
||||
assert data["project_count"] == 0
|
||||
|
||||
# Cleanup
|
||||
integration_client.delete(f"/api/v1/teams/{team_slug}")
|
||||
|
||||
def test_create_team_duplicate_slug(self, integration_client, unique_test_id):
|
||||
"""Test that duplicate team slugs are rejected."""
|
||||
team_slug = f"dup-team-{unique_test_id}"
|
||||
|
||||
# Create first team
|
||||
response = integration_client.post(
|
||||
"/api/v1/teams",
|
||||
json={"name": "First Team", "slug": team_slug},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
# Try to create second team with same slug
|
||||
response = integration_client.post(
|
||||
"/api/v1/teams",
|
||||
json={"name": "Second Team", "slug": team_slug},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert "already exists" in response.json()["detail"].lower()
|
||||
|
||||
# Cleanup
|
||||
integration_client.delete(f"/api/v1/teams/{team_slug}")
|
||||
|
||||
def test_create_team_invalid_slug(self, integration_client):
|
||||
"""Test that invalid team slugs are rejected."""
|
||||
invalid_slugs = [
|
||||
"UPPERCASE",
|
||||
"with spaces",
|
||||
"-starts-with-hyphen",
|
||||
"ends-with-hyphen-",
|
||||
"has--double--hyphen",
|
||||
]
|
||||
|
||||
for invalid_slug in invalid_slugs:
|
||||
response = integration_client.post(
|
||||
"/api/v1/teams",
|
||||
json={"name": "Test", "slug": invalid_slug},
|
||||
)
|
||||
assert response.status_code == 422, f"Slug '{invalid_slug}' should be invalid"
|
||||
|
||||
def test_list_teams(self, integration_client, unique_test_id):
|
||||
"""Test listing teams the user belongs to."""
|
||||
# Create a team
|
||||
team_slug = f"list-team-{unique_test_id}"
|
||||
integration_client.post(
|
||||
"/api/v1/teams",
|
||||
json={"name": "List Test Team", "slug": team_slug},
|
||||
)
|
||||
|
||||
# List teams
|
||||
response = integration_client.get("/api/v1/teams")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert "items" in data
|
||||
assert "pagination" in data
|
||||
|
||||
# Find our team
|
||||
team = next((t for t in data["items"] if t["slug"] == team_slug), None)
|
||||
assert team is not None
|
||||
assert team["name"] == "List Test Team"
|
||||
|
||||
# Cleanup
|
||||
integration_client.delete(f"/api/v1/teams/{team_slug}")
|
||||
|
||||
def test_get_team(self, integration_client, unique_test_id):
|
||||
"""Test getting team details."""
|
||||
team_slug = f"get-team-{unique_test_id}"
|
||||
integration_client.post(
|
||||
"/api/v1/teams",
|
||||
json={"name": "Get Test Team", "slug": team_slug, "description": "Test"},
|
||||
)
|
||||
|
||||
response = integration_client.get(f"/api/v1/teams/{team_slug}")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert data["slug"] == team_slug
|
||||
assert data["name"] == "Get Test Team"
|
||||
assert data["user_role"] == "owner"
|
||||
|
||||
# Cleanup
|
||||
integration_client.delete(f"/api/v1/teams/{team_slug}")
|
||||
|
||||
def test_get_nonexistent_team(self, integration_client):
|
||||
"""Test getting a team that doesn't exist."""
|
||||
response = integration_client.get("/api/v1/teams/nonexistent-team-12345")
|
||||
assert response.status_code == 404
|
||||
|
||||
def test_update_team(self, integration_client, unique_test_id):
|
||||
"""Test updating team details."""
|
||||
team_slug = f"update-team-{unique_test_id}"
|
||||
integration_client.post(
|
||||
"/api/v1/teams",
|
||||
json={"name": "Original Name", "slug": team_slug},
|
||||
)
|
||||
|
||||
response = integration_client.put(
|
||||
f"/api/v1/teams/{team_slug}",
|
||||
json={"name": "Updated Name", "description": "New description"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert data["name"] == "Updated Name"
|
||||
assert data["description"] == "New description"
|
||||
assert data["slug"] == team_slug # Slug should not change
|
||||
|
||||
# Cleanup
|
||||
integration_client.delete(f"/api/v1/teams/{team_slug}")
|
||||
|
||||
def test_delete_team(self, integration_client, unique_test_id):
|
||||
"""Test deleting a team."""
|
||||
team_slug = f"delete-team-{unique_test_id}"
|
||||
integration_client.post(
|
||||
"/api/v1/teams",
|
||||
json={"name": "Delete Test Team", "slug": team_slug},
|
||||
)
|
||||
|
||||
response = integration_client.delete(f"/api/v1/teams/{team_slug}")
|
||||
assert response.status_code == 204
|
||||
|
||||
# Verify team is gone
|
||||
response = integration_client.get(f"/api/v1/teams/{team_slug}")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestTeamMembers:
|
||||
"""Tests for team membership management."""
|
||||
|
||||
@pytest.fixture
|
||||
def test_team(self, integration_client, unique_test_id):
|
||||
"""Create a test team for member tests."""
|
||||
team_slug = f"member-team-{unique_test_id}"
|
||||
response = integration_client.post(
|
||||
"/api/v1/teams",
|
||||
json={"name": "Member Test Team", "slug": team_slug},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
yield team_slug
|
||||
|
||||
# Cleanup
|
||||
try:
|
||||
integration_client.delete(f"/api/v1/teams/{team_slug}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def test_list_members(self, integration_client, test_team):
|
||||
"""Test listing team members."""
|
||||
response = integration_client.get(f"/api/v1/teams/{test_team}/members")
|
||||
assert response.status_code == 200
|
||||
|
||||
members = response.json()
|
||||
assert len(members) == 1
|
||||
assert members[0]["role"] == "owner"
|
||||
|
||||
def test_owner_is_first_member(self, integration_client, test_team):
|
||||
"""Test that the team creator is automatically the owner."""
|
||||
response = integration_client.get(f"/api/v1/teams/{test_team}/members")
|
||||
members = response.json()
|
||||
|
||||
assert len(members) >= 1
|
||||
owner = next((m for m in members if m["role"] == "owner"), None)
|
||||
assert owner is not None
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestTeamProjects:
|
||||
"""Tests for team project management."""
|
||||
|
||||
@pytest.fixture
|
||||
def test_team(self, integration_client, unique_test_id):
|
||||
"""Create a test team for project tests."""
|
||||
team_slug = f"proj-team-{unique_test_id}"
|
||||
response = integration_client.post(
|
||||
"/api/v1/teams",
|
||||
json={"name": "Project Test Team", "slug": team_slug},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
data = response.json()
|
||||
yield {"slug": team_slug, "id": data["id"]}
|
||||
|
||||
# Cleanup
|
||||
try:
|
||||
integration_client.delete(f"/api/v1/teams/{team_slug}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def test_list_team_projects_empty(self, integration_client, test_team):
|
||||
"""Test listing projects in an empty team."""
|
||||
response = integration_client.get(f"/api/v1/teams/{test_team['slug']}/projects")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert data["items"] == []
|
||||
assert data["pagination"]["total"] == 0
|
||||
|
||||
def test_create_project_in_team(self, integration_client, test_team, unique_test_id):
|
||||
"""Test creating a project within a team."""
|
||||
project_name = f"team-project-{unique_test_id}"
|
||||
|
||||
response = integration_client.post(
|
||||
"/api/v1/projects",
|
||||
json={
|
||||
"name": project_name,
|
||||
"description": "A team project",
|
||||
"team_id": test_team["id"],
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200, f"Failed to create project: {response.text}"
|
||||
|
||||
data = response.json()
|
||||
assert data["team_id"] == test_team["id"]
|
||||
assert data["team_slug"] == test_team["slug"]
|
||||
|
||||
# Verify project appears in team projects list
|
||||
response = integration_client.get(f"/api/v1/teams/{test_team['slug']}/projects")
|
||||
assert response.status_code == 200
|
||||
projects = response.json()["items"]
|
||||
assert any(p["name"] == project_name for p in projects)
|
||||
|
||||
# Cleanup
|
||||
integration_client.delete(f"/api/v1/projects/{project_name}")
|
||||
|
||||
def test_project_team_info_in_response(self, integration_client, test_team, unique_test_id):
|
||||
"""Test that project responses include team info."""
|
||||
project_name = f"team-info-project-{unique_test_id}"
|
||||
|
||||
# Create project in team
|
||||
integration_client.post(
|
||||
"/api/v1/projects",
|
||||
json={"name": project_name, "team_id": test_team["id"]},
|
||||
)
|
||||
|
||||
# Get project and verify team info
|
||||
response = integration_client.get(f"/api/v1/projects/{project_name}")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert data["team_id"] == test_team["id"]
|
||||
assert data["team_slug"] == test_team["slug"]
|
||||
assert data["team_name"] == "Project Test Team"
|
||||
|
||||
# Cleanup
|
||||
integration_client.delete(f"/api/v1/projects/{project_name}")
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestTeamAuthorization:
|
||||
"""Tests for team-based authorization."""
|
||||
|
||||
def test_cannot_delete_team_with_projects(self, integration_client, unique_test_id):
|
||||
"""Test that teams with projects cannot be deleted."""
|
||||
team_slug = f"nodelete-team-{unique_test_id}"
|
||||
project_name = f"nodelete-project-{unique_test_id}"
|
||||
|
||||
# Create team
|
||||
response = integration_client.post(
|
||||
"/api/v1/teams",
|
||||
json={"name": "No Delete Team", "slug": team_slug},
|
||||
)
|
||||
team_id = response.json()["id"]
|
||||
|
||||
# Create project in team
|
||||
integration_client.post(
|
||||
"/api/v1/projects",
|
||||
json={"name": project_name, "team_id": team_id},
|
||||
)
|
||||
|
||||
# Try to delete team - should fail
|
||||
response = integration_client.delete(f"/api/v1/teams/{team_slug}")
|
||||
assert response.status_code == 400
|
||||
assert "project" in response.json()["detail"].lower()
|
||||
|
||||
# Cleanup - delete project first, then team
|
||||
integration_client.delete(f"/api/v1/projects/{project_name}")
|
||||
integration_client.delete(f"/api/v1/teams/{team_slug}")
|
||||
Reference in New Issue
Block a user