Sistem web pentru rezervarea de birouri și săli de ședință cu flux de aprobare administrativă. Stack: FastAPI + Vue.js 3 + SQLite + TypeScript Features implementate: - Autentificare JWT + Self-registration cu email verification - CRUD Spații, Utilizatori, Settings (Admin) - Calendar interactiv (FullCalendar) cu drag-and-drop - Creare rezervări cu validare (durată, program, overlap, max/zi) - Rezervări recurente (săptămânal) - Admin: aprobare/respingere/anulare cereri - Admin: creare directă rezervări (bypass approval) - Admin: editare orice rezervare - User: editare/anulare rezervări proprii - Notificări in-app (bell icon + dropdown) - Notificări email (async SMTP cu BackgroundTasks) - Jurnal acțiuni administrative (audit log) - Rapoarte avansate (utilizare, top users, approval rate) - Șabloane rezervări (booking templates) - Atașamente fișiere (upload/download) - Conflict warnings (verificare disponibilitate real-time) - Integrare Google Calendar (OAuth2) - Suport timezone (UTC storage + user preference) - 225+ teste backend Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
273 lines
8.7 KiB
Python
273 lines
8.7 KiB
Python
"""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()
|