InnSight-Backend / api /tests /test_export.py
jackonthemike's picture
Initial commit: InnSight scraper backend with Playwright
d77abf8
"""
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]