- 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.
685 lines
24 KiB
Python
685 lines
24 KiB
Python
"""
|
|
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
|