"""Tests for booking email notifications.""" from datetime import datetime from unittest.mock import AsyncMock, patch from fastapi.testclient import TestClient from sqlalchemy.orm import Session from app.models.booking import Booking from app.models.space import Space from app.models.user import User @patch("app.api.bookings.send_booking_notification", new_callable=AsyncMock) def test_booking_creation_sends_email_to_admins( mock_email: AsyncMock, client: TestClient, user_token: str, test_space: Space, test_user: User, test_admin: User, db: Session, ) -> None: """Test that creating a booking sends email notifications to all admins.""" from app.core.security import get_password_hash # Create another admin user admin2 = User( email="admin2@example.com", full_name="Second Admin", hashed_password=get_password_hash("password"), role="admin", is_active=True, ) db.add(admin2) db.commit() db.refresh(admin2) # Create a booking booking_data = { "space_id": test_space.id, "start_datetime": "2024-06-15T10:00:00", "end_datetime": "2024-06-15T12:00:00", "title": "Team Planning Session", "description": "Q3 planning and retrospective", } response = client.post( "/api/bookings", json=booking_data, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == 201 # Verify email was sent to both admins (2 calls) assert mock_email.call_count == 2 # Verify the calls contain the correct parameters calls = mock_email.call_args_list admin_emails = {test_admin.email, admin2.email} called_emails = {call[0][2] for call in calls} # Third argument is user_email assert called_emails == admin_emails # Verify all calls have event_type "created" for call in calls: assert call[0][1] == "created" # Second argument is event_type assert call[0][3] == test_user.full_name # Fourth argument is user_name @patch("app.api.bookings.send_booking_notification", new_callable=AsyncMock) def test_booking_approval_sends_email_to_user( mock_email: AsyncMock, client: TestClient, admin_token: str, test_admin: User, test_space: Space, test_user: User, db: Session, ) -> None: """Test that approving a booking sends email notification to the user.""" # Create a pending booking booking = Booking( user_id=test_user.id, space_id=test_space.id, title="Team Meeting", description="Q3 Planning", start_datetime=datetime(2024, 6, 15, 10, 0, 0), end_datetime=datetime(2024, 6, 15, 12, 0, 0), status="pending", created_at=datetime.utcnow(), ) db.add(booking) db.commit() db.refresh(booking) # Approve the booking response = client.put( f"/api/admin/bookings/{booking.id}/approve", headers={"Authorization": f"Bearer {admin_token}"}, ) assert response.status_code == 200 # Verify email was sent mock_email.assert_called_once() # Verify call parameters call_args = mock_email.call_args[0] assert call_args[1] == "approved" # event_type assert call_args[2] == test_user.email # user_email assert call_args[3] == test_user.full_name # user_name assert call_args[4] is None # extra_data @patch("app.api.bookings.send_booking_notification", new_callable=AsyncMock) def test_booking_rejection_sends_email_with_reason( mock_email: AsyncMock, client: TestClient, admin_token: str, test_space: Space, test_user: User, db: Session, ) -> None: """Test that rejecting a booking sends email notification with rejection reason.""" # Create a pending booking booking = Booking( user_id=test_user.id, space_id=test_space.id, title="Team Meeting", description="Q3 Planning", start_datetime=datetime(2024, 6, 15, 10, 0, 0), end_datetime=datetime(2024, 6, 15, 12, 0, 0), status="pending", created_at=datetime.utcnow(), ) db.add(booking) db.commit() db.refresh(booking) # Reject the booking with reason rejection_reason = "Space maintenance scheduled" response = client.put( f"/api/admin/bookings/{booking.id}/reject", json={"reason": rejection_reason}, headers={"Authorization": f"Bearer {admin_token}"}, ) assert response.status_code == 200 # Verify email was sent mock_email.assert_called_once() # Verify call parameters call_args = mock_email.call_args[0] assert call_args[1] == "rejected" # event_type assert call_args[2] == test_user.email # user_email assert call_args[3] == test_user.full_name # user_name # Verify extra_data contains rejection_reason extra_data = call_args[4] assert extra_data is not None assert extra_data["rejection_reason"] == rejection_reason @patch("app.api.bookings.send_booking_notification", new_callable=AsyncMock) def test_admin_cancel_sends_email_with_reason( mock_email: AsyncMock, client: TestClient, admin_token: str, test_space: Space, test_user: User, db: Session, ) -> None: """Test that admin canceling a booking sends email notification with cancellation reason.""" # Create an approved booking booking = Booking( user_id=test_user.id, space_id=test_space.id, title="Team Meeting", description="Q3 Planning", start_datetime=datetime(2024, 6, 15, 10, 0, 0), end_datetime=datetime(2024, 6, 15, 12, 0, 0), status="approved", created_at=datetime.utcnow(), ) db.add(booking) db.commit() db.refresh(booking) # Cancel the booking with reason cancellation_reason = "Emergency maintenance required" response = client.put( f"/api/admin/bookings/{booking.id}/cancel", json={"cancellation_reason": cancellation_reason}, headers={"Authorization": f"Bearer {admin_token}"}, ) assert response.status_code == 200 # Verify email was sent mock_email.assert_called_once() # Verify call parameters call_args = mock_email.call_args[0] assert call_args[1] == "canceled" # event_type assert call_args[2] == test_user.email # user_email assert call_args[3] == test_user.full_name # user_name # Verify extra_data contains cancellation_reason extra_data = call_args[4] assert extra_data is not None assert extra_data["cancellation_reason"] == cancellation_reason @patch("app.api.bookings.send_booking_notification", new_callable=AsyncMock) def test_booking_rejection_without_reason( mock_email: AsyncMock, client: TestClient, admin_token: str, test_space: Space, test_user: User, db: Session, ) -> None: """Test that rejecting a booking without reason sends email with None reason.""" # Create a pending booking booking = Booking( user_id=test_user.id, space_id=test_space.id, title="Team Meeting", start_datetime=datetime(2024, 6, 15, 10, 0, 0), end_datetime=datetime(2024, 6, 15, 12, 0, 0), status="pending", created_at=datetime.utcnow(), ) db.add(booking) db.commit() db.refresh(booking) # Reject the booking without reason response = client.put( f"/api/admin/bookings/{booking.id}/reject", json={}, headers={"Authorization": f"Bearer {admin_token}"}, ) assert response.status_code == 200 # Verify email was sent mock_email.assert_called_once() # Verify call parameters call_args = mock_email.call_args[0] assert call_args[1] == "rejected" # event_type # Verify extra_data contains rejection_reason as None extra_data = call_args[4] assert extra_data is not None assert extra_data["rejection_reason"] is None @patch("app.api.bookings.send_booking_notification", new_callable=AsyncMock) def test_admin_cancel_without_reason( mock_email: AsyncMock, client: TestClient, admin_token: str, test_space: Space, test_user: User, db: Session, ) -> None: """Test that admin canceling without reason sends email with None reason.""" # Create a pending booking booking = Booking( user_id=test_user.id, space_id=test_space.id, title="Client Meeting", start_datetime=datetime(2024, 6, 16, 14, 0, 0), end_datetime=datetime(2024, 6, 16, 16, 0, 0), status="pending", created_at=datetime.utcnow(), ) db.add(booking) db.commit() db.refresh(booking) # Cancel the booking without reason response = client.put( f"/api/admin/bookings/{booking.id}/cancel", json={}, headers={"Authorization": f"Bearer {admin_token}"}, ) assert response.status_code == 200 # Verify email was sent mock_email.assert_called_once() # Verify call parameters call_args = mock_email.call_args[0] assert call_args[1] == "canceled" # event_type # Verify extra_data contains cancellation_reason as None extra_data = call_args[4] assert extra_data is not None assert extra_data["cancellation_reason"] is None