"""Tests for attachments API.""" from io import BytesIO from pathlib import Path import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session from app.models.attachment import Attachment from app.models.booking import Booking from app.models.user import User @pytest.fixture def test_attachment(db: Session, test_booking: Booking, test_user: User) -> Attachment: """Create test attachment.""" attachment = Attachment( booking_id=test_booking.id, filename="test.pdf", stored_filename="uuid-test.pdf", filepath="/tmp/uuid-test.pdf", size=1024, content_type="application/pdf", uploaded_by=test_user.id, ) db.add(attachment) db.commit() db.refresh(attachment) return attachment def test_upload_attachment( client: TestClient, auth_headers: dict[str, str], test_booking: Booking ) -> None: """Test uploading file attachment.""" # Create a test PDF file file_content = b"PDF file content here" files = {"file": ("test.pdf", BytesIO(file_content), "application/pdf")} response = client.post( f"/api/bookings/{test_booking.id}/attachments", files=files, headers=auth_headers ) assert response.status_code == 201 data = response.json() assert data["booking_id"] == test_booking.id assert data["filename"] == "test.pdf" assert data["size"] == len(file_content) assert data["content_type"] == "application/pdf" def test_upload_attachment_invalid_type( client: TestClient, auth_headers: dict[str, str], test_booking: Booking ) -> None: """Test uploading file with invalid type.""" file_content = b"Invalid file content" files = {"file": ("test.exe", BytesIO(file_content), "application/exe")} response = client.post( f"/api/bookings/{test_booking.id}/attachments", files=files, headers=auth_headers ) assert response.status_code == 400 assert "not allowed" in response.json()["detail"] def test_upload_attachment_too_large( client: TestClient, auth_headers: dict[str, str], test_booking: Booking ) -> None: """Test uploading file that exceeds size limit.""" # Create file larger than 10MB file_content = b"x" * (11 * 1024 * 1024) files = {"file": ("large.pdf", BytesIO(file_content), "application/pdf")} response = client.post( f"/api/bookings/{test_booking.id}/attachments", files=files, headers=auth_headers ) assert response.status_code == 400 assert "too large" in response.json()["detail"] def test_upload_exceeds_limit( client: TestClient, auth_headers: dict[str, str], test_booking: Booking ) -> None: """Test uploading more than 5 files.""" # Upload 5 files for i in range(5): file_content = b"PDF file content" files = {"file": (f"test{i}.pdf", BytesIO(file_content), "application/pdf")} response = client.post( f"/api/bookings/{test_booking.id}/attachments", files=files, headers=auth_headers ) assert response.status_code == 201 # Try to upload 6th file file_content = b"PDF file content" files = {"file": ("test6.pdf", BytesIO(file_content), "application/pdf")} response = client.post( f"/api/bookings/{test_booking.id}/attachments", files=files, headers=auth_headers ) assert response.status_code == 400 assert "Maximum 5 files" in response.json()["detail"] def test_upload_to_others_booking( client: TestClient, test_user: User, test_admin: User, db: Session ) -> None: """Test user cannot upload to another user's booking.""" from datetime import datetime from app.core.security import create_access_token from app.models.space import Space # Create space space = Space(name="Test Room", type="sala", capacity=10, is_active=True) db.add(space) db.commit() # Create booking for admin booking = Booking( user_id=test_admin.id, space_id=space.id, title="Admin Meeting", start_datetime=datetime(2024, 3, 15, 10, 0, 0), end_datetime=datetime(2024, 3, 15, 12, 0, 0), status="approved", ) db.add(booking) db.commit() # Try to upload as regular user user_token = create_access_token(subject=int(test_user.id)) headers = {"Authorization": f"Bearer {user_token}"} file_content = b"PDF file content" files = {"file": ("test.pdf", BytesIO(file_content), "application/pdf")} response = client.post(f"/api/bookings/{booking.id}/attachments", files=files, headers=headers) assert response.status_code == 403 def test_list_attachments( client: TestClient, auth_headers: dict[str, str], test_booking: Booking, test_attachment: Attachment ) -> None: """Test listing attachments for a booking.""" response = client.get(f"/api/bookings/{test_booking.id}/attachments", headers=auth_headers) assert response.status_code == 200 data = response.json() assert len(data) == 1 assert data[0]["id"] == test_attachment.id assert data[0]["filename"] == test_attachment.filename def test_download_attachment( client: TestClient, auth_headers: dict[str, str], test_attachment: Attachment ) -> None: """Test downloading attachment file.""" # Create actual file Path(test_attachment.filepath).parent.mkdir(parents=True, exist_ok=True) Path(test_attachment.filepath).write_bytes(b"Test file content") response = client.get(f"/api/attachments/{test_attachment.id}/download", headers=auth_headers) assert response.status_code == 200 assert response.content == b"Test file content" # Cleanup Path(test_attachment.filepath).unlink() def test_download_attachment_not_found(client: TestClient, auth_headers: dict[str, str]) -> None: """Test downloading non-existent attachment.""" response = client.get("/api/attachments/999/download", headers=auth_headers) assert response.status_code == 404 def test_delete_attachment( client: TestClient, auth_headers: dict[str, str], test_attachment: Attachment, db: Session ) -> None: """Test deleting attachment.""" # Create actual file Path(test_attachment.filepath).parent.mkdir(parents=True, exist_ok=True) Path(test_attachment.filepath).write_bytes(b"Test file content") response = client.delete(f"/api/attachments/{test_attachment.id}", headers=auth_headers) assert response.status_code == 204 # Verify deleted from database attachment = db.query(Attachment).filter(Attachment.id == test_attachment.id).first() assert attachment is None # Verify file deleted assert not Path(test_attachment.filepath).exists() def test_delete_attachment_not_owner( client: TestClient, auth_headers: dict[str, str], test_user: User, db: Session ) -> None: """Test user cannot delete another user's attachment.""" from datetime import datetime from app.core.security import get_password_hash from app.models.space import Space # Create another user other_user = User( email="other@example.com", full_name="Other User", hashed_password=get_password_hash("password"), role="user", is_active=True, ) db.add(other_user) db.commit() # Create space space = Space(name="Test Room", type="sala", capacity=10, is_active=True) db.add(space) db.commit() # Create booking for other user booking = Booking( user_id=other_user.id, space_id=space.id, title="Other User Meeting", start_datetime=datetime(2024, 3, 15, 10, 0, 0), end_datetime=datetime(2024, 3, 15, 12, 0, 0), status="approved", ) db.add(booking) db.commit() # Create attachment uploaded by other user attachment = Attachment( booking_id=booking.id, filename="other.pdf", stored_filename="uuid-other.pdf", filepath="/tmp/uuid-other.pdf", size=1024, content_type="application/pdf", uploaded_by=other_user.id, ) db.add(attachment) db.commit() # Try to delete as test_user response = client.delete(f"/api/attachments/{attachment.id}", headers=auth_headers) assert response.status_code == 403 def test_admin_can_delete_any_attachment( client: TestClient, admin_headers: dict[str, str], test_attachment: Attachment ) -> None: """Test admin can delete any attachment.""" # Create actual file Path(test_attachment.filepath).parent.mkdir(parents=True, exist_ok=True) Path(test_attachment.filepath).write_bytes(b"Test file content") response = client.delete(f"/api/attachments/{test_attachment.id}", headers=admin_headers) assert response.status_code == 204 # Cleanup if Path(test_attachment.filepath).exists(): Path(test_attachment.filepath).unlink()