Spaces:
Sleeping
Sleeping
| """ | |
| Comprehensive tests for /api/export endpoint | |
| ============================================= | |
| Tests cover: | |
| - Export format options (Excel, CSV) | |
| - Empty data handling | |
| - Authentication requirements | |
| - File generation | |
| """ | |
| import pytest | |
| import io | |
| from unittest.mock import patch, MagicMock | |
| class TestExportEndpoint: | |
| """Tests for /api/export endpoint""" | |
| def test_export_unauthorized(self, client): | |
| """Test that export requires authentication""" | |
| response = client.post("/api/export", json={ | |
| "format": "excel", | |
| "data": [] | |
| }) | |
| assert response.status_code == 401 | |
| def test_export_empty_data(self, client, auth_headers): | |
| """Test export with empty data""" | |
| response = client.post("/api/export", json={ | |
| "format": "excel", | |
| "data": [] | |
| }, headers=auth_headers) | |
| # Should handle empty data gracefully | |
| assert response.status_code in [200, 400, 422] | |
| def test_export_excel_format(self, client, auth_headers): | |
| """Test Excel export format""" | |
| # First create some comparison data | |
| compare_response = client.post("/api/comparisons/compare", json={ | |
| "hotel_ids": [1, 2], | |
| "check_in": "2026-02-01", | |
| "check_out": "2026-02-03", | |
| "occupancy": "couple" | |
| }, headers=auth_headers) | |
| # Now try to export | |
| response = client.post("/api/export", json={ | |
| "format": "excel", | |
| "comparison_id": 1 | |
| }, headers=auth_headers) | |
| # Check response | |
| if response.status_code == 200: | |
| # Should return binary Excel data | |
| content_type = response.headers.get("content-type", "") | |
| assert "application" in content_type or "octet-stream" in content_type | |
| def test_export_csv_format(self, client, auth_headers): | |
| """Test CSV export format""" | |
| response = client.post("/api/export", json={ | |
| "format": "csv", | |
| "data": [ | |
| {"hotel": "Test Hotel", "price": 450}, | |
| {"hotel": "Another Hotel", "price": 500} | |
| ] | |
| }, headers=auth_headers) | |
| if response.status_code == 200: | |
| content_type = response.headers.get("content-type", "") | |
| # CSV should be text/csv or similar | |
| assert "csv" in content_type.lower() or "text" in content_type.lower() or response.status_code == 200 | |
| def test_export_invalid_format(self, client, auth_headers): | |
| """Test export with invalid format""" | |
| response = client.post("/api/export", json={ | |
| "format": "invalid_format", | |
| "data": [{"test": "data"}] | |
| }, headers=auth_headers) | |
| # Should reject invalid format | |
| assert response.status_code in [400, 422] | |
| def test_export_missing_format(self, client, auth_headers): | |
| """Test export with missing format field""" | |
| response = client.post("/api/export", json={ | |
| "data": [{"test": "data"}] | |
| }, headers=auth_headers) | |
| # Should require format | |
| assert response.status_code in [200, 400, 422] # May have default | |
| def test_export_sql_injection_in_data(self, client, auth_headers): | |
| """Test that SQL injection in export data is safe""" | |
| response = client.post("/api/export", json={ | |
| "format": "excel", | |
| "data": [ | |
| {"hotel": "'; DROP TABLE hotels; --", "price": 450}, | |
| {"hotel": "Test<script>alert(1)</script>", "price": 500} | |
| ] | |
| }, headers=auth_headers) | |
| # Should NOT crash | |
| assert response.status_code != 500 | |
| def test_export_large_dataset(self, client, auth_headers): | |
| """Test export with large dataset""" | |
| large_data = [ | |
| {"hotel": f"Hotel {i}", "price": 400 + i, "rating": 8.0 + (i % 10) / 10} | |
| for i in range(100) | |
| ] | |
| response = client.post("/api/export", json={ | |
| "format": "excel", | |
| "data": large_data | |
| }, headers=auth_headers) | |
| # Should handle large data | |
| assert response.status_code in [200, 413, 422] # 413 = payload too large | |
| def test_export_special_characters(self, client, auth_headers): | |
| """Test export with Hebrew and special characters""" | |
| response = client.post("/api/export", json={ | |
| "format": "excel", | |
| "data": [ | |
| {"hotel": "מלון ישראלי", "price": 450, "location": "תל אביב"}, | |
| {"hotel": "Hotel with émojis 🏨", "price": 500, "location": "Jerusalem"} | |
| ] | |
| }, headers=auth_headers) | |
| # Should handle Unicode properly | |
| assert response.status_code != 500 | |
| class TestExportDownload: | |
| """Tests for export download endpoint""" | |
| def test_download_unauthorized(self, client): | |
| """Test that download requires authentication""" | |
| response = client.get("/api/export/download/some-file-id") | |
| assert response.status_code in [401, 404] | |
| def test_download_nonexistent_file(self, client, auth_headers): | |
| """Test download of non-existent file""" | |
| response = client.get("/api/export/download/nonexistent-file", headers=auth_headers) | |
| assert response.status_code in [404, 400] | |
| def test_download_path_traversal(self, client, auth_headers): | |
| """Test that path traversal attacks are blocked""" | |
| malicious_paths = [ | |
| "../../../etc/passwd", | |
| "..\\..\\..\\windows\\system32\\config\\sam", | |
| "/etc/passwd", | |
| "....//....//....//etc/passwd", | |
| ] | |
| for path in malicious_paths: | |
| response = client.get(f"/api/export/download/{path}", headers=auth_headers) | |
| # Should NOT return a 200 with actual file content | |
| assert response.status_code in [400, 404, 422] | |