Files
PROJECT-CONTORL/backend/tests/test_collaboration.py
beabigegg 3470428411 feat: implement collaboration module
- Backend (FastAPI):
  - Task comments with nested replies and soft delete
  - @mention parsing with 10-mention limit per comment
  - Notification system with read/unread tracking
  - Blocker management with project owner notification
  - WebSocket endpoint with JWT auth and keepalive
  - User search API for @mention autocomplete
  - Alembic migration for 4 new tables

- Frontend (React + Vite):
  - Comments component with @mention autocomplete
  - NotificationBell with real-time WebSocket updates
  - BlockerDialog for task blocking workflow
  - NotificationContext for state management

- OpenSpec:
  - 4 requirements with scenarios defined
  - add-collaboration change archived

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-29 20:45:07 +08:00

421 lines
13 KiB
Python

import pytest
import uuid
from app.models import User, Space, Project, Task, TaskStatus, Comment, Notification, Blocker
@pytest.fixture
def test_user(db):
"""Create a test user."""
user = User(
id=str(uuid.uuid4()),
email="testuser@example.com",
name="Test User",
role_id="00000000-0000-0000-0000-000000000003",
is_active=True,
is_system_admin=False,
)
db.add(user)
db.commit()
return user
@pytest.fixture
def user_token(client, mock_redis, test_user):
"""Get a user token for testing."""
from app.core.security import create_access_token, create_token_payload
token_data = create_token_payload(
user_id=test_user.id,
email=test_user.email,
role="engineer",
department_id=None,
is_system_admin=False,
)
token = create_access_token(token_data)
mock_redis.setex(f"session:{test_user.id}", 900, token)
return token
@pytest.fixture
def test_space(db):
"""Create a test space."""
space = Space(
id=str(uuid.uuid4()),
name="Test Space",
description="A test space",
owner_id="00000000-0000-0000-0000-000000000001",
)
db.add(space)
db.commit()
return space
@pytest.fixture
def test_project(db, test_space):
"""Create a test project."""
project = Project(
id=str(uuid.uuid4()),
space_id=test_space.id,
title="Test Project",
description="A test project",
owner_id="00000000-0000-0000-0000-000000000001",
security_level="public",
)
db.add(project)
db.commit()
return project
@pytest.fixture
def test_status(db, test_project):
"""Create a test task status."""
status = TaskStatus(
id=str(uuid.uuid4()),
project_id=test_project.id,
name="To Do",
color="#3498db",
position=0,
)
db.add(status)
db.commit()
return status
@pytest.fixture
def test_task(db, test_project, test_status):
"""Create a test task."""
task = Task(
id=str(uuid.uuid4()),
project_id=test_project.id,
title="Test Task",
description="A test task",
status_id=test_status.id,
created_by="00000000-0000-0000-0000-000000000001",
)
db.add(task)
db.commit()
return task
class TestComments:
"""Tests for Comments API."""
def test_create_comment(self, client, admin_token, test_task):
"""Test creating a comment."""
response = client.post(
f"/api/tasks/{test_task.id}/comments",
headers={"Authorization": f"Bearer {admin_token}"},
json={"content": "This is a test comment"},
)
assert response.status_code == 201
data = response.json()
assert data["content"] == "This is a test comment"
assert data["task_id"] == test_task.id
assert data["is_edited"] is False
assert data["is_deleted"] is False
def test_list_comments(self, client, admin_token, db, test_task):
"""Test listing comments."""
# Create a comment first
comment = Comment(
id=str(uuid.uuid4()),
task_id=test_task.id,
author_id="00000000-0000-0000-0000-000000000001",
content="Test comment",
)
db.add(comment)
db.commit()
response = client.get(
f"/api/tasks/{test_task.id}/comments",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
data = response.json()
assert data["total"] == 1
assert len(data["comments"]) == 1
assert data["comments"][0]["content"] == "Test comment"
def test_update_comment(self, client, admin_token, db, test_task):
"""Test updating a comment."""
comment = Comment(
id=str(uuid.uuid4()),
task_id=test_task.id,
author_id="00000000-0000-0000-0000-000000000001",
content="Original content",
)
db.add(comment)
db.commit()
response = client.put(
f"/api/comments/{comment.id}",
headers={"Authorization": f"Bearer {admin_token}"},
json={"content": "Updated content"},
)
assert response.status_code == 200
data = response.json()
assert data["content"] == "Updated content"
assert data["is_edited"] is True
def test_delete_comment(self, client, admin_token, db, test_task):
"""Test deleting a comment (soft delete)."""
comment = Comment(
id=str(uuid.uuid4()),
task_id=test_task.id,
author_id="00000000-0000-0000-0000-000000000001",
content="To be deleted",
)
db.add(comment)
db.commit()
response = client.delete(
f"/api/comments/{comment.id}",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 204
# Verify soft delete
db.refresh(comment)
assert comment.is_deleted is True
def test_mention_limit(self, client, admin_token, test_task):
"""Test that @mention limit is enforced."""
# Create content with more than 10 mentions
mentions = " ".join([f"@user{i}" for i in range(15)])
response = client.post(
f"/api/tasks/{test_task.id}/comments",
headers={"Authorization": f"Bearer {admin_token}"},
json={"content": f"Test with many mentions: {mentions}"},
)
assert response.status_code == 400
assert "10 mentions" in response.json()["detail"]
class TestNotifications:
"""Tests for Notifications API."""
def test_list_notifications(self, client, admin_token, db):
"""Test listing notifications."""
# Create a notification
notification = Notification(
id=str(uuid.uuid4()),
user_id="00000000-0000-0000-0000-000000000001",
type="mention",
reference_type="comment",
reference_id=str(uuid.uuid4()),
title="Test notification",
message="Test message",
)
db.add(notification)
db.commit()
response = client.get(
"/api/notifications",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
data = response.json()
assert data["total"] >= 1
assert data["unread_count"] >= 1
def test_mark_notification_as_read(self, client, admin_token, db):
"""Test marking a notification as read."""
notification = Notification(
id=str(uuid.uuid4()),
user_id="00000000-0000-0000-0000-000000000001",
type="assignment",
reference_type="task",
reference_id=str(uuid.uuid4()),
title="New assignment",
)
db.add(notification)
db.commit()
response = client.put(
f"/api/notifications/{notification.id}/read",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
data = response.json()
assert data["is_read"] is True
assert data["read_at"] is not None
def test_mark_all_as_read(self, client, admin_token, db):
"""Test marking all notifications as read."""
# Create multiple unread notifications
for i in range(3):
notification = Notification(
id=str(uuid.uuid4()),
user_id="00000000-0000-0000-0000-000000000001",
type="comment",
reference_type="task",
reference_id=str(uuid.uuid4()),
title=f"Notification {i}",
)
db.add(notification)
db.commit()
response = client.put(
"/api/notifications/read-all",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
data = response.json()
assert data["updated_count"] >= 3
def test_get_unread_count(self, client, admin_token, db):
"""Test getting unread notification count."""
# Create unread notifications
for i in range(2):
notification = Notification(
id=str(uuid.uuid4()),
user_id="00000000-0000-0000-0000-000000000001",
type="blocker",
reference_type="task",
reference_id=str(uuid.uuid4()),
title=f"Blocker {i}",
)
db.add(notification)
db.commit()
response = client.get(
"/api/notifications/unread-count",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
data = response.json()
assert data["unread_count"] >= 2
class TestBlockers:
"""Tests for Blockers API."""
def test_create_blocker(self, client, admin_token, test_task):
"""Test creating a blocker."""
response = client.post(
f"/api/tasks/{test_task.id}/blockers",
headers={"Authorization": f"Bearer {admin_token}"},
json={"reason": "Waiting for external dependency"},
)
assert response.status_code == 201
data = response.json()
assert data["reason"] == "Waiting for external dependency"
assert data["resolved_at"] is None
def test_resolve_blocker(self, client, admin_token, db, test_task):
"""Test resolving a blocker."""
blocker = Blocker(
id=str(uuid.uuid4()),
task_id=test_task.id,
reported_by="00000000-0000-0000-0000-000000000001",
reason="Test blocker",
)
db.add(blocker)
test_task.blocker_flag = True
db.commit()
response = client.put(
f"/api/blockers/{blocker.id}/resolve",
headers={"Authorization": f"Bearer {admin_token}"},
json={"resolution_note": "Issue resolved by updating config"},
)
assert response.status_code == 200
data = response.json()
assert data["resolved_at"] is not None
assert data["resolution_note"] == "Issue resolved by updating config"
# Verify task blocker_flag is cleared
db.refresh(test_task)
assert test_task.blocker_flag is False
def test_list_blockers(self, client, admin_token, db, test_task):
"""Test listing blockers for a task."""
blocker = Blocker(
id=str(uuid.uuid4()),
task_id=test_task.id,
reported_by="00000000-0000-0000-0000-000000000001",
reason="Test blocker",
)
db.add(blocker)
db.commit()
response = client.get(
f"/api/tasks/{test_task.id}/blockers",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
data = response.json()
assert data["total"] == 1
assert data["blockers"][0]["reason"] == "Test blocker"
def test_cannot_create_duplicate_active_blocker(self, client, admin_token, db, test_task):
"""Test that duplicate active blockers are prevented."""
# Create first blocker
blocker = Blocker(
id=str(uuid.uuid4()),
task_id=test_task.id,
reported_by="00000000-0000-0000-0000-000000000001",
reason="First blocker",
)
db.add(blocker)
db.commit()
# Try to create second blocker
response = client.post(
f"/api/tasks/{test_task.id}/blockers",
headers={"Authorization": f"Bearer {admin_token}"},
json={"reason": "Second blocker"},
)
assert response.status_code == 400
assert "already has an unresolved blocker" in response.json()["detail"]
class TestUserSearch:
"""Tests for User Search API."""
def test_search_users(self, client, admin_token, db):
"""Test searching users by name."""
# Create test users
user1 = User(
id=str(uuid.uuid4()),
email="john@example.com",
name="John Doe",
is_active=True,
)
user2 = User(
id=str(uuid.uuid4()),
email="jane@example.com",
name="Jane Doe",
is_active=True,
)
db.add_all([user1, user2])
db.commit()
response = client.get(
"/api/users/search?q=Doe",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
data = response.json()
assert len(data) >= 2
def test_search_users_by_email(self, client, admin_token, db):
"""Test searching users by email."""
user = User(
id=str(uuid.uuid4()),
email="searchtest@example.com",
name="Search Test",
is_active=True,
)
db.add(user)
db.commit()
response = client.get(
"/api/users/search?q=searchtest",
headers={"Authorization": f"Bearer {admin_token}"},
)
assert response.status_code == 200
data = response.json()
assert len(data) >= 1
assert any(u["email"] == "searchtest@example.com" for u in data)