Files
PROJECT-CONTORL/backend/tests/test_reports.py
beabigegg 64874d5425 feat: enhance weekly report and realtime notifications
Weekly Report (fix-weekly-report):
- Remove 5-task limit, show all tasks per category
- Add blocked tasks with blocker_reason and blocked_since
- Add next week tasks (due in coming week)
- Add assignee_name, completed_at, days_overdue to task details
- Frontend collapsible sections for each task category
- 8 new tests for enhanced report content

Realtime Notifications (fix-realtime-notifications):
- SQLAlchemy event-based notification publishing
- Redis Pub/Sub for multi-process broadcast
- Fix soft rollback handler stacking issue
- Fix ping scheduling drift (send immediately when interval expires)
- Frontend NotificationContext with WebSocket reconnection

Spec Fixes:
- Add missing ## Purpose sections to 5 specs

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

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

444 lines
15 KiB
Python

import pytest
import uuid
from datetime import datetime, timedelta
from app.models import User, Space, Project, Task, TaskStatus, ScheduledReport, ReportHistory, Blocker
from app.services.report_service import ReportService
@pytest.fixture
def test_user(db):
"""Create a test user."""
user = User(
id=str(uuid.uuid4()),
email="reportuser@example.com",
name="Report 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 test_user_token(client, mock_redis, test_user):
"""Get a token for test user."""
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, test_user):
"""Create a test space."""
space = Space(
id=str(uuid.uuid4()),
name="Report Test Space",
description="Test space for reports",
owner_id=test_user.id,
)
db.add(space)
db.commit()
return space
@pytest.fixture
def test_project(db, test_space, test_user):
"""Create a test project."""
project = Project(
id=str(uuid.uuid4()),
space_id=test_space.id,
title="Report Test Project",
description="Test project for reports",
owner_id=test_user.id,
)
db.add(project)
db.commit()
return project
@pytest.fixture
def test_statuses(db, test_project):
"""Create test task statuses."""
todo = TaskStatus(
id=str(uuid.uuid4()),
project_id=test_project.id,
name="To Do",
color="#808080",
position=0,
)
in_progress = TaskStatus(
id=str(uuid.uuid4()),
project_id=test_project.id,
name="In Progress",
color="#0000FF",
position=1,
)
done = TaskStatus(
id=str(uuid.uuid4()),
project_id=test_project.id,
name="Done",
color="#00FF00",
position=2,
)
db.add_all([todo, in_progress, done])
db.commit()
return {"todo": todo, "in_progress": in_progress, "done": done}
@pytest.fixture
def test_tasks(db, test_project, test_user, test_statuses):
"""Create test tasks with various statuses."""
tasks = []
# Completed task (updated this week)
completed_task = Task(
id=str(uuid.uuid4()),
project_id=test_project.id,
title="Completed Task",
status_id=test_statuses["done"].id,
created_by=test_user.id,
)
completed_task.updated_at = datetime.utcnow()
tasks.append(completed_task)
# In progress task
in_progress_task = Task(
id=str(uuid.uuid4()),
project_id=test_project.id,
title="In Progress Task",
status_id=test_statuses["in_progress"].id,
created_by=test_user.id,
)
tasks.append(in_progress_task)
# Overdue task
overdue_task = Task(
id=str(uuid.uuid4()),
project_id=test_project.id,
title="Overdue Task",
status_id=test_statuses["todo"].id,
due_date=datetime.utcnow() - timedelta(days=3),
created_by=test_user.id,
)
tasks.append(overdue_task)
db.add_all(tasks)
db.commit()
return tasks
class TestReportService:
"""Tests for ReportService."""
def test_get_week_start(self):
"""Test week start calculation."""
# Test with a known Wednesday
wednesday = datetime(2024, 12, 25, 15, 30, 0) # Wednesday
week_start = ReportService.get_week_start(wednesday)
assert week_start.weekday() == 0 # Monday
assert week_start.hour == 0
assert week_start.minute == 0
def test_get_weekly_stats_empty(self, db, test_user):
"""Test weekly stats with no projects."""
stats = ReportService.get_weekly_stats(db, test_user.id)
assert stats["summary"]["completed_count"] == 0
assert stats["summary"]["in_progress_count"] == 0
assert stats["summary"]["total_tasks"] == 0
assert len(stats["projects"]) == 0
def test_get_weekly_stats_with_tasks(self, db, test_user, test_project, test_tasks, test_statuses):
"""Test weekly stats with tasks."""
stats = ReportService.get_weekly_stats(db, test_user.id)
assert stats["summary"]["completed_count"] == 1
assert stats["summary"]["in_progress_count"] == 1
assert stats["summary"]["overdue_count"] == 1
assert stats["summary"]["total_tasks"] == 3
assert len(stats["projects"]) == 1
assert stats["projects"][0]["project_title"] == "Report Test Project"
def test_generate_weekly_report(self, db, test_user, test_project, test_tasks, test_statuses):
"""Test generating a weekly report."""
report = ReportService.generate_weekly_report(db, test_user.id)
assert report is not None
assert report.status == "sent"
assert "summary" in report.content
# Check scheduled report was created
scheduled = db.query(ScheduledReport).filter(
ScheduledReport.recipient_id == test_user.id
).first()
assert scheduled is not None
assert scheduled.last_sent_at is not None
class TestReportAPI:
"""Tests for Report API endpoints."""
def test_preview_weekly_report(self, client, test_user_token, test_project, test_tasks, test_statuses):
"""Test previewing weekly report."""
response = client.get(
"/api/reports/weekly/preview",
headers={"Authorization": f"Bearer {test_user_token}"},
)
assert response.status_code == 200
data = response.json()
assert "summary" in data
assert "projects" in data
assert data["summary"]["total_tasks"] == 3
def test_generate_weekly_report_api(self, client, test_user_token, test_project, test_tasks, test_statuses):
"""Test generating weekly report via API."""
response = client.post(
"/api/reports/weekly/generate",
headers={"Authorization": f"Bearer {test_user_token}"},
)
assert response.status_code == 200
data = response.json()
assert data["message"] == "Weekly report generated successfully"
assert "report_id" in data
assert "summary" in data
def test_list_report_history_empty(self, client, test_user_token):
"""Test listing report history when empty."""
response = client.get(
"/api/reports/history",
headers={"Authorization": f"Bearer {test_user_token}"},
)
assert response.status_code == 200
data = response.json()
assert data["total"] == 0
assert len(data["reports"]) == 0
def test_list_report_history_with_reports(self, client, test_user_token, test_project, test_tasks, test_statuses, db, test_user):
"""Test listing report history with existing reports."""
# Generate a report first
ReportService.generate_weekly_report(db, test_user.id)
response = client.get(
"/api/reports/history",
headers={"Authorization": f"Bearer {test_user_token}"},
)
assert response.status_code == 200
data = response.json()
assert data["total"] >= 1
assert len(data["reports"]) >= 1
assert data["reports"][0]["status"] == "sent"
def test_get_report_detail(self, client, test_user_token, test_project, test_tasks, test_statuses, db, test_user):
"""Test getting specific report detail."""
# Generate a report first
report = ReportService.generate_weekly_report(db, test_user.id)
response = client.get(
f"/api/reports/history/{report.id}",
headers={"Authorization": f"Bearer {test_user_token}"},
)
assert response.status_code == 200
data = response.json()
assert data["id"] == report.id
assert "content" in data
class TestWeeklyReportContent:
"""Tests for enhanced weekly report content (blocked/next_week tasks)."""
def test_blocked_tasks_included(self, db, test_user, test_project, test_statuses):
"""Test that blocked tasks are included in weekly stats."""
# Create a task with blocker
blocked_task = Task(
id=str(uuid.uuid4()),
project_id=test_project.id,
title="Blocked Task",
status_id=test_statuses["in_progress"].id,
created_by=test_user.id,
assignee_id=test_user.id,
)
db.add(blocked_task)
db.flush()
# Create an unresolved blocker
blocker = Blocker(
id=str(uuid.uuid4()),
task_id=blocked_task.id,
reported_by=test_user.id,
reason="Waiting for external dependency",
)
db.add(blocker)
db.commit()
stats = ReportService.get_weekly_stats(db, test_user.id)
assert stats["summary"]["blocked_count"] == 1
assert len(stats["projects"]) == 1
assert stats["projects"][0]["blocked_count"] == 1
assert len(stats["projects"][0]["blocked_tasks"]) == 1
assert stats["projects"][0]["blocked_tasks"][0]["title"] == "Blocked Task"
assert stats["projects"][0]["blocked_tasks"][0]["blocker_reason"] == "Waiting for external dependency"
def test_resolved_blocker_not_included(self, db, test_user, test_project, test_statuses):
"""Test that resolved blockers are not counted."""
# Create a task with resolved blocker
task = Task(
id=str(uuid.uuid4()),
project_id=test_project.id,
title="Previously Blocked Task",
status_id=test_statuses["in_progress"].id,
created_by=test_user.id,
)
db.add(task)
db.flush()
# Create a resolved blocker
blocker = Blocker(
id=str(uuid.uuid4()),
task_id=task.id,
reported_by=test_user.id,
reason="Was blocked",
resolved_by=test_user.id,
resolved_at=datetime.utcnow(),
resolution_note="Fixed",
)
db.add(blocker)
db.commit()
stats = ReportService.get_weekly_stats(db, test_user.id)
assert stats["summary"]["blocked_count"] == 0
assert stats["projects"][0]["blocked_count"] == 0
def test_next_week_tasks_included(self, db, test_user, test_project, test_statuses):
"""Test that next week tasks are included in weekly stats."""
# Calculate next week dates
week_start = ReportService.get_week_start()
next_week_date = week_start + timedelta(days=10) # Next week
# Create a task due next week
next_week_task = Task(
id=str(uuid.uuid4()),
project_id=test_project.id,
title="Next Week Task",
status_id=test_statuses["todo"].id,
due_date=next_week_date,
created_by=test_user.id,
assignee_id=test_user.id,
)
db.add(next_week_task)
db.commit()
stats = ReportService.get_weekly_stats(db, test_user.id)
assert stats["summary"]["next_week_count"] == 1
assert len(stats["projects"][0]["next_week_tasks"]) == 1
assert stats["projects"][0]["next_week_tasks"][0]["title"] == "Next Week Task"
def test_completed_task_not_in_next_week(self, db, test_user, test_project, test_statuses):
"""Test that completed tasks are not included in next week list."""
week_start = ReportService.get_week_start()
next_week_date = week_start + timedelta(days=10)
# Create a completed task due next week
task = Task(
id=str(uuid.uuid4()),
project_id=test_project.id,
title="Done Next Week Task",
status_id=test_statuses["done"].id,
due_date=next_week_date,
created_by=test_user.id,
)
db.add(task)
db.commit()
stats = ReportService.get_weekly_stats(db, test_user.id)
assert stats["summary"]["next_week_count"] == 0
def test_task_details_include_assignee_name(self, db, test_user, test_project, test_statuses):
"""Test that task details include assignee name."""
task = Task(
id=str(uuid.uuid4()),
project_id=test_project.id,
title="Assigned Task",
status_id=test_statuses["in_progress"].id,
created_by=test_user.id,
assignee_id=test_user.id,
)
db.add(task)
db.commit()
stats = ReportService.get_weekly_stats(db, test_user.id)
assert len(stats["projects"][0]["in_progress_tasks"]) == 1
assert stats["projects"][0]["in_progress_tasks"][0]["assignee_name"] == "Report User"
def test_overdue_days_calculated(self, db, test_user, test_project, test_statuses):
"""Test that days_overdue is correctly calculated."""
# Create task overdue by 5 days
overdue_task = Task(
id=str(uuid.uuid4()),
project_id=test_project.id,
title="5 Days Overdue",
status_id=test_statuses["todo"].id,
due_date=datetime.utcnow() - timedelta(days=5),
created_by=test_user.id,
)
db.add(overdue_task)
db.commit()
stats = ReportService.get_weekly_stats(db, test_user.id)
assert len(stats["projects"][0]["overdue_tasks"]) == 1
assert stats["projects"][0]["overdue_tasks"][0]["days_overdue"] >= 5
def test_full_task_lists_no_limit(self, db, test_user, test_project, test_statuses):
"""Test that task lists have no 5-item limit."""
# Create 10 completed tasks
for i in range(10):
task = Task(
id=str(uuid.uuid4()),
project_id=test_project.id,
title=f"Completed Task {i}",
status_id=test_statuses["done"].id,
created_by=test_user.id,
)
task.updated_at = datetime.utcnow()
db.add(task)
db.commit()
stats = ReportService.get_weekly_stats(db, test_user.id)
assert stats["summary"]["completed_count"] == 10
assert len(stats["projects"][0]["completed_tasks"]) == 10 # No limit
def test_summary_includes_all_counts(self, db, test_user, test_project, test_statuses):
"""Test that summary includes all new count fields."""
stats = ReportService.get_weekly_stats(db, test_user.id)
summary = stats["summary"]
assert "completed_count" in summary
assert "in_progress_count" in summary
assert "overdue_count" in summary
assert "blocked_count" in summary
assert "next_week_count" in summary
assert "total_tasks" in summary