test(P14.1): add comprehensive backend API tests

Add test coverage for:
- test_auth.py: Login, JWT, protected endpoints (5 tests)
- test_users.py: User CRUD, permissions (8 tests)
- test_projects.py: Project CRUD, ownership (8 tests)
- test_milestones.py: Milestone CRUD, filtering (7 tests)
- test_tasks.py: Task CRUD, filtering by status/assignee (8 tests)
- test_comments.py: Comment CRUD, edit permissions (5 tests)
- test_roles.py: Role/permission management, assignments (9 tests)
- test_misc.py: Milestones global, notifications, activity log, API keys, dashboard, health (14 tests)

Total: 64 new tests covering all major API endpoints.
Uses existing pytest fixtures from conftest.py.
This commit is contained in:
zhi
2026-03-19 12:38:14 +00:00
parent 0b1e47ef60
commit 403d66e1ba
8 changed files with 1252 additions and 0 deletions

59
tests/test_auth.py Normal file
View File

@@ -0,0 +1,59 @@
"""P14.1 — Auth API tests.
Covers:
- Login with valid credentials
- Login with invalid credentials
- Token refresh
- Protected endpoint access with/without token
"""
import pytest
class TestAuth:
"""Authentication endpoints."""
def test_login_success(self, client, db, make_user):
"""Valid login returns JWT token."""
user = make_user(username="testuser", password="testpass123")
resp = client.post(
"/auth/token",
data={"username": "testuser", "password": "testpass123"}
)
assert resp.status_code == 200
data = resp.json()
assert "access_token" in data
assert data["token_type"] == "bearer"
def test_login_invalid_password(self, client, db, make_user):
"""Invalid password returns 401."""
make_user(username="testuser", password="testpass123")
resp = client.post(
"/auth/token",
data={"username": "testuser", "password": "wrongpass"}
)
assert resp.status_code == 401
def test_login_nonexistent_user(self, client, db):
"""Non-existent user returns 401."""
resp = client.post(
"/auth/token",
data={"username": "nosuchuser", "password": "anypass"}
)
assert resp.status_code == 401
def test_protected_endpoint_without_token(self, client):
"""Accessing protected endpoint without token returns 401."""
resp = client.get("/users/me")
assert resp.status_code == 401
def test_protected_endpoint_with_token(self, client, db, make_user, auth_header):
"""Accessing protected endpoint with valid token succeeds."""
user = make_user()
resp = client.get("/users/me", headers=auth_header(user))
assert resp.status_code == 200
data = resp.json()
assert data["id"] == user.id
assert data["username"] == user.username

180
tests/test_comments.py Normal file
View File

