feat(tests): add comprehensive E2E tests for MCP and Agent workflows
- Introduced end-to-end tests for MCP workflows, including server discovery, authentication, context engine operations, error handling, and input validation. - Added full lifecycle tests for agent workflows, covering type management, instance spawning, status transitions, and admin-only operations. - Enhanced test coverage for real-world MCP and Agent scenarios across PostgreSQL and async environments.
This commit is contained in:
684
backend/tests/e2e/test_project_workflows.py
Normal file
684
backend/tests/e2e/test_project_workflows.py
Normal file
@@ -0,0 +1,684 @@
|
||||
"""
|
||||
Project and Agent E2E Workflow Tests.
|
||||
|
||||
Tests complete project management workflows with real PostgreSQL:
|
||||
- Project CRUD and lifecycle management
|
||||
- Agent spawning and lifecycle
|
||||
- Issue management within projects
|
||||
- Sprint planning and execution
|
||||
|
||||
Usage:
|
||||
make test-e2e # Run all E2E tests
|
||||
"""
|
||||
|
||||
from datetime import date, timedelta
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.e2e,
|
||||
pytest.mark.postgres,
|
||||
pytest.mark.asyncio,
|
||||
]
|
||||
|
||||
|
||||
class TestProjectCRUDWorkflows:
|
||||
"""Test complete project CRUD workflows."""
|
||||
|
||||
async def test_create_project_workflow(self, e2e_client):
|
||||
"""Test creating a project as authenticated user."""
|
||||
# Register and login
|
||||
email = f"project-owner-{uuid4().hex[:8]}@example.com"
|
||||
password = "SecurePass123!"
|
||||
|
||||
await e2e_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"email": email,
|
||||
"password": password,
|
||||
"first_name": "Project",
|
||||
"last_name": "Owner",
|
||||
},
|
||||
)
|
||||
login_resp = await e2e_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": email, "password": password},
|
||||
)
|
||||
tokens = login_resp.json()
|
||||
|
||||
# Create project
|
||||
project_slug = f"test-project-{uuid4().hex[:8]}"
|
||||
create_resp = await e2e_client.post(
|
||||
"/api/v1/projects",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
json={
|
||||
"name": "E2E Test Project",
|
||||
"slug": project_slug,
|
||||
"description": "A project for E2E testing",
|
||||
"autonomy_level": "milestone",
|
||||
},
|
||||
)
|
||||
|
||||
assert create_resp.status_code == 201, f"Failed: {create_resp.text}"
|
||||
project = create_resp.json()
|
||||
assert project["name"] == "E2E Test Project"
|
||||
assert project["slug"] == project_slug
|
||||
assert project["status"] == "active"
|
||||
assert project["agent_count"] == 0
|
||||
assert project["issue_count"] == 0
|
||||
|
||||
async def test_list_projects_only_shows_owned(self, e2e_client):
|
||||
"""Test that users only see their own projects."""
|
||||
# Create two users with projects
|
||||
users = []
|
||||
for i in range(2):
|
||||
email = f"user-{i}-{uuid4().hex[:8]}@example.com"
|
||||
password = "SecurePass123!"
|
||||
|
||||
await e2e_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"email": email,
|
||||
"password": password,
|
||||
"first_name": f"User{i}",
|
||||
"last_name": "Test",
|
||||
},
|
||||
)
|
||||
login_resp = await e2e_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": email, "password": password},
|
||||
)
|
||||
tokens = login_resp.json()
|
||||
|
||||
# Each user creates their own project
|
||||
project_slug = f"user{i}-project-{uuid4().hex[:8]}"
|
||||
await e2e_client.post(
|
||||
"/api/v1/projects",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
json={
|
||||
"name": f"User {i} Project",
|
||||
"slug": project_slug,
|
||||
},
|
||||
)
|
||||
users.append({"email": email, "tokens": tokens, "slug": project_slug})
|
||||
|
||||
# User 0 should only see their project
|
||||
list_resp = await e2e_client.get(
|
||||
"/api/v1/projects",
|
||||
headers={"Authorization": f"Bearer {users[0]['tokens']['access_token']}"},
|
||||
)
|
||||
assert list_resp.status_code == 200
|
||||
data = list_resp.json()
|
||||
slugs = [p["slug"] for p in data["data"]]
|
||||
assert users[0]["slug"] in slugs
|
||||
assert users[1]["slug"] not in slugs
|
||||
|
||||
async def test_project_lifecycle_pause_resume(self, e2e_client):
|
||||
"""Test pausing and resuming a project."""
|
||||
# Setup user and project
|
||||
email = f"lifecycle-{uuid4().hex[:8]}@example.com"
|
||||
password = "SecurePass123!"
|
||||
|
||||
await e2e_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"email": email,
|
||||
"password": password,
|
||||
"first_name": "Lifecycle",
|
||||
"last_name": "Test",
|
||||
},
|
||||
)
|
||||
login_resp = await e2e_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": email, "password": password},
|
||||
)
|
||||
tokens = login_resp.json()
|
||||
|
||||
project_slug = f"lifecycle-project-{uuid4().hex[:8]}"
|
||||
create_resp = await e2e_client.post(
|
||||
"/api/v1/projects",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
json={"name": "Lifecycle Project", "slug": project_slug},
|
||||
)
|
||||
project = create_resp.json()
|
||||
project_id = project["id"]
|
||||
|
||||
# Pause the project
|
||||
pause_resp = await e2e_client.post(
|
||||
f"/api/v1/projects/{project_id}/pause",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
)
|
||||
assert pause_resp.status_code == 200
|
||||
assert pause_resp.json()["status"] == "paused"
|
||||
|
||||
# Resume the project
|
||||
resume_resp = await e2e_client.post(
|
||||
f"/api/v1/projects/{project_id}/resume",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
)
|
||||
assert resume_resp.status_code == 200
|
||||
assert resume_resp.json()["status"] == "active"
|
||||
|
||||
async def test_project_archive(self, e2e_client):
|
||||
"""Test archiving a project (soft delete)."""
|
||||
# Setup user and project
|
||||
email = f"archive-{uuid4().hex[:8]}@example.com"
|
||||
password = "SecurePass123!"
|
||||
|
||||
await e2e_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"email": email,
|
||||
"password": password,
|
||||
"first_name": "Archive",
|
||||
"last_name": "Test",
|
||||
},
|
||||
)
|
||||
login_resp = await e2e_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": email, "password": password},
|
||||
)
|
||||
tokens = login_resp.json()
|
||||
|
||||
project_slug = f"archive-project-{uuid4().hex[:8]}"
|
||||
create_resp = await e2e_client.post(
|
||||
"/api/v1/projects",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
json={"name": "Archive Project", "slug": project_slug},
|
||||
)
|
||||
project = create_resp.json()
|
||||
project_id = project["id"]
|
||||
|
||||
# Archive the project
|
||||
archive_resp = await e2e_client.delete(
|
||||
f"/api/v1/projects/{project_id}",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
)
|
||||
assert archive_resp.status_code == 200
|
||||
assert archive_resp.json()["success"] is True
|
||||
|
||||
# Verify project is archived
|
||||
get_resp = await e2e_client.get(
|
||||
f"/api/v1/projects/{project_id}",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
)
|
||||
assert get_resp.status_code == 200
|
||||
assert get_resp.json()["status"] == "archived"
|
||||
|
||||
|
||||
class TestIssueWorkflows:
|
||||
"""Test issue management workflows within projects."""
|
||||
|
||||
async def test_create_and_list_issues(self, e2e_client):
|
||||
"""Test creating and listing issues in a project."""
|
||||
# Setup user and project
|
||||
email = f"issue-test-{uuid4().hex[:8]}@example.com"
|
||||
password = "SecurePass123!"
|
||||
|
||||
await e2e_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"email": email,
|
||||
"password": password,
|
||||
"first_name": "Issue",
|
||||
"last_name": "Tester",
|
||||
},
|
||||
)
|
||||
login_resp = await e2e_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": email, "password": password},
|
||||
)
|
||||
tokens = login_resp.json()
|
||||
|
||||
project_slug = f"issue-project-{uuid4().hex[:8]}"
|
||||
create_resp = await e2e_client.post(
|
||||
"/api/v1/projects",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
json={"name": "Issue Test Project", "slug": project_slug},
|
||||
)
|
||||
project = create_resp.json()
|
||||
project_id = project["id"]
|
||||
|
||||
# Create multiple issues
|
||||
issues = []
|
||||
for i in range(3):
|
||||
issue_resp = await e2e_client.post(
|
||||
f"/api/v1/projects/{project_id}/issues",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
json={
|
||||
"project_id": project_id,
|
||||
"title": f"Test Issue {i + 1}",
|
||||
"body": f"Description for issue {i + 1}",
|
||||
"priority": ["low", "medium", "high"][i],
|
||||
},
|
||||
)
|
||||
assert issue_resp.status_code == 201, f"Failed: {issue_resp.text}"
|
||||
issues.append(issue_resp.json())
|
||||
|
||||
# List issues
|
||||
list_resp = await e2e_client.get(
|
||||
f"/api/v1/projects/{project_id}/issues",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
)
|
||||
assert list_resp.status_code == 200
|
||||
data = list_resp.json()
|
||||
assert data["pagination"]["total"] == 3
|
||||
|
||||
async def test_issue_status_transitions(self, e2e_client):
|
||||
"""Test issue status workflow transitions."""
|
||||
# Setup user and project
|
||||
email = f"status-test-{uuid4().hex[:8]}@example.com"
|
||||
password = "SecurePass123!"
|
||||
|
||||
await e2e_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"email": email,
|
||||
"password": password,
|
||||
"first_name": "Status",
|
||||
"last_name": "Tester",
|
||||
},
|
||||
)
|
||||
login_resp = await e2e_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": email, "password": password},
|
||||
)
|
||||
tokens = login_resp.json()
|
||||
|
||||
project_slug = f"status-project-{uuid4().hex[:8]}"
|
||||
create_resp = await e2e_client.post(
|
||||
"/api/v1/projects",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
json={"name": "Status Test Project", "slug": project_slug},
|
||||
)
|
||||
project = create_resp.json()
|
||||
project_id = project["id"]
|
||||
|
||||
# Create issue
|
||||
issue_resp = await e2e_client.post(
|
||||
f"/api/v1/projects/{project_id}/issues",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
json={
|
||||
"project_id": project_id,
|
||||
"title": "Status Workflow Issue",
|
||||
"body": "Testing status transitions",
|
||||
},
|
||||
)
|
||||
issue = issue_resp.json()
|
||||
issue_id = issue["id"]
|
||||
assert issue["status"] == "open"
|
||||
|
||||
# Transition through statuses
|
||||
for new_status in ["in_progress", "in_review", "closed"]:
|
||||
update_resp = await e2e_client.patch(
|
||||
f"/api/v1/projects/{project_id}/issues/{issue_id}",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
json={"status": new_status},
|
||||
)
|
||||
assert update_resp.status_code == 200, f"Failed: {update_resp.text}"
|
||||
assert update_resp.json()["status"] == new_status
|
||||
|
||||
async def test_issue_filtering(self, e2e_client):
|
||||
"""Test issue filtering by status and priority."""
|
||||
# Setup user and project
|
||||
email = f"filter-test-{uuid4().hex[:8]}@example.com"
|
||||
password = "SecurePass123!"
|
||||
|
||||
await e2e_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"email": email,
|
||||
"password": password,
|
||||
"first_name": "Filter",
|
||||
"last_name": "Tester",
|
||||
},
|
||||
)
|
||||
login_resp = await e2e_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": email, "password": password},
|
||||
)
|
||||
tokens = login_resp.json()
|
||||
|
||||
project_slug = f"filter-project-{uuid4().hex[:8]}"
|
||||
create_resp = await e2e_client.post(
|
||||
"/api/v1/projects",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
json={"name": "Filter Test Project", "slug": project_slug},
|
||||
)
|
||||
project = create_resp.json()
|
||||
project_id = project["id"]
|
||||
|
||||
# Create issues with different priorities
|
||||
for priority in ["low", "medium", "high"]:
|
||||
await e2e_client.post(
|
||||
f"/api/v1/projects/{project_id}/issues",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
json={
|
||||
"project_id": project_id,
|
||||
"title": f"{priority.title()} Priority Issue",
|
||||
"priority": priority,
|
||||
},
|
||||
)
|
||||
|
||||
# Filter by high priority
|
||||
filter_resp = await e2e_client.get(
|
||||
f"/api/v1/projects/{project_id}/issues",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
params={"priority": "high"},
|
||||
)
|
||||
assert filter_resp.status_code == 200
|
||||
data = filter_resp.json()
|
||||
assert data["pagination"]["total"] == 1
|
||||
assert data["data"][0]["priority"] == "high"
|
||||
|
||||
|
||||
class TestSprintWorkflows:
|
||||
"""Test sprint planning and execution workflows."""
|
||||
|
||||
async def test_sprint_lifecycle(self, e2e_client):
|
||||
"""Test complete sprint lifecycle: plan -> start -> complete."""
|
||||
# Setup user and project
|
||||
email = f"sprint-test-{uuid4().hex[:8]}@example.com"
|
||||
password = "SecurePass123!"
|
||||
|
||||
await e2e_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"email": email,
|
||||
"password": password,
|
||||
"first_name": "Sprint",
|
||||
"last_name": "Tester",
|
||||
},
|
||||
)
|
||||
login_resp = await e2e_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": email, "password": password},
|
||||
)
|
||||
tokens = login_resp.json()
|
||||
|
||||
project_slug = f"sprint-project-{uuid4().hex[:8]}"
|
||||
create_resp = await e2e_client.post(
|
||||
"/api/v1/projects",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
json={"name": "Sprint Test Project", "slug": project_slug},
|
||||
)
|
||||
project = create_resp.json()
|
||||
project_id = project["id"]
|
||||
|
||||
# Create sprint
|
||||
today = date.today()
|
||||
sprint_resp = await e2e_client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
json={
|
||||
"project_id": project_id,
|
||||
"name": "Sprint 1",
|
||||
"number": 1,
|
||||
"goal": "Complete initial features",
|
||||
"start_date": today.isoformat(),
|
||||
"end_date": (today + timedelta(days=14)).isoformat(),
|
||||
},
|
||||
)
|
||||
assert sprint_resp.status_code == 201, f"Failed: {sprint_resp.text}"
|
||||
sprint = sprint_resp.json()
|
||||
sprint_id = sprint["id"]
|
||||
assert sprint["status"] == "planned"
|
||||
|
||||
# Start sprint
|
||||
start_resp = await e2e_client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/start",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
)
|
||||
assert start_resp.status_code == 200, f"Failed: {start_resp.text}"
|
||||
assert start_resp.json()["status"] == "active"
|
||||
|
||||
# Complete sprint
|
||||
complete_resp = await e2e_client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/complete",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
)
|
||||
assert complete_resp.status_code == 200, f"Failed: {complete_resp.text}"
|
||||
assert complete_resp.json()["status"] == "completed"
|
||||
|
||||
async def test_add_issues_to_sprint(self, e2e_client):
|
||||
"""Test adding issues to a sprint."""
|
||||
# Setup user and project
|
||||
email = f"sprint-issues-{uuid4().hex[:8]}@example.com"
|
||||
password = "SecurePass123!"
|
||||
|
||||
await e2e_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"email": email,
|
||||
"password": password,
|
||||
"first_name": "SprintIssues",
|
||||
"last_name": "Tester",
|
||||
},
|
||||
)
|
||||
login_resp = await e2e_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": email, "password": password},
|
||||
)
|
||||
tokens = login_resp.json()
|
||||
|
||||
project_slug = f"sprint-issues-project-{uuid4().hex[:8]}"
|
||||
create_resp = await e2e_client.post(
|
||||
"/api/v1/projects",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
json={"name": "Sprint Issues Project", "slug": project_slug},
|
||||
)
|
||||
project = create_resp.json()
|
||||
project_id = project["id"]
|
||||
|
||||
# Create sprint
|
||||
today = date.today()
|
||||
sprint_resp = await e2e_client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
json={
|
||||
"project_id": project_id,
|
||||
"name": "Sprint 1",
|
||||
"number": 1,
|
||||
"start_date": today.isoformat(),
|
||||
"end_date": (today + timedelta(days=14)).isoformat(),
|
||||
},
|
||||
)
|
||||
assert sprint_resp.status_code == 201, f"Failed: {sprint_resp.text}"
|
||||
sprint = sprint_resp.json()
|
||||
sprint_id = sprint["id"]
|
||||
|
||||
# Create issue
|
||||
issue_resp = await e2e_client.post(
|
||||
f"/api/v1/projects/{project_id}/issues",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
json={
|
||||
"project_id": project_id,
|
||||
"title": "Sprint Issue",
|
||||
"story_points": 5,
|
||||
},
|
||||
)
|
||||
issue = issue_resp.json()
|
||||
issue_id = issue["id"]
|
||||
|
||||
# Add issue to sprint
|
||||
add_resp = await e2e_client.post(
|
||||
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/issues",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
params={"issue_id": issue_id},
|
||||
)
|
||||
assert add_resp.status_code == 200, f"Failed: {add_resp.text}"
|
||||
|
||||
# Verify issue is in sprint
|
||||
issue_check = await e2e_client.get(
|
||||
f"/api/v1/projects/{project_id}/issues/{issue_id}",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
)
|
||||
assert issue_check.json()["sprint_id"] == sprint_id
|
||||
|
||||
|
||||
class TestCrossEntityValidation:
|
||||
"""Test validation across related entities."""
|
||||
|
||||
async def test_cannot_access_other_users_project(self, e2e_client):
|
||||
"""Test that users cannot access projects they don't own."""
|
||||
# Create two users
|
||||
owner_email = f"owner-{uuid4().hex[:8]}@example.com"
|
||||
other_email = f"other-{uuid4().hex[:8]}@example.com"
|
||||
password = "SecurePass123!"
|
||||
|
||||
# Register owner
|
||||
await e2e_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"email": owner_email,
|
||||
"password": password,
|
||||
"first_name": "Owner",
|
||||
"last_name": "User",
|
||||
},
|
||||
)
|
||||
owner_tokens = (
|
||||
await e2e_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": owner_email, "password": password},
|
||||
)
|
||||
).json()
|
||||
|
||||
# Register other user
|
||||
await e2e_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"email": other_email,
|
||||
"password": password,
|
||||
"first_name": "Other",
|
||||
"last_name": "User",
|
||||
},
|
||||
)
|
||||
other_tokens = (
|
||||
await e2e_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": other_email, "password": password},
|
||||
)
|
||||
).json()
|
||||
|
||||
# Owner creates project
|
||||
project_slug = f"private-project-{uuid4().hex[:8]}"
|
||||
create_resp = await e2e_client.post(
|
||||
"/api/v1/projects",
|
||||
headers={"Authorization": f"Bearer {owner_tokens['access_token']}"},
|
||||
json={"name": "Private Project", "slug": project_slug},
|
||||
)
|
||||
project = create_resp.json()
|
||||
project_id = project["id"]
|
||||
|
||||
# Other user tries to access
|
||||
access_resp = await e2e_client.get(
|
||||
f"/api/v1/projects/{project_id}",
|
||||
headers={"Authorization": f"Bearer {other_tokens['access_token']}"},
|
||||
)
|
||||
assert access_resp.status_code == 403
|
||||
|
||||
async def test_duplicate_project_slug_rejected(self, e2e_client):
|
||||
"""Test that duplicate project slugs are rejected."""
|
||||
email = f"dup-test-{uuid4().hex[:8]}@example.com"
|
||||
password = "SecurePass123!"
|
||||
|
||||
await e2e_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"email": email,
|
||||
"password": password,
|
||||
"first_name": "Dup",
|
||||
"last_name": "Tester",
|
||||
},
|
||||
)
|
||||
tokens = (
|
||||
await e2e_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": email, "password": password},
|
||||
)
|
||||
).json()
|
||||
|
||||
slug = f"unique-slug-{uuid4().hex[:8]}"
|
||||
|
||||
# First creation should succeed
|
||||
resp1 = await e2e_client.post(
|
||||
"/api/v1/projects",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
json={"name": "First Project", "slug": slug},
|
||||
)
|
||||
assert resp1.status_code == 201
|
||||
|
||||
# Second creation with same slug should fail
|
||||
resp2 = await e2e_client.post(
|
||||
"/api/v1/projects",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
json={"name": "Second Project", "slug": slug},
|
||||
)
|
||||
assert resp2.status_code == 409 # Conflict
|
||||
|
||||
|
||||
class TestIssueStats:
|
||||
"""Test issue statistics endpoints."""
|
||||
|
||||
async def test_issue_stats_aggregation(self, e2e_client):
|
||||
"""Test that issue stats are correctly aggregated."""
|
||||
email = f"stats-test-{uuid4().hex[:8]}@example.com"
|
||||
password = "SecurePass123!"
|
||||
|
||||
await e2e_client.post(
|
||||
"/api/v1/auth/register",
|
||||
json={
|
||||
"email": email,
|
||||
"password": password,
|
||||
"first_name": "Stats",
|
||||
"last_name": "Tester",
|
||||
},
|
||||
)
|
||||
tokens = (
|
||||
await e2e_client.post(
|
||||
"/api/v1/auth/login",
|
||||
json={"email": email, "password": password},
|
||||
)
|
||||
).json()
|
||||
|
||||
project_slug = f"stats-project-{uuid4().hex[:8]}"
|
||||
create_resp = await e2e_client.post(
|
||||
"/api/v1/projects",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
json={"name": "Stats Project", "slug": project_slug},
|
||||
)
|
||||
project = create_resp.json()
|
||||
project_id = project["id"]
|
||||
|
||||
# Create issues with different priorities and story points
|
||||
await e2e_client.post(
|
||||
f"/api/v1/projects/{project_id}/issues",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
json={
|
||||
"project_id": project_id,
|
||||
"title": "High Priority",
|
||||
"priority": "high",
|
||||
"story_points": 8,
|
||||
},
|
||||
)
|
||||
await e2e_client.post(
|
||||
f"/api/v1/projects/{project_id}/issues",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
json={
|
||||
"project_id": project_id,
|
||||
"title": "Low Priority",
|
||||
"priority": "low",
|
||||
"story_points": 2,
|
||||
},
|
||||
)
|
||||
|
||||
# Get stats
|
||||
stats_resp = await e2e_client.get(
|
||||
f"/api/v1/projects/{project_id}/issues/stats",
|
||||
headers={"Authorization": f"Bearer {tokens['access_token']}"},
|
||||
)
|
||||
assert stats_resp.status_code == 200
|
||||
stats = stats_resp.json()
|
||||
assert stats["total"] == 2
|
||||
assert stats["total_story_points"] == 10
|
||||
Reference in New Issue
Block a user