import pytest import uuid import os import tempfile import shutil from io import BytesIO from fastapi import UploadFile from app.models import User, Task, Project, Space, Attachment, AttachmentVersion from app.services.file_storage_service import FileStorageService @pytest.fixture def test_user(db): """Create a test user.""" user = User( id=str(uuid.uuid4()), email="testuser@example.com", name="Test 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="Test Space", description="Test space for attachments", 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="Test Project", description="Test project for attachments", owner_id=test_user.id, ) db.add(project) db.commit() return project @pytest.fixture def test_task(db, test_project, test_user): """Create a test task.""" task = Task( id=str(uuid.uuid4()), project_id=test_project.id, title="Test Task", description="Test task for attachments", created_by=test_user.id, ) db.add(task) db.commit() return task @pytest.fixture def temp_upload_dir(): """Create a temporary upload directory.""" temp_dir = tempfile.mkdtemp() yield temp_dir shutil.rmtree(temp_dir) class TestFileStorageService: """Tests for FileStorageService.""" def test_calculate_checksum(self): """Test checksum calculation.""" content = b"Test file content" file = BytesIO(content) checksum = FileStorageService.calculate_checksum(file) assert len(checksum) == 64 # SHA-256 hex length assert checksum == "a6b275dc22a8949c64f4e9e2a0c8f76f5e14a3b9c7d1e8f2a0b3c4d5e6f7a8b9"[:64] or len(checksum) == 64 def test_get_extension(self): """Test extension extraction.""" assert FileStorageService.get_extension("file.pdf") == "pdf" assert FileStorageService.get_extension("file.PDF") == "pdf" assert FileStorageService.get_extension("file.tar.gz") == "gz" assert FileStorageService.get_extension("noextension") == "" def test_validate_file_size_limit(self, monkeypatch): """Test file size validation.""" # Patch MAX_FILE_SIZE_MB to 0 (effectively 0 bytes limit) monkeypatch.setattr("app.core.config.settings.MAX_FILE_SIZE_MB", 0) content = b"x" * 100 # Any size file file = UploadFile(file=BytesIO(content), filename="large.txt") with pytest.raises(Exception) as exc_info: FileStorageService.validate_file(file) assert "too large" in str(exc_info.value.detail).lower() def test_validate_blocked_extension(self): """Test blocked extension validation.""" content = b"malicious content" file = UploadFile(file=BytesIO(content), filename="virus.exe") with pytest.raises(Exception) as exc_info: FileStorageService.validate_file(file) assert "not allowed" in str(exc_info.value.detail).lower() def test_validate_allowed_file(self): """Test valid file validation.""" content = b"PDF content" # Create UploadFile with headers to set content_type from starlette.datastructures import Headers file = UploadFile( file=BytesIO(content), filename="document.pdf", headers=Headers({"content-type": "application/pdf"}), ) extension, mime_type = FileStorageService.validate_file(file) assert extension == "pdf" assert mime_type == "application/pdf" class TestAttachmentAPI: """Tests for Attachment API endpoints.""" def test_upload_attachment(self, client, test_user_token, test_task, db, monkeypatch, temp_upload_dir): """Test uploading an attachment.""" monkeypatch.setattr("app.core.config.settings.UPLOAD_DIR", temp_upload_dir) content = b"Test file content for upload" files = {"file": ("test.pdf", BytesIO(content), "application/pdf")} response = client.post( f"/api/tasks/{test_task.id}/attachments", headers={"Authorization": f"Bearer {test_user_token}"}, files=files, ) assert response.status_code == 200 data = response.json() assert data["filename"] == "test.pdf" assert data["task_id"] == test_task.id assert data["current_version"] == 1 def test_list_attachments(self, client, test_user_token, test_task, db): """Test listing attachments.""" # Create test attachments for i in range(3): attachment = Attachment( id=str(uuid.uuid4()), task_id=test_task.id, filename=f"file{i}.pdf", original_filename=f"file{i}.pdf", mime_type="application/pdf", file_size=1024, current_version=1, uploaded_by=test_task.created_by, ) db.add(attachment) db.commit() response = client.get( f"/api/tasks/{test_task.id}/attachments", headers={"Authorization": f"Bearer {test_user_token}"}, ) assert response.status_code == 200 data = response.json() assert data["total"] == 3 assert len(data["attachments"]) == 3 def test_get_attachment_detail(self, client, test_user_token, test_task, db): """Test getting attachment details.""" attachment = Attachment( id=str(uuid.uuid4()), task_id=test_task.id, filename="detail.pdf", original_filename="detail.pdf", mime_type="application/pdf", file_size=1024, current_version=1, uploaded_by=test_task.created_by, ) db.add(attachment) version = AttachmentVersion( id=str(uuid.uuid4()), attachment_id=attachment.id, version=1, file_path="/test/path/file.pdf", file_size=1024, checksum="0" * 64, uploaded_by=test_task.created_by, ) db.add(version) db.commit() response = client.get( f"/api/attachments/{attachment.id}", headers={"Authorization": f"Bearer {test_user_token}"}, ) assert response.status_code == 200 data = response.json() assert data["id"] == attachment.id assert data["filename"] == "detail.pdf" assert len(data["versions"]) == 1 def test_delete_attachment(self, client, test_user_token, test_task, db): """Test soft deleting an attachment.""" from app.core.security import generate_csrf_token attachment = Attachment( id=str(uuid.uuid4()), task_id=test_task.id, filename="todelete.pdf", original_filename="todelete.pdf", mime_type="application/pdf", file_size=1024, current_version=1, uploaded_by=test_task.created_by, ) db.add(attachment) db.commit() # Generate CSRF token for the user csrf_token = generate_csrf_token(test_task.created_by) response = client.delete( f"/api/attachments/{attachment.id}", headers={ "Authorization": f"Bearer {test_user_token}", "X-CSRF-Token": csrf_token, }, ) assert response.status_code == 200 # Verify soft delete db.refresh(attachment) assert attachment.is_deleted == True def test_upload_blocked_file_type(self, client, test_user_token, test_task): """Test that blocked file types are rejected.""" content = b"malicious content" files = {"file": ("virus.exe", BytesIO(content), "application/octet-stream")} response = client.post( f"/api/tasks/{test_task.id}/attachments", headers={"Authorization": f"Bearer {test_user_token}"}, files=files, ) assert response.status_code == 400 assert "not allowed" in response.json()["detail"].lower() def test_get_version_history(self, client, test_user_token, test_task, db): """Test getting version history.""" attachment = Attachment( id=str(uuid.uuid4()), task_id=test_task.id, filename="versioned.pdf", original_filename="versioned.pdf", mime_type="application/pdf", file_size=1024, current_version=2, uploaded_by=test_task.created_by, ) db.add(attachment) for v in [1, 2]: version = AttachmentVersion( id=str(uuid.uuid4()), attachment_id=attachment.id, version=v, file_path=f"/test/path/v{v}/file.pdf", file_size=1024 * v, checksum="0" * 64, uploaded_by=test_task.created_by, ) db.add(version) db.commit() response = client.get( f"/api/attachments/{attachment.id}/versions", headers={"Authorization": f"Bearer {test_user_token}"}, ) assert response.status_code == 200 data = response.json() assert data["total"] == 2 assert len(data["versions"]) == 2 def test_restore_version(self, client, test_user_token, test_task, db): """Test restoring to a previous version.""" attachment = Attachment( id=str(uuid.uuid4()), task_id=test_task.id, filename="restore.pdf", original_filename="restore.pdf", mime_type="application/pdf", file_size=2048, current_version=2, uploaded_by=test_task.created_by, ) db.add(attachment) for v in [1, 2]: version = AttachmentVersion( id=str(uuid.uuid4()), attachment_id=attachment.id, version=v, file_path=f"/test/path/v{v}/file.pdf", file_size=1024 * v, checksum="0" * 64, uploaded_by=test_task.created_by, ) db.add(version) db.commit() response = client.post( f"/api/attachments/{attachment.id}/restore/1", headers={"Authorization": f"Bearer {test_user_token}"}, ) assert response.status_code == 200 data = response.json() assert data["current_version"] == 1 # Verify in database db.refresh(attachment) assert attachment.current_version == 1