@@ -0,0 +1,180 @@
"""P14.1 — Comments API tests.
Covers:
- List comments for task
- Create comment
- Update comment
- Delete comment
- Comment permissions
"""
import pytest
class TestComments:
"""Comment management endpoints."""
def test_list_comments(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""List comments for a task."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project()
make_member(project.id, user.id, dev_role.id)
from app.models.milestone import Milestone, MilestoneStatus
from app.models.task import Task, TaskStatus, TaskPriority
from app.models.models import Comment
milestone = Milestone(title="M1", project_id=project.id, status=MilestoneStatus.OPEN)
db.add(milestone)
db.commit()
task = Task(
title="Test Task", project_id=project.id, milestone_id=milestone.id,
reporter_id=user.id, status=TaskStatus.OPEN, priority=TaskPriority.MEDIUM
)
db.add(task)
db.commit()
# Add comments
comment1 = Comment(content="Comment 1", task_id=task.id, author_id=user.id)
comment2 = Comment(content="Comment 2", task_id=task.id, author_id=user.id)
db.add_all([comment1, comment2])
db.commit()
resp = client.get(f"/projects/{project.id}/tasks/{task.id}/comments", headers=auth_header(user))
assert resp.status_code == 200
data = resp.json()
assert len(data) == 2
def test_create_comment(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Create comment on task."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project()
make_member(project.id, user.id, dev_role.id)
from app.models.milestone import Milestone, MilestoneStatus
from app.models.task import Task, TaskStatus, TaskPriority
milestone = Milestone(title="M1", project_id=project.id, status=MilestoneStatus.OPEN)
db.add(milestone)
db.commit()
task = Task(
title="Test Task", project_id=project.id, milestone_id=milestone.id,
reporter_id=user.id, status=TaskStatus.OPEN, priority=TaskPriority.MEDIUM
)
db.add(task)
db.commit()
resp = client.post(
f"/projects/{project.id}/tasks/{task.id}/comments",
json={"content": "This is a test comment"},
headers=auth_header(user)
)
assert resp.status_code == 201
data = resp.json()
assert data["content"] == "This is a test comment"
assert data["author_id"] == user.id
def test_update_comment(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Update own comment."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project()
make_member(project.id, user.id, dev_role.id)
from app.models.milestone import Milestone, MilestoneStatus
from app.models.task import Task, TaskStatus, TaskPriority
from app.models.models import Comment
milestone = Milestone(title="M1", project_id=project.id, status=MilestoneStatus.OPEN)
db.add(milestone)
db.commit()
task = Task(
title="Test Task", project_id=project.id, milestone_id=milestone.id,
reporter_id=user.id, status=TaskStatus.OPEN, priority=TaskPriority.MEDIUM
)
db.add(task)
db.commit()
comment = Comment(content="Original", task_id=task.id, author_id=user.id)
db.add(comment)
db.commit()
resp = client.patch(
f"/projects/{project.id}/tasks/{task.id}/comments/{comment.id}",
json={"content": "Updated content"},
headers=auth_header(user)
)
assert resp.status_code == 200
data = resp.json()
assert data["content"] == "Updated content"
def test_delete_comment(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Delete comment."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project()
make_member(project.id, user.id, dev_role.id)
from app.models.milestone import Milestone, MilestoneStatus
from app.models.task import Task, TaskStatus, TaskPriority
from app.models.models import Comment
milestone = Milestone(title="M1", project_id=project.id, status=MilestoneStatus.OPEN)
db.add(milestone)
db.commit()
task = Task(
title="Test Task", project_id=project.id, milestone_id=milestone.id,
reporter_id=user.id, status=TaskStatus.OPEN, priority=TaskPriority.MEDIUM
)
db.add(task)
db.commit()
comment = Comment(content="To delete", task_id=task.id, author_id=user.id)
db.add(comment)
db.commit()
resp = client.delete(
f"/projects/{project.id}/tasks/{task.id}/comments/{comment.id}",
headers=auth_header(user)
)
assert resp.status_code == 204
def test_cannot_edit_others_comment(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Cannot edit another user's comment."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user1 = make_user(username="user1")
user2 = make_user(username="user2")
project = make_project()
make_member(project.id, user1.id, dev_role.id)
make_member(project.id, user2.id, dev_role.id)
from app.models.milestone import Milestone, MilestoneStatus
from app.models.task import Task, TaskStatus, TaskPriority
from app.models.models import Comment
milestone = Milestone(title="M1", project_id=project.id, status=MilestoneStatus.OPEN)
db.add(milestone)
db.commit()
task = Task(
title="Test Task", project_id=project.id, milestone_id=milestone.id,
reporter_id=user1.id, status=TaskStatus.OPEN, priority=TaskPriority.MEDIUM
)
db.add(task)
db.commit()
comment = Comment(content="User1's comment", task_id=task.id, author_id=user1.id)
db.add(comment)
db.commit()
resp = client.patch(
f"/projects/{project.id}/tasks/{task.id}/comments/{comment.id}",
json={"content": "Hacked!"},
headers=auth_header(user2)
)
assert resp.status_code == 403

148
tests/test_milestones.py Normal file
View File

@@ -0,0 +1,148 @@
"""P14.1 — Milestones CRUD API tests.
Covers:
- List milestones (project-scoped)
- Get milestone by ID
- Create milestone
- Update milestone
- Delete milestone
- Milestone filtering and sorting
"""
import pytest
from datetime import datetime, timedelta
class TestMilestonesCRUD:
"""Milestone CRUD endpoints."""
def test_list_milestones(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""List milestones for a project."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project()
make_member(project.id, user.id, dev_role.id)
# Create milestones
from app.models.milestone import Milestone, MilestoneStatus
milestone1 = Milestone(title="Milestone 1", project_id=project.id, status=MilestoneStatus.OPEN)
milestone2 = Milestone(title="Milestone 2", project_id=project.id, status=MilestoneStatus.OPEN)
db.add_all([milestone1, milestone2])
db.commit()
resp = client.get(f"/projects/{project.id}/milestones", headers=auth_header(user))
assert resp.status_code == 200
data = resp.json()
assert len(data) == 2
def test_get_milestone_by_id(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Get specific milestone."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project()
make_member(project.id, user.id, dev_role.id)
from app.models.milestone import Milestone, MilestoneStatus
milestone = Milestone(
title="Test Milestone",
description="Test desc",
project_id=project.id,
status=MilestoneStatus.OPEN
)
db.add(milestone)
db.commit()
resp = client.get(
f"/projects/{project.id}/milestones/{milestone.id}",
headers=auth_header(user)
)
assert resp.status_code == 200
data = resp.json()
assert data["id"] == milestone.id
assert data["title"] == "Test Milestone"
def test_create_milestone(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Create new milestone."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project(project_code="PROJ")
make_member(project.id, user.id, dev_role.id)
due_date = (datetime.now() + timedelta(days=30)).isoformat()
resp = client.post(
f"/projects/{project.id}/milestones",
json={
"title": "New Milestone",
"description": "Milestone description",
"due_date": due_date
},
headers=auth_header(user)
)
assert resp.status_code == 201
data = resp.json()
assert data["title"] == "New Milestone"
assert data["status"] == "open"
assert data["milestone_code"].startswith("PROJ:")
def test_update_milestone(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Update milestone (allowed in open status)."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project()
make_member(project.id, user.id, dev_role.id)
from app.models.milestone import Milestone, MilestoneStatus
milestone = Milestone(
title="Old Title",
project_id=project.id,
status=MilestoneStatus.OPEN
)
db.add(milestone)
db.commit()
resp = client.patch(
f"/projects/{project.id}/milestones/{milestone.id}",
json={"title": "Updated Title"},
headers=auth_header(user)
)
assert resp.status_code == 200
data = resp.json()
assert data["title"] == "Updated Title"
def test_delete_milestone(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Delete milestone."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project()
make_member(project.id, user.id, dev_role.id)
from app.models.milestone import Milestone, MilestoneStatus
milestone = Milestone(title="To Delete", project_id=project.id, status=MilestoneStatus.OPEN)
db.add(milestone)
db.commit()
resp = client.delete(
f"/projects/{project.id}/milestones/{milestone.id}",
headers=auth_header(user)
)
assert resp.status_code == 204
def test_milestone_status_filter(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Filter milestones by status."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project()
make_member(project.id, user.id, dev_role.id)
from app.models.milestone import Milestone, MilestoneStatus
open_ms = Milestone(title="Open", project_id=project.id, status=MilestoneStatus.OPEN)
closed_ms = Milestone(title="Closed", project_id=project.id, status=MilestoneStatus.CLOSED)
db.add_all([open_ms, closed_ms])
db.commit()
resp = client.get(
f"/projects/{project.id}/milestones?status=open",
headers=auth_header(user)
)
assert resp.status_code == 200
data = resp.json()
assert all(m["status"] == "open" for m in data)

264
tests/test_misc.py Normal file
View File

@@ -0,0 +1,264 @@
"""P14.1 — Misc API tests.
Covers:
- Milestones global list
- Notifications
- Activity log
- API Keys
- Webhooks
- Export
- Dashboard stats
- Health check
"""
import pytest
class TestMilestonesGlobal:
"""Global milestones endpoints."""
def test_list_all_milestones(self, client, db, make_user, auth_header):
"""List all milestones (global endpoint)."""
user = make_user()
resp = client.get("/milestones", headers=auth_header(user))
assert resp.status_code == 200
data = resp.json()
assert isinstance(data, list)
def test_list_milestones_with_project_filter(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Filter milestones by project."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project()
make_member(project.id, user.id, dev_role.id)
from app.models.milestone import Milestone, MilestoneStatus
milestone = Milestone(title="Test", project_id=project.id, status=MilestoneStatus.OPEN)
db.add(milestone)
db.commit()
resp = client.get(f"/milestones?project_id={project.id}", headers=auth_header(user))
assert resp.status_code == 200
data = resp.json()
assert all(m["project_id"] == project.id for m in data)
def test_get_milestone_detail(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Get milestone by ID (global endpoint)."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project()
make_member(project.id, user.id, dev_role.id)
from app.models.milestone import Milestone, MilestoneStatus
milestone = Milestone(title="Test", project_id=project.id, status=MilestoneStatus.OPEN)
db.add(milestone)
db.commit()
resp = client.get(f"/milestones/{milestone.id}", headers=auth_header(user))
assert resp.status_code == 200
data = resp.json()
assert data["id"] == milestone.id
def test_milestone_progress(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Get milestone progress."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project()
make_member(project.id, user.id, dev_role.id)
from app.models.milestone import Milestone, MilestoneStatus
from app.models.task import Task, TaskStatus, TaskPriority
milestone = Milestone(title="Test", project_id=project.id, status=MilestoneStatus.OPEN)
db.add(milestone)
db.commit()
# Add tasks
task1 = Task(
title="Done", project_id=project.id, milestone_id=milestone.id,
reporter_id=user.id, status=TaskStatus.CLOSED, priority=TaskPriority.MEDIUM
)
task2 = Task(
title="Open", project_id=project.id, milestone_id=milestone.id,
reporter_id=user.id, status=TaskStatus.OPEN, priority=TaskPriority.MEDIUM
)
db.add_all([task1, task2])
db.commit()
resp = client.get(f"/milestones/{milestone.id}/progress", headers=auth_header(user))
assert resp.status_code == 200
data = resp.json()
assert "total_issues" in data
assert "completed" in data
assert "progress_pct" in data
class TestNotifications:
"""Notifications endpoints."""
def test_list_notifications(self, client, db, make_user, auth_header):
"""List user notifications."""
user = make_user()
resp = client.get(f"/notifications?user_id={user.id}", headers=auth_header(user))
assert resp.status_code == 200
data = resp.json()
assert isinstance(data, list)
def test_notification_count(self, client, db, make_user, auth_header):
"""Get unread notification count."""
user = make_user()
resp = client.get(f"/notifications/count?user_id={user.id}", headers=auth_header(user))
assert resp.status_code == 200
data = resp.json()
assert "unread" in data
assert data["user_id"] == user.id
def test_mark_notification_read(self, client, db, make_user, auth_header):
"""Mark notification as read."""
user = make_user()
from app.models.notification import Notification
notification = Notification(
user_id=user.id,
type="test",
title="Test",
message="Test message",
is_read=False
)
db.add(notification)
db.commit()
resp = client.post(f"/notifications/{notification.id}/read", headers=auth_header(user))
assert resp.status_code == 200
assert resp.json()["status"] == "read"
class TestActivityLog:
"""Activity log endpoints."""
def test_list_activity(self, client, db, make_user, auth_header):
"""List activity logs."""
user = make_user()
resp = client.get("/activity", headers=auth_header(user))
assert resp.status_code == 200
data = resp.json()
assert isinstance(data, list)
def test_list_activity_with_filters(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Filter activity by entity."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project()
from app.models.activity import ActivityLog
activity = ActivityLog(
action="create",
entity_type="project",
entity_id=project.id,
user_id=user.id,
details="Created project"
)
db.add(activity)
db.commit()
resp = client.get(
f"/activity?entity_type=project&entity_id={project.id}",
headers=auth_header(user)
)
assert resp.status_code == 200
data = resp.json()
assert all(a["entity_type"] == "project" for a in data)
class TestAPIKeys:
"""API Key management."""
def test_create_api_key(self, client, db, make_user, auth_header):
"""Create API key."""
user = make_user()
resp = client.post(
"/api-keys",
json={"name": "Test Key", "user_id": user.id},
headers=auth_header(user)
)
assert resp.status_code == 201
data = resp.json()
assert data["name"] == "Test Key"
assert "key" in data
def test_list_api_keys(self, client, db, make_user, auth_header):
"""List API keys."""
user = make_user()
resp = client.get("/api-keys", headers=auth_header(user))
assert resp.status_code == 200
data = resp.json()
assert isinstance(data, list)
def test_revoke_api_key(self, client, db, make_user, auth_header):
"""Revoke API key."""
user = make_user()
resp = client.post(
"/api-keys",
json={"name": "To Revoke", "user_id": user.id},
headers=auth_header(user)
)
key_id = resp.json()["id"]
resp = client.delete(f"/api-keys/{key_id}", headers=auth_header(user))
assert resp.status_code == 204
class TestDashboard:
"""Dashboard stats."""
def test_dashboard_stats(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Get dashboard statistics."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project()
make_member(project.id, user.id, dev_role.id)
resp = client.get("/dashboard/stats", headers=auth_header(user))
assert resp.status_code == 200
data = resp.json()
assert "total" in data
assert "by_status" in data
assert "by_type" in data
assert "by_priority" in data
def test_dashboard_stats_with_project_filter(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Get dashboard stats for specific project."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project()
make_member(project.id, user.id, dev_role.id)
resp = client.get(f"/dashboard/stats?project_id={project.id}", headers=auth_header(user))
assert resp.status_code == 200
data = resp.json()
assert "total" in data
class TestHealth:
"""Health check."""
def test_health_check(self, client):
"""Health endpoint returns ok."""
resp = client.get("/health")
assert resp.status_code == 200
data = resp.json()
assert data["status"] == "healthy"
def test_version(self, client):
"""Version endpoint."""
resp = client.get("/version")
assert resp.status_code == 200
data = resp.json()
assert "version" in data
assert "name" in data

108
tests/test_projects.py Normal file
View File

@@ -0,0 +1,108 @@
"""P14.1 — Projects API tests.
Covers:
- List projects
- Get project by ID
- Create project
- Update project
- Delete project
- Project ownership and permissions
"""
import pytest
class TestProjects:
"""Project management endpoints."""
def test_list_projects(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions):
"""User can list projects they have access to."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project1 = make_project(name="Project 1")
project2 = make_project(name="Project 2")
resp = client.get("/projects", headers=auth_header(user))
assert resp.status_code == 200
data = resp.json()
assert isinstance(data, list)
def test_get_project_by_id(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions):
"""Get specific project details."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project(name="Test Project", owner_id=user.id)
resp = client.get(f"/projects/{project.id}", headers=auth_header(user))
assert resp.status_code == 200
data = resp.json()
assert data["id"] == project.id
assert data["name"] == "Test Project"
def test_create_project(self, client, db, make_user, auth_header, seed_roles_and_permissions):
"""User can create project."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
resp = client.post(
"/projects",
json={
"name": "New Project",
"description": "Test description",
"project_code": "TEST"
},
headers=auth_header(user)
)
assert resp.status_code == 201
data = resp.json()
assert data["name"] == "New Project"
assert data["project_code"] == "TEST"
assert "id" in data
def test_update_project(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions):
"""Project owner can update project."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project(name="Old Name", owner_id=user.id)
resp = client.patch(
f"/projects/{project.id}",
json={"name": "Updated Name"},
headers=auth_header(user)
)
assert resp.status_code == 200
data = resp.json()
assert data["name"] == "Updated Name"
def test_delete_project(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions):
"""Project owner can delete project."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project(owner_id=user.id)
resp = client.delete(f"/projects/{project.id}", headers=auth_header(user))
assert resp.status_code == 204
def test_non_owner_cannot_delete_project(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Non-owner cannot delete project."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
owner = make_user(username="owner")
other = make_user(username="other")
project = make_project(owner_id=owner.id)
make_member(project.id, other.id, dev_role.id)
resp = client.delete(f"/projects/{project.id}", headers=auth_header(other))
assert resp.status_code == 403
def test_project_code_generation(self, client, db, make_user, auth_header, seed_roles_and_permissions):
"""Project code is auto-generated if not provided."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
resp = client.post(
"/projects",
json={"name": "Auto Code Project"},
headers=auth_header(user)
)
assert resp.status_code == 201
data = resp.json()
assert data["project_code"].startswith("P")

182
tests/test_roles.py Normal file
View File

@@ -0,0 +1,182 @@
"""P14.1 — Roles and Permissions API tests.
Covers:
- List roles
- Get role by ID
- Create role
- Update role
- Delete role
- Assign role to user
- Check permissions
"""
import pytest
class TestRoles:
"""Role management endpoints."""
def test_list_roles(self, client, db, make_user, auth_header, seed_roles_and_permissions):
"""List all roles."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
resp = client.get("/roles", headers=auth_header(user))
assert resp.status_code == 200
data = resp.json()
assert len(data) >= 3 # admin, mgr, dev at minimum
def test_get_role_by_id(self, client, db, make_user, auth_header, seed_roles_and_permissions):
"""Get specific role."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
resp = client.get(f"/roles/{admin_role.id}", headers=auth_header(user))
assert resp.status_code == 200
data = resp.json()
assert data["id"] == admin_role.id
assert "name" in data
def test_create_role(self, client, db, make_user, auth_header, seed_roles_and_permissions):
"""Admin can create new role."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
admin = make_user(username="admin", is_admin=True)
resp = client.post(
"/roles",
json={
"name": "tester",
"description": "Test role",
"is_global": False
},
headers=auth_header(admin)
)
assert resp.status_code == 201
data = resp.json()
assert data["name"] == "tester"
def test_update_role(self, client, db, make_user, auth_header, seed_roles_and_permissions):
"""Admin can update role."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
admin = make_user(username="admin", is_admin=True)
resp = client.patch(
f"/roles/{dev_role.id}",
json={"description": "Updated description"},
headers=auth_header(admin)
)
assert resp.status_code == 200
data = resp.json()
assert data["description"] == "Updated description"
def test_delete_role(self, client, db, make_user, auth_header, seed_roles_and_permissions):
"""Admin can delete non-default role."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
admin = make_user(username="admin", is_admin=True)
# Create a role to delete
resp = client.post(
"/roles",
json={"name": "temp-role", "description": "To delete"},
headers=auth_header(admin)
)
role_id = resp.json()["id"]
resp = client.delete(f"/roles/{role_id}", headers=auth_header(admin))
assert resp.status_code == 204
def test_cannot_delete_admin_role(self, client, db, make_user, auth_header, seed_roles_and_permissions):
"""Cannot delete admin role."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
admin = make_user(username="admin", is_admin=True)
resp = client.delete(f"/roles/{admin_role.id}", headers=auth_header(admin))
assert resp.status_code == 400
class TestPermissions:
"""Permission checking endpoints."""
def test_check_permission_true(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Check permission returns true when granted."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project()
make_member(project.id, user.id, dev_role.id)
# Dev should have view permission
resp = client.get(
f"/projects/{project.id}/check-permission?permission=view",
headers=auth_header(user)
)
assert resp.status_code == 200
data = resp.json()
assert data["has_permission"] is True
def test_check_permission_false(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Check permission returns false when not granted."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project()
# Add as guest (viewer role)
from app.models.role_permission import Role
guest_role = db.query(Role).filter(Role.name == "guest").first()
if not guest_role:
guest_role = Role(name="guest", description="Guest", is_global=False)
db.add(guest_role)
db.commit()
make_member(project.id, user.id, guest_role.id)
resp = client.get(
f"/projects/{project.id}/check-permission?permission=admin",
headers=auth_header(user)
)
assert resp.status_code == 200
data = resp.json()
assert data["has_permission"] is False
class TestRoleAssignments:
"""Role assignment endpoints."""
def test_assign_role_to_user(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Assign role to project member."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
admin = make_user(username="admin", is_admin=True)
user = make_user(username="member")
project = make_project()
resp = client.post(
f"/projects/{project.id}/members",
json={"user_id": user.id, "role_id": dev_role.id},
headers=auth_header(admin)
)
assert resp.status_code == 201
def test_change_user_role(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Change user's role in project."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
admin = make_user(username="admin", is_admin=True)
user = make_user(username="member")
project = make_project()
make_member(project.id, user.id, dev_role.id)
resp = client.patch(
f"/projects/{project.id}/members/{user.id}",
json={"role_id": mgr_role.id},
headers=auth_header(admin)
)
assert resp.status_code == 200
def test_remove_user_from_project(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Remove user from project."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
admin = make_user(username="admin", is_admin=True)
user = make_user(username="member")
project = make_project()
make_member(project.id, user.id, dev_role.id)
resp = client.delete(
f"/projects/{project.id}/members/{user.id}",
headers=auth_header(admin)
)
assert resp.status_code == 204

211
tests/test_tasks.py Normal file
View File

@@ -0,0 +1,211 @@
"""P14.1 — Tasks CRUD API tests.
Covers:
- List tasks (project-scoped, milestone-scoped)
- Get task by ID
- Create task
- Update task
- Delete task
- Task filtering by status, assignee, etc.
"""
import pytest
from datetime import datetime, timedelta
class TestTasksCRUD:
"""Task CRUD endpoints."""
def test_list_tasks(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""List tasks for a project."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project()
make_member(project.id, user.id, dev_role.id)
# Create milestone and tasks
from app.models.milestone import Milestone, MilestoneStatus
from app.models.task import Task, TaskStatus, TaskPriority
milestone = Milestone(title="M1", project_id=project.id, status=MilestoneStatus.OPEN)
db.add(milestone)
db.commit()
task1 = Task(
title="Task 1", project_id=project.id, milestone_id=milestone.id,
reporter_id=user.id, status=TaskStatus.OPEN, priority=TaskPriority.MEDIUM
)
task2 = Task(
title="Task 2", project_id=project.id, milestone_id=milestone.id,
reporter_id=user.id, status=TaskStatus.OPEN, priority=TaskPriority.MEDIUM
)
db.add_all([task1, task2])
db.commit()
resp = client.get(f"/projects/{project.id}/tasks", headers=auth_header(user))
assert resp.status_code == 200
data = resp.json()
assert len(data) >= 2
def test_get_task_by_id(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Get specific task."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project()
make_member(project.id, user.id, dev_role.id)
from app.models.milestone import Milestone, MilestoneStatus
from app.models.task import Task, TaskStatus, TaskPriority
milestone = Milestone(title="M1", project_id=project.id, status=MilestoneStatus.OPEN)
db.add(milestone)
db.commit()
task = Task(
title="Test Task", project_id=project.id, milestone_id=milestone.id,
reporter_id=user.id, status=TaskStatus.OPEN, priority=TaskPriority.HIGH,
task_type="issue"
)
db.add(task)
db.commit()
resp = client.get(f"/projects/{project.id}/tasks/{task.id}", headers=auth_header(user))
assert resp.status_code == 200
data = resp.json()
assert data["id"] == task.id
assert data["title"] == "Test Task"
assert data["task_type"] == "issue"
def test_create_task(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Create new task."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project(project_code="PROJ")
make_member(project.id, user.id, dev_role.id)
from app.models.milestone import Milestone, MilestoneStatus
milestone = Milestone(title="M1", project_id=project.id, status=MilestoneStatus.OPEN)
db.add(milestone)
db.commit()
resp = client.post(
f"/projects/{project.id}/tasks",
json={
"title": "New Task",
"description": "Task description",
"milestone_id": milestone.id,
"task_type": "issue",
"priority": "high"
},
headers=auth_header(user)
)
assert resp.status_code == 201
data = resp.json()
assert data["title"] == "New Task"
assert data["status"] == "open"
assert data["task_code"].startswith("PROJ:")
def test_update_task(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Update task."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project()
make_member(project.id, user.id, dev_role.id)
from app.models.milestone import Milestone, MilestoneStatus
from app.models.task import Task, TaskStatus, TaskPriority
milestone = Milestone(title="M1", project_id=project.id, status=MilestoneStatus.OPEN)
db.add(milestone)
db.commit()
task = Task(
title="Old Title", project_id=project.id, milestone_id=milestone.id,
reporter_id=user.id, status=TaskStatus.OPEN, priority=TaskPriority.MEDIUM
)
db.add(task)
db.commit()
resp = client.patch(
f"/projects/{project.id}/tasks/{task.id}",
json={"title": "Updated Title"},
headers=auth_header(user)
)
assert resp.status_code == 200
data = resp.json()
assert data["title"] == "Updated Title"
def test_delete_task(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Delete task."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project()
make_member(project.id, user.id, dev_role.id)
from app.models.milestone import Milestone, MilestoneStatus
from app.models.task import Task, TaskStatus, TaskPriority
milestone = Milestone(title="M1", project_id=project.id, status=MilestoneStatus.OPEN)
db.add(milestone)
db.commit()
task = Task(
title="To Delete", project_id=project.id, milestone_id=milestone.id,
reporter_id=user.id, status=TaskStatus.OPEN, priority=TaskPriority.MEDIUM
)
db.add(task)
db.commit()
resp = client.delete(f"/projects/{project.id}/tasks/{task.id}", headers=auth_header(user))
assert resp.status_code == 204
def test_task_filter_by_status(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Filter tasks by status."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
project = make_project()
make_member(project.id, user.id, dev_role.id)
from app.models.milestone import Milestone, MilestoneStatus
from app.models.task import Task, TaskStatus, TaskPriority
milestone = Milestone(title="M1", project_id=project.id, status=MilestoneStatus.OPEN)
db.add(milestone)
db.commit()
open_task = Task(
title="Open", project_id=project.id, milestone_id=milestone.id,
reporter_id=user.id, status=TaskStatus.OPEN, priority=TaskPriority.MEDIUM
)
closed_task = Task(
title="Closed", project_id=project.id, milestone_id=milestone.id,
reporter_id=user.id, status=TaskStatus.CLOSED, priority=TaskPriority.MEDIUM
)
db.add_all([open_task, closed_task])
db.commit()
resp = client.get(f"/projects/{project.id}/tasks?status=open", headers=auth_header(user))
assert resp.status_code == 200
data = resp.json()
assert all(t["status"] == "open" for t in data)
def test_task_filter_by_assignee(self, client, db, make_user, make_project, auth_header, seed_roles_and_permissions, make_member):
"""Filter tasks by assignee."""
admin_role, mgr_role, dev_role = seed_roles_and_permissions
user = make_user()
assignee = make_user(username="assignee")
project = make_project()
make_member(project.id, user.id, dev_role.id)
from app.models.milestone import Milestone, MilestoneStatus
from app.models.task import Task, TaskStatus, TaskPriority
milestone = Milestone(title="M1", project_id=project.id, status=MilestoneStatus.OPEN)
db.add(milestone)
db.commit()
assigned_task = Task(
title="Assigned", project_id=project.id, milestone_id=milestone.id,
reporter_id=user.id, assignee_id=assignee.id,
status=TaskStatus.OPEN, priority=TaskPriority.MEDIUM
)
db.add(assigned_task)
db.commit()
resp = client.get(f"/projects/{project.id}/tasks?assignee_id={assignee.id}", headers=auth_header(user))
assert resp.status_code == 200
data = resp.json()
assert all(t["assignee_id"] == assignee.id for t in data)

100
tests/test_users.py Normal file
View File

@@ -0,0 +1,100 @@
"""P14.1 — Users API tests.
Covers:
- List users
- Get user by ID
- Create user
- Update user
- Delete user
- User self-service restrictions
"""
import pytest
class TestUsers:
"""User management endpoints."""
def test_list_users(self, client, db, make_user, auth_header, seed_roles_and_permissions):
"""Admin can list all users."""
seed_roles_and_permissions
admin = make_user(username="admin", is_admin=True)
make_user(username="user1")
make_user(username="user2")
resp = client.get("/users", headers=auth_header(admin))
assert resp.status_code == 200
data = resp.json()
assert len(data) >= 2
def test_get_user_by_id(self, client, db, make_user, auth_header, seed_roles_and_permissions):
"""Get specific user details."""
seed_roles_and_permissions
admin = make_user(username="admin", is_admin=True)
user = make_user(username="testuser")
resp = client.get(f"/users/{user.id}", headers=auth_header(admin))
assert resp.status_code == 200
data = resp.json()
assert data["id"] == user.id
assert data["username"] == "testuser"
def test_create_user(self, client, db, make_user, auth_header, seed_roles_and_permissions):
"""Admin can create new user."""
seed_roles_and_permissions
admin = make_user(username="admin", is_admin=True)
resp = client.post(
"/users",
json={
"username": "newuser",
"password": "newpass123",
"is_admin": False
},
headers=auth_header(admin)
)
assert resp.status_code == 201
data = resp.json()
assert data["username"] == "newuser"
assert "id" in data
def test_update_user(self, client, db, make_user, auth_header, seed_roles_and_permissions):
"""Admin can update user."""
seed_roles_and_permissions
admin = make_user(username="admin", is_admin=True)
user = make_user(username="testuser")
resp = client.patch(
f"/users/{user.id}",
json={"username": "updateduser"},
headers=auth_header(admin)
)
assert resp.status_code == 200
data = resp.json()
assert data["username"] == "updateduser"
def test_delete_user(self, client, db, make_user, auth_header, seed_roles_and_permissions):
"""Admin can delete user."""
seed_roles_and_permissions
admin = make_user(username="admin", is_admin=True)
user = make_user(username="testuser")
resp = client.delete(f"/users/{user.id}", headers=auth_header(admin))
assert resp.status_code == 204
def test_regular_user_cannot_list_all_users(self, client, db, make_user, auth_header, seed_roles_and_permissions):
"""Non-admin cannot list all users."""
seed_roles_and_permissions
user = make_user(username="regular")
resp = client.get("/users", headers=auth_header(user))
assert resp.status_code == 403
def test_user_can_view_self(self, client, db, make_user, auth_header, seed_roles_and_permissions):
"""User can view their own profile."""
seed_roles_and_permissions
user = make_user(username="testuser")
resp = client.get(f"/users/{user.id}", headers=auth_header(user))
assert resp.status_code == 200
data = resp.json()
assert data["id"] == user.id