- Added tests for OAuth provider admin and consent endpoints covering edge cases. - Extended agent-related tests to handle incorrect project associations and lifecycle state transitions. - Introduced tests for sprint status transitions and validation checks. - Improved multiline formatting consistency across all test functions.
1049 lines
38 KiB
Python
1049 lines
38 KiB
Python
# tests/api/routes/syndarix/test_projects.py
|
|
"""
|
|
Comprehensive tests for the Projects API endpoints.
|
|
|
|
Tests cover:
|
|
- CRUD operations (create, read, update, archive)
|
|
- Lifecycle operations (pause, resume)
|
|
- Authorization (ownership checks, superuser access)
|
|
- Pagination and filtering
|
|
- Error handling (not found, validation, duplicates)
|
|
"""
|
|
|
|
import uuid
|
|
|
|
import pytest
|
|
from fastapi import status
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
class TestCreateProject:
|
|
"""Tests for POST /api/v1/projects endpoint."""
|
|
|
|
async def test_create_project_success(self, client, user_token):
|
|
"""Test successful project creation."""
|
|
response = await client.post(
|
|
"/api/v1/projects",
|
|
json={
|
|
"name": "Test Project",
|
|
"slug": "test-project",
|
|
"description": "A test project description",
|
|
"autonomy_level": "milestone",
|
|
},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_201_CREATED
|
|
data = response.json()
|
|
|
|
assert data["name"] == "Test Project"
|
|
assert data["slug"] == "test-project"
|
|
assert data["description"] == "A test project description"
|
|
assert data["autonomy_level"] == "milestone"
|
|
assert data["status"] == "active"
|
|
assert data["agent_count"] == 0
|
|
assert data["issue_count"] == 0
|
|
assert data["active_sprint_name"] is None
|
|
assert "id" in data
|
|
assert "created_at" in data
|
|
assert "updated_at" in data
|
|
|
|
async def test_create_project_minimal_fields(self, client, user_token):
|
|
"""Test creating project with only required fields."""
|
|
response = await client.post(
|
|
"/api/v1/projects",
|
|
json={
|
|
"name": "Minimal Project",
|
|
"slug": "minimal-project",
|
|
},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_201_CREATED
|
|
data = response.json()
|
|
assert data["name"] == "Minimal Project"
|
|
assert data["slug"] == "minimal-project"
|
|
assert data["autonomy_level"] == "milestone" # Default
|
|
assert data["status"] == "active" # Default
|
|
|
|
async def test_create_project_with_settings(self, client, user_token):
|
|
"""Test creating project with custom settings."""
|
|
response = await client.post(
|
|
"/api/v1/projects",
|
|
json={
|
|
"name": "Project With Settings",
|
|
"slug": "project-with-settings",
|
|
"settings": {"theme": "dark", "notifications": True},
|
|
},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_201_CREATED
|
|
data = response.json()
|
|
assert data["settings"]["theme"] == "dark"
|
|
assert data["settings"]["notifications"] is True
|
|
|
|
async def test_create_project_duplicate_slug(self, client, user_token):
|
|
"""Test that duplicate slugs are rejected."""
|
|
# Create first project
|
|
await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "First Project", "slug": "duplicate-slug"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
# Attempt duplicate
|
|
response = await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Second Project", "slug": "duplicate-slug"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_409_CONFLICT
|
|
data = response.json()
|
|
assert "already exists" in data["errors"][0]["message"].lower()
|
|
|
|
async def test_create_project_invalid_slug_format(self, client, user_token):
|
|
"""Test that invalid slug formats are rejected."""
|
|
# Uppercase letters
|
|
response = await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Bad Slug", "slug": "Invalid-Slug"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
|
|
|
# Starting with hyphen
|
|
response = await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Bad Slug", "slug": "-invalid"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
|
|
|
# Consecutive hyphens
|
|
response = await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Bad Slug", "slug": "invalid--slug"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
|
|
|
async def test_create_project_empty_name(self, client, user_token):
|
|
"""Test that empty name is rejected."""
|
|
response = await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "", "slug": "empty-name"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
|
|
|
async def test_create_project_unauthenticated(self, client):
|
|
"""Test that unauthenticated requests are rejected."""
|
|
response = await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Test Project", "slug": "test-project"},
|
|
)
|
|
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
|
|
|
async def test_create_project_all_autonomy_levels(self, client, user_token):
|
|
"""Test creating projects with all autonomy levels."""
|
|
levels = ["full_control", "milestone", "autonomous"]
|
|
|
|
for level in levels:
|
|
# Slugs must use hyphens, not underscores
|
|
slug = f"project-{level.replace('_', '-')}"
|
|
response = await client.post(
|
|
"/api/v1/projects",
|
|
json={
|
|
"name": f"Project {level}",
|
|
"slug": slug,
|
|
"autonomy_level": level,
|
|
},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
assert response.status_code == status.HTTP_201_CREATED
|
|
assert response.json()["autonomy_level"] == level
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
class TestListProjects:
|
|
"""Tests for GET /api/v1/projects endpoint."""
|
|
|
|
async def test_list_projects_empty(self, client, user_token):
|
|
"""Test listing projects when none exist."""
|
|
response = await client.get(
|
|
"/api/v1/projects",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
data = response.json()
|
|
assert "data" in data
|
|
assert "pagination" in data
|
|
assert data["data"] == []
|
|
assert data["pagination"]["total"] == 0
|
|
|
|
async def test_list_projects_with_data(self, client, user_token):
|
|
"""Test listing projects returns created projects."""
|
|
# Create projects
|
|
for i in range(3):
|
|
await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": f"Project {i}", "slug": f"project-{i}"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
response = await client.get(
|
|
"/api/v1/projects",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
data = response.json()
|
|
assert len(data["data"]) == 3
|
|
assert data["pagination"]["total"] == 3
|
|
|
|
async def test_list_projects_pagination(self, client, user_token):
|
|
"""Test pagination works correctly."""
|
|
# Create 5 projects
|
|
for i in range(5):
|
|
await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": f"Project {i}", "slug": f"project-{i}"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
# Get first page (2 items)
|
|
response = await client.get(
|
|
"/api/v1/projects?page=1&limit=2",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
data = response.json()
|
|
assert len(data["data"]) == 2
|
|
assert data["pagination"]["total"] == 5
|
|
assert data["pagination"]["page"] == 1
|
|
assert data["pagination"]["total_pages"] == 3
|
|
|
|
# Get second page
|
|
response = await client.get(
|
|
"/api/v1/projects?page=2&limit=2",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
data = response.json()
|
|
assert len(data["data"]) == 2
|
|
assert data["pagination"]["page"] == 2
|
|
|
|
async def test_list_projects_filter_by_status(self, client, user_token):
|
|
"""Test filtering projects by status."""
|
|
# Create active project
|
|
await client.post(
|
|
"/api/v1/projects",
|
|
json={
|
|
"name": "Active Project",
|
|
"slug": "active-project",
|
|
"status": "active",
|
|
},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
# Create paused project
|
|
await client.post(
|
|
"/api/v1/projects",
|
|
json={
|
|
"name": "Paused Project",
|
|
"slug": "paused-project",
|
|
"status": "paused",
|
|
},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
# Filter by active
|
|
response = await client.get(
|
|
"/api/v1/projects?status=active",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
data = response.json()
|
|
assert len(data["data"]) == 1
|
|
assert data["data"][0]["status"] == "active"
|
|
|
|
async def test_list_projects_search(self, client, user_token):
|
|
"""Test searching projects by name."""
|
|
await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Alpha Project", "slug": "alpha-project"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Beta Project", "slug": "beta-project"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
response = await client.get(
|
|
"/api/v1/projects?search=alpha",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
data = response.json()
|
|
assert len(data["data"]) == 1
|
|
assert "alpha" in data["data"][0]["name"].lower()
|
|
|
|
async def test_list_projects_user_isolation(
|
|
self, client, user_token, superuser_token, async_test_user
|
|
):
|
|
"""Test that users only see their own projects."""
|
|
# Create project as regular user
|
|
await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "User Project", "slug": "user-project"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
# Create project as superuser
|
|
await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Admin Project", "slug": "admin-project"},
|
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
|
)
|
|
|
|
# Regular user should only see their project
|
|
response = await client.get(
|
|
"/api/v1/projects",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
assert response.status_code == status.HTTP_200_OK
|
|
data = response.json()
|
|
assert len(data["data"]) == 1
|
|
assert data["data"][0]["slug"] == "user-project"
|
|
|
|
async def test_list_projects_superuser_all_projects(
|
|
self, client, user_token, superuser_token
|
|
):
|
|
"""Test that superuser can see all projects with all_projects flag."""
|
|
# Create project as regular user
|
|
await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "User Project", "slug": "user-project"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
# Create project as superuser
|
|
await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Admin Project", "slug": "admin-project"},
|
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
|
)
|
|
|
|
# Superuser with all_projects=true should see both
|
|
response = await client.get(
|
|
"/api/v1/projects?all_projects=true",
|
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
|
)
|
|
assert response.status_code == status.HTTP_200_OK
|
|
data = response.json()
|
|
assert len(data["data"]) == 2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
class TestGetProject:
|
|
"""Tests for GET /api/v1/projects/{project_id} endpoint."""
|
|
|
|
async def test_get_project_success(self, client, user_token):
|
|
"""Test getting a project by ID."""
|
|
# Create project
|
|
create_response = await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Get Test Project", "slug": "get-test-project"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
project_id = create_response.json()["id"]
|
|
|
|
# Get project
|
|
response = await client.get(
|
|
f"/api/v1/projects/{project_id}",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
data = response.json()
|
|
assert data["id"] == project_id
|
|
assert data["name"] == "Get Test Project"
|
|
|
|
async def test_get_project_not_found(self, client, user_token):
|
|
"""Test getting a non-existent project."""
|
|
fake_id = str(uuid.uuid4())
|
|
response = await client.get(
|
|
f"/api/v1/projects/{fake_id}",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_404_NOT_FOUND
|
|
assert "not found" in response.json()["errors"][0]["message"].lower()
|
|
|
|
async def test_get_project_unauthorized(self, client, user_token, superuser_token):
|
|
"""Test that users cannot access other users' projects."""
|
|
# Create project as superuser
|
|
create_response = await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Admin Project", "slug": "admin-project"},
|
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
|
)
|
|
project_id = create_response.json()["id"]
|
|
|
|
# Try to access as regular user
|
|
response = await client.get(
|
|
f"/api/v1/projects/{project_id}",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
|
|
|
async def test_get_project_superuser_can_access_any(
|
|
self, client, user_token, superuser_token
|
|
):
|
|
"""Test that superuser can access any project."""
|
|
# Create project as regular user
|
|
create_response = await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "User Project", "slug": "user-project"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
project_id = create_response.json()["id"]
|
|
|
|
# Access as superuser
|
|
response = await client.get(
|
|
f"/api/v1/projects/{project_id}",
|
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
class TestGetProjectBySlug:
|
|
"""Tests for GET /api/v1/projects/slug/{slug} endpoint."""
|
|
|
|
async def test_get_project_by_slug_success(self, client, user_token):
|
|
"""Test getting a project by slug."""
|
|
# Create project
|
|
await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Slug Test Project", "slug": "slug-test-project"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
# Get by slug
|
|
response = await client.get(
|
|
"/api/v1/projects/slug/slug-test-project",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
data = response.json()
|
|
assert data["slug"] == "slug-test-project"
|
|
assert data["name"] == "Slug Test Project"
|
|
|
|
async def test_get_project_by_slug_not_found(self, client, user_token):
|
|
"""Test getting a non-existent project by slug."""
|
|
response = await client.get(
|
|
"/api/v1/projects/slug/non-existent-slug",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_404_NOT_FOUND
|
|
|
|
async def test_get_project_by_slug_unauthorized(
|
|
self, client, user_token, superuser_token
|
|
):
|
|
"""Test that users cannot access other users' projects by slug."""
|
|
# Create project as superuser
|
|
await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Admin Project", "slug": "admin-project"},
|
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
|
)
|
|
|
|
# Try to access as regular user
|
|
response = await client.get(
|
|
"/api/v1/projects/slug/admin-project",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
class TestUpdateProject:
|
|
"""Tests for PATCH /api/v1/projects/{project_id} endpoint."""
|
|
|
|
async def test_update_project_success(self, client, user_token):
|
|
"""Test updating a project."""
|
|
# Create project
|
|
create_response = await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Original Name", "slug": "original-slug"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
project_id = create_response.json()["id"]
|
|
|
|
# Update project
|
|
response = await client.patch(
|
|
f"/api/v1/projects/{project_id}",
|
|
json={"name": "Updated Name", "description": "New description"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
data = response.json()
|
|
assert data["name"] == "Updated Name"
|
|
assert data["description"] == "New description"
|
|
assert data["slug"] == "original-slug" # Unchanged
|
|
|
|
async def test_update_project_slug(self, client, user_token):
|
|
"""Test updating project slug."""
|
|
# Create project
|
|
create_response = await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Test Project", "slug": "old-slug"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
project_id = create_response.json()["id"]
|
|
|
|
# Update slug
|
|
response = await client.patch(
|
|
f"/api/v1/projects/{project_id}",
|
|
json={"slug": "new-slug"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
assert response.json()["slug"] == "new-slug"
|
|
|
|
async def test_update_project_autonomy_level(self, client, user_token):
|
|
"""Test updating project autonomy level."""
|
|
# Create project with milestone autonomy
|
|
create_response = await client.post(
|
|
"/api/v1/projects",
|
|
json={
|
|
"name": "Test Project",
|
|
"slug": "test-project",
|
|
"autonomy_level": "milestone",
|
|
},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
project_id = create_response.json()["id"]
|
|
|
|
# Update to autonomous
|
|
response = await client.patch(
|
|
f"/api/v1/projects/{project_id}",
|
|
json={"autonomy_level": "autonomous"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
assert response.json()["autonomy_level"] == "autonomous"
|
|
|
|
async def test_update_project_not_found(self, client, user_token):
|
|
"""Test updating a non-existent project."""
|
|
fake_id = str(uuid.uuid4())
|
|
response = await client.patch(
|
|
f"/api/v1/projects/{fake_id}",
|
|
json={"name": "New Name"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_404_NOT_FOUND
|
|
|
|
async def test_update_project_unauthorized(
|
|
self, client, user_token, superuser_token
|
|
):
|
|
"""Test that users cannot update other users' projects."""
|
|
# Create project as superuser
|
|
create_response = await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Admin Project", "slug": "admin-project"},
|
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
|
)
|
|
project_id = create_response.json()["id"]
|
|
|
|
# Try to update as regular user
|
|
response = await client.patch(
|
|
f"/api/v1/projects/{project_id}",
|
|
json={"name": "Hacked Name"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
|
|
|
async def test_update_project_duplicate_slug(self, client, user_token):
|
|
"""Test that updating to a duplicate slug is rejected."""
|
|
# Create two projects
|
|
await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "First Project", "slug": "first-project"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
create_response = await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Second Project", "slug": "second-project"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
project_id = create_response.json()["id"]
|
|
|
|
# Try to update second project's slug to first's
|
|
response = await client.patch(
|
|
f"/api/v1/projects/{project_id}",
|
|
json={"slug": "first-project"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_409_CONFLICT
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
class TestArchiveProject:
|
|
"""Tests for DELETE /api/v1/projects/{project_id} endpoint."""
|
|
|
|
async def test_archive_project_success(self, client, user_token):
|
|
"""Test archiving a project."""
|
|
# Create project
|
|
create_response = await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Archive Test", "slug": "archive-test"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
project_id = create_response.json()["id"]
|
|
|
|
# Archive project
|
|
response = await client.delete(
|
|
f"/api/v1/projects/{project_id}",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
data = response.json()
|
|
assert data["success"] is True
|
|
assert "archived" in data["message"].lower()
|
|
|
|
# Verify project is archived
|
|
get_response = await client.get(
|
|
f"/api/v1/projects/{project_id}",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
assert get_response.json()["status"] == "archived"
|
|
|
|
async def test_archive_project_already_archived(self, client, user_token):
|
|
"""Test archiving an already archived project."""
|
|
# Create and archive project
|
|
create_response = await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Archive Test", "slug": "archive-test"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
project_id = create_response.json()["id"]
|
|
|
|
await client.delete(
|
|
f"/api/v1/projects/{project_id}",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
# Try to archive again
|
|
response = await client.delete(
|
|
f"/api/v1/projects/{project_id}",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
assert "already archived" in response.json()["message"].lower()
|
|
|
|
async def test_archive_project_not_found(self, client, user_token):
|
|
"""Test archiving a non-existent project."""
|
|
fake_id = str(uuid.uuid4())
|
|
response = await client.delete(
|
|
f"/api/v1/projects/{fake_id}",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_404_NOT_FOUND
|
|
|
|
async def test_archive_project_unauthorized(
|
|
self, client, user_token, superuser_token
|
|
):
|
|
"""Test that users cannot archive other users' projects."""
|
|
# Create project as superuser
|
|
create_response = await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Admin Project", "slug": "admin-project"},
|
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
|
)
|
|
project_id = create_response.json()["id"]
|
|
|
|
# Try to archive as regular user
|
|
response = await client.delete(
|
|
f"/api/v1/projects/{project_id}",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
class TestPauseProject:
|
|
"""Tests for POST /api/v1/projects/{project_id}/pause endpoint."""
|
|
|
|
async def test_pause_project_success(self, client, user_token):
|
|
"""Test pausing an active project."""
|
|
# Create project
|
|
create_response = await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Pause Test", "slug": "pause-test"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
project_id = create_response.json()["id"]
|
|
|
|
# Pause project
|
|
response = await client.post(
|
|
f"/api/v1/projects/{project_id}/pause",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
assert response.json()["status"] == "paused"
|
|
|
|
async def test_pause_already_paused_project(self, client, user_token):
|
|
"""Test that pausing an already paused project returns validation error."""
|
|
# Create and pause project
|
|
create_response = await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Pause Test", "slug": "pause-test"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
project_id = create_response.json()["id"]
|
|
|
|
await client.post(
|
|
f"/api/v1/projects/{project_id}/pause",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
# Try to pause again
|
|
response = await client.post(
|
|
f"/api/v1/projects/{project_id}/pause",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
|
assert "already paused" in response.json()["errors"][0]["message"].lower()
|
|
|
|
async def test_pause_archived_project(self, client, user_token):
|
|
"""Test that pausing an archived project returns validation error."""
|
|
# Create and archive project
|
|
create_response = await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Archive Test", "slug": "archive-test"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
project_id = create_response.json()["id"]
|
|
|
|
await client.delete(
|
|
f"/api/v1/projects/{project_id}",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
# Try to pause
|
|
response = await client.post(
|
|
f"/api/v1/projects/{project_id}/pause",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
|
assert "archived" in response.json()["errors"][0]["message"].lower()
|
|
|
|
async def test_pause_completed_project(self, client, user_token):
|
|
"""Test that pausing a completed project returns validation error."""
|
|
# Create project with completed status
|
|
create_response = await client.post(
|
|
"/api/v1/projects",
|
|
json={
|
|
"name": "Completed Test",
|
|
"slug": "completed-test",
|
|
"status": "completed",
|
|
},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
project_id = create_response.json()["id"]
|
|
|
|
# Try to pause
|
|
response = await client.post(
|
|
f"/api/v1/projects/{project_id}/pause",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
|
assert "completed" in response.json()["errors"][0]["message"].lower()
|
|
|
|
async def test_pause_project_not_found(self, client, user_token):
|
|
"""Test pausing a non-existent project."""
|
|
fake_id = str(uuid.uuid4())
|
|
response = await client.post(
|
|
f"/api/v1/projects/{fake_id}/pause",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_404_NOT_FOUND
|
|
|
|
async def test_pause_project_unauthorized(
|
|
self, client, user_token, superuser_token
|
|
):
|
|
"""Test that users cannot pause other users' projects."""
|
|
# Create project as superuser
|
|
create_response = await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Admin Project", "slug": "admin-project"},
|
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
|
)
|
|
project_id = create_response.json()["id"]
|
|
|
|
# Try to pause as regular user
|
|
response = await client.post(
|
|
f"/api/v1/projects/{project_id}/pause",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
class TestResumeProject:
|
|
"""Tests for POST /api/v1/projects/{project_id}/resume endpoint."""
|
|
|
|
async def test_resume_project_success(self, client, user_token):
|
|
"""Test resuming a paused project."""
|
|
# Create and pause project
|
|
create_response = await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Resume Test", "slug": "resume-test"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
project_id = create_response.json()["id"]
|
|
|
|
await client.post(
|
|
f"/api/v1/projects/{project_id}/pause",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
# Resume project
|
|
response = await client.post(
|
|
f"/api/v1/projects/{project_id}/resume",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_200_OK
|
|
assert response.json()["status"] == "active"
|
|
|
|
async def test_resume_already_active_project(self, client, user_token):
|
|
"""Test that resuming an active project returns validation error."""
|
|
# Create project (active by default)
|
|
create_response = await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Active Test", "slug": "active-test"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
project_id = create_response.json()["id"]
|
|
|
|
# Try to resume
|
|
response = await client.post(
|
|
f"/api/v1/projects/{project_id}/resume",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
|
assert "already active" in response.json()["errors"][0]["message"].lower()
|
|
|
|
async def test_resume_archived_project(self, client, user_token):
|
|
"""Test that resuming an archived project returns validation error."""
|
|
# Create and archive project
|
|
create_response = await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Archive Test", "slug": "archive-test"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
project_id = create_response.json()["id"]
|
|
|
|
await client.delete(
|
|
f"/api/v1/projects/{project_id}",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
# Try to resume
|
|
response = await client.post(
|
|
f"/api/v1/projects/{project_id}/resume",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
|
assert "archived" in response.json()["errors"][0]["message"].lower()
|
|
|
|
async def test_resume_completed_project(self, client, user_token):
|
|
"""Test that resuming a completed project returns validation error."""
|
|
# Create project with completed status
|
|
create_response = await client.post(
|
|
"/api/v1/projects",
|
|
json={
|
|
"name": "Completed Test",
|
|
"slug": "completed-test",
|
|
"status": "completed",
|
|
},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
project_id = create_response.json()["id"]
|
|
|
|
# Try to resume
|
|
response = await client.post(
|
|
f"/api/v1/projects/{project_id}/resume",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
|
|
assert "completed" in response.json()["errors"][0]["message"].lower()
|
|
|
|
async def test_resume_project_not_found(self, client, user_token):
|
|
"""Test resuming a non-existent project."""
|
|
fake_id = str(uuid.uuid4())
|
|
response = await client.post(
|
|
f"/api/v1/projects/{fake_id}/resume",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_404_NOT_FOUND
|
|
|
|
async def test_resume_project_unauthorized(
|
|
self, client, user_token, superuser_token
|
|
):
|
|
"""Test that users cannot resume other users' projects."""
|
|
# Create and pause project as superuser
|
|
create_response = await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "Admin Project", "slug": "admin-project"},
|
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
|
)
|
|
project_id = create_response.json()["id"]
|
|
|
|
await client.post(
|
|
f"/api/v1/projects/{project_id}/pause",
|
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
|
)
|
|
|
|
# Try to resume as regular user
|
|
response = await client.post(
|
|
f"/api/v1/projects/{project_id}/resume",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
|
|
assert response.status_code == status.HTTP_403_FORBIDDEN
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
class TestProjectWorkflow:
|
|
"""Integration tests for project lifecycle workflows."""
|
|
|
|
async def test_full_project_lifecycle(self, client, user_token):
|
|
"""Test complete project lifecycle: create -> pause -> resume -> archive."""
|
|
# Create
|
|
create_response = await client.post(
|
|
"/api/v1/projects",
|
|
json={
|
|
"name": "Lifecycle Test",
|
|
"slug": "lifecycle-test",
|
|
"description": "Testing full lifecycle",
|
|
},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
assert create_response.status_code == status.HTTP_201_CREATED
|
|
project_id = create_response.json()["id"]
|
|
|
|
# Pause
|
|
pause_response = await client.post(
|
|
f"/api/v1/projects/{project_id}/pause",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
assert pause_response.status_code == status.HTTP_200_OK
|
|
assert pause_response.json()["status"] == "paused"
|
|
|
|
# Resume
|
|
resume_response = await client.post(
|
|
f"/api/v1/projects/{project_id}/resume",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
assert resume_response.status_code == status.HTTP_200_OK
|
|
assert resume_response.json()["status"] == "active"
|
|
|
|
# Archive
|
|
archive_response = await client.delete(
|
|
f"/api/v1/projects/{project_id}",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
assert archive_response.status_code == status.HTTP_200_OK
|
|
|
|
# Verify final state
|
|
get_response = await client.get(
|
|
f"/api/v1/projects/{project_id}",
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
assert get_response.json()["status"] == "archived"
|
|
|
|
async def test_superuser_can_manage_any_project(
|
|
self, client, user_token, superuser_token
|
|
):
|
|
"""Test that superuser can perform all operations on any project."""
|
|
# Create project as regular user
|
|
create_response = await client.post(
|
|
"/api/v1/projects",
|
|
json={"name": "User Project", "slug": "user-project"},
|
|
headers={"Authorization": f"Bearer {user_token}"},
|
|
)
|
|
project_id = create_response.json()["id"]
|
|
|
|
# Superuser can read
|
|
get_response = await client.get(
|
|
f"/api/v1/projects/{project_id}",
|
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
|
)
|
|
assert get_response.status_code == status.HTTP_200_OK
|
|
|
|
# Superuser can update
|
|
update_response = await client.patch(
|
|
f"/api/v1/projects/{project_id}",
|
|
json={"description": "Updated by admin"},
|
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
|
)
|
|
assert update_response.status_code == status.HTTP_200_OK
|
|
|
|
# Superuser can pause
|
|
pause_response = await client.post(
|
|
f"/api/v1/projects/{project_id}/pause",
|
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
|
)
|
|
assert pause_response.status_code == status.HTTP_200_OK
|
|
|
|
# Superuser can resume
|
|
resume_response = await client.post(
|
|
f"/api/v1/projects/{project_id}/resume",
|
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
|
)
|
|
assert resume_response.status_code == status.HTTP_200_OK
|
|
|
|
# Superuser can archive
|
|
archive_response = await client.delete(
|
|
f"/api/v1/projects/{project_id}",
|
|
headers={"Authorization": f"Bearer {superuser_token}"},
|
|
)
|
|
assert archive_response.status_code == status.HTTP_200_OK
|