diff --git a/tests/test_file_detail_endpoints.py b/tests/test_file_detail_endpoints.py index e5dc1c5a..8ab51719 100644 --- a/tests/test_file_detail_endpoints.py +++ b/tests/test_file_detail_endpoints.py @@ -71,6 +71,55 @@ class TestFileReprocessing: assert "not found on disk" in response.json()["detail"].lower() +@pytest.mark.integration +class TestSubtaskRetry: + """Tests for per-subtask retry endpoint.""" + + def test_retry_subtask_invalid_file(self, client: TestClient): + """Test retrying a subtask for nonexistent file.""" + response = client.post("/api/files/99999/retry-subtask?subtask_name=upload_to_dropbox") + assert response.status_code == 404 + assert "not found" in response.json()["detail"].lower() + + def test_retry_subtask_invalid_task_name(self, client: TestClient, db_session, sample_pdf_path): + """Test retrying with invalid subtask name.""" + # Create a file record + file_record = FileRecord( + filehash="retry123", + original_filename="retry.pdf", + local_filename=sample_pdf_path, + file_size=1024, + mime_type="application/pdf" + ) + db_session.add(file_record) + db_session.commit() + db_session.refresh(file_record) + + # Test with invalid subtask name + response = client.post(f"/api/files/{file_record.id}/retry-subtask?subtask_name=invalid_task") + assert response.status_code == 400 + assert "invalid subtask name" in response.json()["detail"].lower() + + def test_retry_subtask_missing_processed_file(self, client: TestClient, db_session, sample_pdf_path): + """Test retrying when processed file is missing.""" + # Create a file record + file_record = FileRecord( + filehash="retry456", + original_filename="retry2.pdf", + local_filename=sample_pdf_path, + file_size=1024, + mime_type="application/pdf" + ) + db_session.add(file_record) + db_session.commit() + db_session.refresh(file_record) + + # Test retry (processed file won't exist) + response = client.post(f"/api/files/{file_record.id}/retry-subtask?subtask_name=upload_to_dropbox") + assert response.status_code == 400 + assert "processed file not found" in response.json()["detail"].lower() + + @pytest.mark.integration class TestFilePreview: """Tests for file preview endpoint.""" @@ -193,8 +242,146 @@ class TestFileDetailView: assert b"detail.pdf" in response.content assert b"Processing History" in response.content + def test_file_detail_view_with_upload_branches(self, client: TestClient, db_session, sample_pdf_path): + """Test file detail view with upload subtask branches.""" + # Create a file record + file_record = FileRecord( + filehash="branch123", + original_filename="branches.pdf", + local_filename=sample_pdf_path, + file_size=1024, + mime_type="application/pdf" + ) + db_session.add(file_record) + db_session.commit() + db_session.refresh(file_record) + + # Add processing logs including upload branches + logs = [ + ProcessingLog( + file_id=file_record.id, + task_id="task-1", + step_name="send_to_all_destinations", + status="success", + message="Queued uploads" + ), + ProcessingLog( + file_id=file_record.id, + task_id="task-2", + step_name="upload_to_dropbox", + status="success", + message="Uploaded to Dropbox" + ), + ProcessingLog( + file_id=file_record.id, + task_id="task-3", + step_name="upload_to_s3", + status="failure", + message="S3 connection error" + ), + ProcessingLog( + file_id=file_record.id, + task_id="task-4", + step_name="upload_to_nextcloud", + status="success", + message="Uploaded to Nextcloud" + ) + ] + for log in logs: + db_session.add(log) + db_session.commit() + + # Test detail view + response = client.get(f"/files/{file_record.id}/detail") + assert response.status_code == 200 + # Check that response contains branching visualization elements + assert b"Process Flow Visualization" in response.content + assert b"Processing Status Summary" in response.content + # Should have upload branches + assert b"Dropbox" in response.content or b"dropbox" in response.content + def test_file_detail_view_nonexistent(self, client: TestClient): """Test file detail view for nonexistent file.""" response = client.get("/files/99999/detail") assert response.status_code == 200 # Returns page with error message assert b"not found" in response.content.lower() + + +@pytest.mark.unit +class TestProcessingFlowComputation: + """Tests for the _compute_processing_flow function.""" + + def test_flow_with_upload_branches(self, db_session): + """Test that upload tasks are properly grouped as branches.""" + from app.views.files import _compute_processing_flow + + # Create mock logs + class MockLog: + def __init__(self, step_name, status, message, timestamp, task_id): + self.step_name = step_name + self.status = status + self.message = message + self.timestamp = timestamp + self.task_id = task_id + + logs = [ + MockLog("hash_file", "success", "Hashed", None, "task-1"), + MockLog("send_to_all_destinations", "success", "Queued", None, "task-2"), + MockLog("upload_to_dropbox", "success", "Uploaded", None, "task-3"), + MockLog("upload_to_s3", "failure", "Failed", None, "task-4"), + ] + + flow = _compute_processing_flow(logs) + + # Find the upload stage + upload_stage = None + for stage in flow: + if stage.get("is_branch_parent"): + upload_stage = stage + break + + assert upload_stage is not None + assert "branches" in upload_stage + assert len(upload_stage["branches"]) == 2 + + # Check branch details + branches = {b["key"]: b for b in upload_stage["branches"]} + assert "upload_to_dropbox" in branches + assert branches["upload_to_dropbox"]["status"] == "success" + assert "upload_to_s3" in branches + assert branches["upload_to_s3"]["status"] == "failure" + assert branches["upload_to_s3"]["can_retry"] is True + + +@pytest.mark.unit +class TestStepSummary: + """Tests for the _compute_step_summary function.""" + + def test_summary_with_mixed_statuses(self): + """Test step summary with various statuses.""" + from app.views.files import _compute_step_summary + + # Create mock logs + class MockLog: + def __init__(self, step_name, status): + self.step_name = step_name + self.status = status + + logs = [ + MockLog("hash_file", "success"), + MockLog("create_file_record", "success"), + MockLog("extract_metadata_with_gpt", "failure"), + MockLog("upload_to_dropbox", "success"), + MockLog("upload_to_s3", "failure"), + MockLog("upload_to_nextcloud", "in_progress"), + ] + + summary = _compute_step_summary(logs) + + assert "main" in summary + assert "uploads" in summary + assert summary["total_main_steps"] == 3 + assert summary["total_upload_tasks"] == 3 + assert summary["uploads"]["success"] == 1 + assert summary["uploads"]["failure"] == 1 + assert summary["uploads"]["in_progress"] == 1