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