diff --git a/backend/__pycache__/main.cpython-312.pyc b/backend/__pycache__/main.cpython-312.pyc index 69448ad..6dcde14 100644 Binary files a/backend/__pycache__/main.cpython-312.pyc and b/backend/__pycache__/main.cpython-312.pyc differ diff --git a/backend/core/__pycache__/celery_app.cpython-312.pyc b/backend/core/__pycache__/celery_app.cpython-312.pyc index cd90706..d8775c4 100644 Binary files a/backend/core/__pycache__/celery_app.cpython-312.pyc and b/backend/core/__pycache__/celery_app.cpython-312.pyc differ diff --git a/backend/core/__pycache__/config.cpython-312.pyc b/backend/core/__pycache__/config.cpython-312.pyc index 1fe6175..c9e6c77 100644 Binary files a/backend/core/__pycache__/config.cpython-312.pyc and b/backend/core/__pycache__/config.cpython-312.pyc differ diff --git a/backend/core/__pycache__/database.cpython-312.pyc b/backend/core/__pycache__/database.cpython-312.pyc index cded6e3..e8c0b69 100644 Binary files a/backend/core/__pycache__/database.cpython-312.pyc and b/backend/core/__pycache__/database.cpython-312.pyc differ diff --git a/backend/modules/admin/__pycache__/api.cpython-312.pyc b/backend/modules/admin/__pycache__/api.cpython-312.pyc index b5d6c10..9fde471 100644 Binary files a/backend/modules/admin/__pycache__/api.cpython-312.pyc and b/backend/modules/admin/__pycache__/api.cpython-312.pyc differ diff --git a/backend/modules/calendar/__pycache__/models.cpython-312.pyc b/backend/modules/calendar/__pycache__/models.cpython-312.pyc index 07aaa4f..707d1f4 100644 Binary files a/backend/modules/calendar/__pycache__/models.cpython-312.pyc and b/backend/modules/calendar/__pycache__/models.cpython-312.pyc differ diff --git a/backend/modules/calendar/__pycache__/service.cpython-312.pyc b/backend/modules/calendar/__pycache__/service.cpython-312.pyc index 480f004..3a8c903 100644 Binary files a/backend/modules/calendar/__pycache__/service.cpython-312.pyc and b/backend/modules/calendar/__pycache__/service.cpython-312.pyc differ diff --git a/backend/modules/calendar/__pycache__/tasks.cpython-312.pyc b/backend/modules/calendar/__pycache__/tasks.cpython-312.pyc index 5b748d3..49665c9 100644 Binary files a/backend/modules/calendar/__pycache__/tasks.cpython-312.pyc and b/backend/modules/calendar/__pycache__/tasks.cpython-312.pyc differ diff --git a/backend/modules/notifications/__pycache__/service.cpython-312.pyc b/backend/modules/notifications/__pycache__/service.cpython-312.pyc index 092672a..478eec4 100644 Binary files a/backend/modules/notifications/__pycache__/service.cpython-312.pyc and b/backend/modules/notifications/__pycache__/service.cpython-312.pyc differ diff --git a/backend/modules/todo/__pycache__/models.cpython-312.pyc b/backend/modules/todo/__pycache__/models.cpython-312.pyc index 537ee04..884c88b 100644 Binary files a/backend/modules/todo/__pycache__/models.cpython-312.pyc and b/backend/modules/todo/__pycache__/models.cpython-312.pyc differ diff --git a/backend/modules/user/__pycache__/api.cpython-312.pyc b/backend/modules/user/__pycache__/api.cpython-312.pyc index f2e94ab..1175aaf 100644 Binary files a/backend/modules/user/__pycache__/api.cpython-312.pyc and b/backend/modules/user/__pycache__/api.cpython-312.pyc differ diff --git a/backend/tests/__pycache__/test_calendar.cpython-312-pytest-8.3.5.pyc b/backend/tests/__pycache__/test_calendar.cpython-312-pytest-8.3.5.pyc index 29c3508..346b54c 100644 Binary files a/backend/tests/__pycache__/test_calendar.cpython-312-pytest-8.3.5.pyc and b/backend/tests/__pycache__/test_calendar.cpython-312-pytest-8.3.5.pyc differ diff --git a/backend/tests/test_calendar.py b/backend/tests/test_calendar.py index 35f3bec..d4685a7 100644 --- a/backend/tests/test_calendar.py +++ b/backend/tests/test_calendar.py @@ -1,7 +1,8 @@ from fastapi import status from fastapi.testclient import TestClient from sqlalchemy.orm import Session -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone # Add timezone +from pytest_mock import MockerFixture # Import MockerFixture from tests.helpers import generators from modules.calendar.models import CalendarEvent # Assuming model exists @@ -10,13 +11,14 @@ from tests.conftest import fake # Helper function to create an event payload def create_event_payload(start_offset_days=0, end_offset_days=1): - start_time = datetime.utcnow() + timedelta(days=start_offset_days) - end_time = datetime.utcnow() + timedelta(days=end_offset_days) + # Ensure datetimes are timezone-aware (UTC) + start_time = datetime.now(timezone.utc) + timedelta(days=start_offset_days) + end_time = datetime.now(timezone.utc) + timedelta(days=end_offset_days) return { "title": fake.sentence(nb_words=3), "description": fake.text(), - "start": start_time.isoformat(), # Rename start_time to start - "end": end_time.isoformat(), # Rename end_time to end + "start": start_time.isoformat().replace("+00:00", "Z"), # Ensure Z suffix + "end": end_time.isoformat().replace("+00:00", "Z"), # Ensure Z suffix "all_day": fake.boolean(), } @@ -31,13 +33,20 @@ def test_create_event_unauthorized(client: TestClient) -> None: assert response.status_code == status.HTTP_401_UNAUTHORIZED -def test_create_event_success(db: Session, client: TestClient) -> None: +def test_create_event_success( + db: Session, client: TestClient, mocker: MockerFixture +) -> None: """Test creating a calendar event successfully.""" user, password = generators.create_user(db) login_rsp = generators.login(db, user.username, password) access_token = login_rsp["access_token"] payload = create_event_payload() + # Mock the celery task sending + mock_send_task = mocker.patch( + "core.celery_app.celery_app.send_task" + ) # Corrected patch target + response = client.post( "/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"}, @@ -49,7 +58,7 @@ def test_create_event_success(db: Session, client: TestClient) -> None: data = response.json() assert data["title"] == payload["title"] assert data["description"] == payload["description"] - # Remove the '+ "Z"' as the API doesn't add it + # Assert with Z suffix assert data["start"] == payload["start"] assert data["end"] == payload["end"] assert data["all_day"] == payload["all_day"] @@ -62,6 +71,11 @@ def test_create_event_success(db: Session, client: TestClient) -> None: assert event_in_db.user_id == user.id assert event_in_db.title == payload["title"] + # Assert that the task was called correctly + mock_send_task.assert_called_once_with( + "modules.calendar.tasks.schedule_event_notifications", args=[data["id"]] + ) + # --- Test Get Events --- @@ -72,36 +86,49 @@ def test_get_events_unauthorized(client: TestClient) -> None: assert response.status_code == status.HTTP_401_UNAUTHORIZED -def test_get_events_success(db: Session, client: TestClient) -> None: +def test_get_events_success( + db: Session, client: TestClient, mocker: MockerFixture +) -> None: # Add mocker """Test getting all calendar events for a user.""" - user, password = generators.create_user(db) + user, password = generators.create_user( + db, username="testuser_get_events" + ) # Unique username login_rsp = generators.login(db, user.username, password) access_token = login_rsp["access_token"] + # Mock celery task for creation + mocker.patch("core.celery_app.celery_app.send_task") + # Create a couple of events for the user payload1 = create_event_payload(0, 1) - client.post( + create_rsp1 = client.post( "/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"}, json=payload1, ) + assert create_rsp1.status_code == status.HTTP_201_CREATED + payload2 = create_event_payload(2, 3) - client.post( + create_rsp2 = client.post( "/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"}, json=payload2, ) + assert create_rsp2.status_code == status.HTTP_201_CREATED # Create an event for another user (should not be returned) - other_user, other_password = generators.create_user(db) + other_user, other_password = generators.create_user( + db, username="otheruser_get_events" + ) # Unique username other_login_rsp = generators.login(db, other_user.username, other_password) other_access_token = other_login_rsp["access_token"] other_payload = create_event_payload(4, 5) - client.post( + create_rsp_other = client.post( "/api/calendar/events", headers={"Authorization": f"Bearer {other_access_token}"}, json=other_payload, ) + assert create_rsp_other.status_code == status.HTTP_201_CREATED response = client.get( "/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"} @@ -115,35 +142,51 @@ def test_get_events_success(db: Session, client: TestClient) -> None: assert data[1]["user_id"] == user.id -def test_get_events_filtered(db: Session, client: TestClient) -> None: +def test_get_events_filtered( + db: Session, client: TestClient, mocker: MockerFixture +) -> None: # Add mocker """Test getting filtered calendar events for a user.""" - user, password = generators.create_user(db) + user, password = generators.create_user( + db, username="testuser_filter_events" + ) # Unique username login_rsp = generators.login(db, user.username, password) access_token = login_rsp["access_token"] + # Mock celery task for creation + mocker.patch("core.celery_app.celery_app.send_task") + # Create events payload1 = create_event_payload(0, 1) # Today -> Tomorrow - client.post( + create_rsp1 = client.post( "/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"}, json=payload1, ) + assert create_rsp1.status_code == status.HTTP_201_CREATED + payload2 = create_event_payload(5, 6) # In 5 days -> In 6 days - client.post( + create_rsp2 = client.post( "/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"}, json=payload2, ) + assert create_rsp2.status_code == status.HTTP_201_CREATED + payload3 = create_event_payload(10, 11) # In 10 days -> In 11 days - client.post( + create_rsp3 = client.post( "/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"}, json=payload3, ) + assert create_rsp3.status_code == status.HTTP_201_CREATED # Filter for events starting within the next week - start_filter = datetime.utcnow().isoformat() - end_filter = (datetime.utcnow() + timedelta(days=7)).isoformat() + start_filter = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") + end_filter = ( + (datetime.now(timezone.utc) + timedelta(days=7)) + .isoformat() + .replace("+00:00", "Z") + ) response = client.get( "/api/calendar/events", @@ -157,7 +200,11 @@ def test_get_events_filtered(db: Session, client: TestClient) -> None: assert data[1]["title"] == payload2["title"] # Filter for events starting after 8 days - start_filter_late = (datetime.utcnow() + timedelta(days=8)).isoformat() + start_filter_late = ( + (datetime.now(timezone.utc) + timedelta(days=8)) + .isoformat() + .replace("+00:00", "Z") + ) response = client.get( "/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"}, @@ -172,34 +219,48 @@ def test_get_events_filtered(db: Session, client: TestClient) -> None: # --- Test Get Event By ID --- -def test_get_event_by_id_unauthorized(db: Session, client: TestClient) -> None: +def test_get_event_by_id_unauthorized( + db: Session, client: TestClient, mocker: MockerFixture +) -> None: # Add mocker """Test getting a specific event without authentication.""" user, password = generators.create_user(db) login_rsp = generators.login(db, user.username, password) access_token = login_rsp["access_token"] payload = create_event_payload() + + # Mock celery task for creation + mocker.patch("core.celery_app.celery_app.send_task") + create_response = client.post( "/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"}, json=payload, ) + assert create_response.status_code == status.HTTP_201_CREATED event_id = create_response.json()["id"] response = client.get(f"/api/calendar/events/{event_id}") assert response.status_code == status.HTTP_401_UNAUTHORIZED -def test_get_event_by_id_success(db: Session, client: TestClient) -> None: +def test_get_event_by_id_success( + db: Session, client: TestClient, mocker: MockerFixture +) -> None: # Add mocker """Test getting a specific event successfully.""" user, password = generators.create_user(db) login_rsp = generators.login(db, user.username, password) access_token = login_rsp["access_token"] payload = create_event_payload() + + # Mock celery task for creation + mocker.patch("core.celery_app.celery_app.send_task") + create_response = client.post( "/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"}, json=payload, ) + assert create_response.status_code == status.HTTP_201_CREATED event_id = create_response.json()["id"] response = client.get( @@ -210,6 +271,9 @@ def test_get_event_by_id_success(db: Session, client: TestClient) -> None: data = response.json() assert data["id"] == event_id assert data["title"] == payload["title"] + # Assert datetime with Z suffix + assert data["start"] == payload["start"] + assert data["end"] == payload["end"] assert data["user_id"] == user.id @@ -227,20 +291,31 @@ def test_get_event_by_id_not_found(db: Session, client: TestClient) -> None: assert response.status_code == status.HTTP_404_NOT_FOUND -def test_get_event_by_id_forbidden(db: Session, client: TestClient) -> None: +def test_get_event_by_id_forbidden( + db: Session, client: TestClient, mocker: MockerFixture +) -> None: # Add mocker """Test getting another user's event.""" - user1, password_user1 = generators.create_user(db) - user2, password_user2 = generators.create_user(db) + user1, password_user1 = generators.create_user( + db, username="user1_forbidden_get" + ) # Unique username + user2, password_user2 = generators.create_user( + db, username="user2_forbidden_get" + ) # Unique username # Log in as user1 and create an event login_rsp1 = generators.login(db, user1.username, password_user1) access_token1 = login_rsp1["access_token"] payload = create_event_payload() + + # Mock celery task for creation + mocker.patch("core.celery_app.celery_app.send_task") + create_response = client.post( "/api/calendar/events", headers={"Authorization": f"Bearer {access_token1}"}, json=payload, ) + assert create_response.status_code == status.HTTP_201_CREATED event_id = create_response.json()["id"] # Log in as user2 and try to get user1's event @@ -259,17 +334,24 @@ def test_get_event_by_id_forbidden(db: Session, client: TestClient) -> None: # --- Test Update Event --- -def test_update_event_unauthorized(db: Session, client: TestClient) -> None: +def test_update_event_unauthorized( + db: Session, client: TestClient, mocker: MockerFixture +) -> None: # Add mocker """Test updating an event without authentication.""" user, password = generators.create_user(db) login_rsp = generators.login(db, user.username, password) access_token = login_rsp["access_token"] payload = create_event_payload() + + # Mock celery task for creation + mocker.patch("core.celery_app.celery_app.send_task") + create_response = client.post( "/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"}, json=payload, ) + assert create_response.status_code == status.HTTP_201_CREATED event_id = create_response.json()["id"] update_payload = {"title": "Updated Title"} @@ -277,12 +359,20 @@ def test_update_event_unauthorized(db: Session, client: TestClient) -> None: assert response.status_code == status.HTTP_401_UNAUTHORIZED -def test_update_event_success(db: Session, client: TestClient) -> None: +def test_update_event_success( + db: Session, client: TestClient, mocker: MockerFixture +) -> None: # Add mocker """Test updating an event successfully.""" user, password = generators.create_user(db) login_rsp = generators.login(db, user.username, password) access_token = login_rsp["access_token"] payload = create_event_payload() + + # Mock celery task for creation + mocker.patch( + "core.celery_app.celery_app.send_task", return_value=None + ) # Mock for creation + create_response = client.post( "/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"}, @@ -299,6 +389,13 @@ def test_update_event_success(db: Session, client: TestClient) -> None: "all_day": not payload["all_day"], # Toggle all_day } + # Mock celery task for update (needs separate mock) + mock_send_task_update = mocker.patch( + "modules.calendar.service.celery_app.send_task" + ) + # Mock cancel notifications as well, as it's called synchronously in the service + mocker.patch("modules.calendar.tasks.cancel_event_notifications") + response = client.patch( f"/api/calendar/events/{event_id}", headers={"Authorization": f"Bearer {access_token}"}, @@ -310,7 +407,8 @@ def test_update_event_success(db: Session, client: TestClient) -> None: assert data["title"] == update_payload["title"] assert data["description"] == update_payload["description"] assert data["all_day"] == update_payload["all_day"] - assert data["start"] == payload["start"] # Check correct field name 'start' + # Assert datetime with Z suffix + assert data["start"] == payload["start"] assert data["user_id"] == user.id # Verify in DB @@ -320,6 +418,17 @@ def test_update_event_success(db: Session, client: TestClient) -> None: assert event_in_db.description == update_payload["description"] assert event_in_db.all_day == update_payload["all_day"] + # Assert that the update task was called correctly + mock_send_task_update.assert_called_once_with( + "modules.calendar.tasks.schedule_event_notifications", args=[event_id] + ) + # Assert cancel was NOT called because update doesn't cancel + # mock_cancel_notifications.assert_not_called() # Update: cancel IS called in update path via re-schedule + # Actually, schedule_event_notifications calls cancel_event_notifications first. + # So we need to mock cancel_event_notifications called *within* schedule_event_notifications + # OR mock schedule_event_notifications itself. Let's stick to mocking send_task. + # The cancel mock added earlier handles the direct call in the service layer if any. + def test_update_event_not_found(db: Session, client: TestClient) -> None: """Test updating a non-existent event.""" @@ -337,20 +446,31 @@ def test_update_event_not_found(db: Session, client: TestClient) -> None: assert response.status_code == status.HTTP_404_NOT_FOUND -def test_update_event_forbidden(db: Session, client: TestClient) -> None: +def test_update_event_forbidden( + db: Session, client: TestClient, mocker: MockerFixture +) -> None: # Add mocker """Test updating another user's event.""" - user1, password_user1 = generators.create_user(db) - user2, password_user2 = generators.create_user(db) + user1, password_user1 = generators.create_user( + db, username="user1_forbidden_update" + ) # Unique username + user2, password_user2 = generators.create_user( + db, username="user2_forbidden_update" + ) # Unique username # Log in as user1 and create an event login_rsp1 = generators.login(db, user1.username, password_user1) access_token1 = login_rsp1["access_token"] payload = create_event_payload() + + # Mock celery task for creation + mocker.patch("core.celery_app.celery_app.send_task") + create_response = client.post( "/api/calendar/events", headers={"Authorization": f"Bearer {access_token1}"}, json=payload, ) + assert create_response.status_code == status.HTTP_201_CREATED event_id = create_response.json()["id"] # Log in as user2 and try to update user1's event @@ -371,29 +491,42 @@ def test_update_event_forbidden(db: Session, client: TestClient) -> None: # --- Test Delete Event --- -def test_delete_event_unauthorized(db: Session, client: TestClient) -> None: +def test_delete_event_unauthorized( + db: Session, client: TestClient, mocker: MockerFixture +) -> None: # Add mocker """Test deleting an event without authentication.""" user, password = generators.create_user(db) login_rsp = generators.login(db, user.username, password) access_token = login_rsp["access_token"] payload = create_event_payload() + + # Mock celery task for creation + mocker.patch("core.celery_app.celery_app.send_task") + create_response = client.post( "/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"}, json=payload, ) + assert create_response.status_code == status.HTTP_201_CREATED event_id = create_response.json()["id"] response = client.delete(f"/api/calendar/events/{event_id}") assert response.status_code == status.HTTP_401_UNAUTHORIZED -def test_delete_event_success(db: Session, client: TestClient) -> None: +def test_delete_event_success( + db: Session, client: TestClient, mocker: MockerFixture +) -> None: """Test deleting an event successfully.""" user, password = generators.create_user(db) login_rsp = generators.login(db, user.username, password) access_token = login_rsp["access_token"] payload = create_event_payload() + + # Mock the celery task sending for creation + mocker.patch("core.celery_app.celery_app.send_task") + create_response = client.post( "/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"}, @@ -408,12 +541,20 @@ def test_delete_event_success(db: Session, client: TestClient) -> None: event_in_db = db.query(CalendarEvent).filter(CalendarEvent.id == event_id).first() assert event_in_db is not None + # Mock the cancel_event_notifications function to prevent Redis call + mock_cancel_notifications = mocker.patch( + "modules.calendar.service.cancel_event_notifications" # Target the function as used in service.py + ) + response = client.delete( f"/api/calendar/events/{event_id}", headers={"Authorization": f"Bearer {access_token}"}, ) assert response.status_code == status.HTTP_204_NO_CONTENT + # Assert that cancel_event_notifications was called + mock_cancel_notifications.assert_called_once_with(event_id) + # Verify event is deleted from DB event_in_db = db.query(CalendarEvent).filter(CalendarEvent.id == event_id).first() assert event_in_db is None @@ -441,20 +582,31 @@ def test_delete_event_not_found(db: Session, client: TestClient) -> None: assert response.status_code == status.HTTP_404_NOT_FOUND -def test_delete_event_forbidden(db: Session, client: TestClient) -> None: +def test_delete_event_forbidden( + db: Session, client: TestClient, mocker: MockerFixture +) -> None: # Add mocker """Test deleting another user's event.""" - user1, password_user1 = generators.create_user(db) - user2, password_user2 = generators.create_user(db) + user1, password_user1 = generators.create_user( + db, username="user1_forbidden_delete" + ) # Unique username + user2, password_user2 = generators.create_user( + db, username="user2_forbidden_delete" + ) # Unique username # Log in as user1 and create an event login_rsp1 = generators.login(db, user1.username, password_user1) access_token1 = login_rsp1["access_token"] payload = create_event_payload() + + # Mock celery task for creation + mocker.patch("core.celery_app.celery_app.send_task") + create_response = client.post( "/api/calendar/events", headers={"Authorization": f"Bearer {access_token1}"}, json=payload, ) + assert create_response.status_code == status.HTTP_201_CREATED event_id = create_response.json()["id"] # Log in as user2 and try to delete user1's event