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:
59
tests/test_auth.py
Normal file
59
tests/test_auth.py
Normal 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
180
tests/test_comments.py
Normal 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
148
tests/test_milestones.py
Normal 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
264
tests/test_misc.py
Normal 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
108
tests/test_projects.py
Normal 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
182
tests/test_roles.py
Normal 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
211
tests/test_tasks.py
Normal 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
100
tests/test_users.py
Normal 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
|
||||
Reference in New Issue
Block a user