from unittest.mock import patch import pytest from django.urls import reverse from django.utils import timezone from rest_framework import status from rest_framework.test import APIClient from apps.schedules.models import CustomOnCallShift, OnCallScheduleWeb, ShiftSwapRequest from common.api_helpers.utils import serialize_datetime_as_utc_timestamp from common.insight_log import EntityEvent @pytest.fixture def setup_swap(make_user_for_organization, make_schedule, make_shift_swap_request): def _setup_swap(organization, **kwargs): user = make_user_for_organization(organization) schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb) today = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) tomorrow = today + timezone.timedelta(days=1) two_days_from_now = tomorrow + timezone.timedelta(days=1) swap = make_shift_swap_request(schedule, user, swap_start=tomorrow, swap_end=two_days_from_now) return swap return _setup_swap def assert_swap_response(response, request_data): response_data = response.json() swap = ShiftSwapRequest.objects.get(public_primary_key=response_data["id"]) # check description assert swap.description == response_data["description"] if "description" in request_data: assert response_data["description"] == request_data["description"] # check datetime fields for field in ("swap_start", "swap_end"): db_value = serialize_datetime_as_utc_timestamp(getattr(swap, field)) assert db_value == response_data[field] if field in request_data: assert db_value == request_data[field] # check FK fields for field in ("schedule", "beneficiary", "benefactor"): value = response_data[field] if value: assert getattr(swap, field).public_primary_key == response_data[field] else: assert getattr(swap, field) is None if field in request_data: assert value == request_data[field] @pytest.mark.django_db def test_list_filters( make_organization_and_user_with_token, make_user_for_organization, make_schedule, make_shift_swap_request, ): organization, user, token = make_organization_and_user_with_token() user2 = make_user_for_organization(organization) schedule1 = make_schedule(organization, schedule_class=OnCallScheduleWeb) schedule2 = make_schedule(organization, schedule_class=OnCallScheduleWeb) today = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) yesterday = today - timezone.timedelta(days=1) tomorrow = today + timezone.timedelta(days=1) two_days_from_now = tomorrow + timezone.timedelta(days=1) # open swap1 = make_shift_swap_request(schedule1, user, swap_start=tomorrow, swap_end=two_days_from_now) # past due swap2 = make_shift_swap_request(schedule1, user2, swap_start=yesterday, swap_end=today) # past due / in-progress swap3 = make_shift_swap_request(schedule2, user2, swap_start=today, swap_end=tomorrow) # taken swap4 = make_shift_swap_request(schedule2, user2, swap_start=tomorrow, swap_end=two_days_from_now, benefactor=user) def assert_expected(response, expected): assert response.status_code == status.HTTP_200_OK returned = [s["id"] for s in response.json().get("results", [])] assert sorted(returned) == sorted([s.public_primary_key for s in expected]) client = APIClient() base_url = reverse("api-public:shift_swap-list") url = base_url response = client.get(url, format="json", HTTP_AUTHORIZATION=f"{token}") assert response.status_code == status.HTTP_200_OK assert_expected(response, (swap1, swap4)) url = base_url + f"?schedule_id={schedule1.public_primary_key}" response = client.get(url, format="json", HTTP_AUTHORIZATION=f"{token}") assert response.status_code == status.HTTP_200_OK assert_expected(response, (swap1,)) url = base_url + "?open_only=true" response = client.get(url, format="json", HTTP_AUTHORIZATION=f"{token}") assert response.status_code == status.HTTP_200_OK assert_expected(response, (swap1,)) starting_after = serialize_datetime_as_utc_timestamp(yesterday) url = base_url + f"?beneficiary={user2.public_primary_key}&starting_after={starting_after}" response = client.get(url, format="json", HTTP_AUTHORIZATION=f"{token}") assert response.status_code == status.HTTP_200_OK assert_expected(response, (swap2, swap3, swap4)) url = base_url + f"?benefactor={user.public_primary_key}" response = client.get(url, format="json", HTTP_AUTHORIZATION=f"{token}") assert response.status_code == status.HTTP_200_OK assert_expected(response, (swap4,)) @patch("apps.schedules.models.shift_swap_request.write_resource_insight_log") @patch("apps.schedules.tasks.shift_swaps.create_shift_swap_request_message") @pytest.mark.django_db def test_create( mock_create_shift_swap_request_message, mock_write_resource_insight_log, make_organization_and_user_with_token, make_user_for_organization, make_schedule, ): organization, _, token = make_organization_and_user_with_token() another_user = make_user_for_organization(organization) schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb) today = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) tomorrow = today + timezone.timedelta(days=1) two_days_from_now = tomorrow + timezone.timedelta(days=1) data = { "schedule": schedule.public_primary_key, "description": "Taking a few days off", "swap_start": serialize_datetime_as_utc_timestamp(tomorrow), "swap_end": serialize_datetime_as_utc_timestamp(two_days_from_now), "beneficiary": another_user.public_primary_key, } client = APIClient() url = reverse("api-public:shift_swap-list") response = client.post(url, data, format="json", HTTP_AUTHORIZATION=f"{token}") assert response.status_code == status.HTTP_201_CREATED assert_swap_response(response, data) ssr = ShiftSwapRequest.objects.get(public_primary_key=response.json()["id"]) mock_write_resource_insight_log.assert_called_once_with( instance=ssr, author=another_user, event=EntityEvent.CREATED ) mock_create_shift_swap_request_message.apply_async.assert_called_once_with((ssr.pk,)) @pytest.mark.django_db def test_create_requires_beneficiary( make_organization_and_user_with_token, make_schedule, ): organization, _, token = make_organization_and_user_with_token() schedule = make_schedule(organization, schedule_class=OnCallScheduleWeb) today = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) tomorrow = today + timezone.timedelta(days=1) two_days_from_now = tomorrow + timezone.timedelta(days=1) data = { "schedule": schedule.public_primary_key, "description": "Taking a few days off", "swap_start": serialize_datetime_as_utc_timestamp(tomorrow), "swap_end": serialize_datetime_as_utc_timestamp(two_days_from_now), } client = APIClient() url = reverse("api-public:shift_swap-list") response = client.post(url, data, format="json", HTTP_AUTHORIZATION=f"{token}") assert response.status_code == status.HTTP_400_BAD_REQUEST assert ShiftSwapRequest.objects.count() == 0 @patch("apps.api.views.shift_swap.write_resource_insight_log") @patch("apps.api.views.shift_swap.update_shift_swap_request_message") @pytest.mark.django_db def test_update( mock_update_shift_swap_request_message, mock_write_resource_insight_log, make_organization_and_user_with_token, setup_swap, ): organization, user, token = make_organization_and_user_with_token() swap = setup_swap(organization) assert swap.description is None insights_log_prev_state = swap.insight_logs_serialized data = { "description": "Taking a few days off", "schedule": swap.schedule.public_primary_key, "swap_start": serialize_datetime_as_utc_timestamp(swap.swap_start), "swap_end": serialize_datetime_as_utc_timestamp(swap.swap_end), } client = APIClient() url = reverse("api-public:shift_swap-detail", kwargs={"pk": swap.public_primary_key}) response = client.put(url, data, format="json", HTTP_AUTHORIZATION=f"{token}") assert response.status_code == status.HTTP_200_OK assert_swap_response(response, data) swap.refresh_from_db() mock_write_resource_insight_log.assert_called_once_with( instance=swap, author=user, event=EntityEvent.UPDATED, prev_state=insights_log_prev_state, new_state=swap.insight_logs_serialized, ) mock_update_shift_swap_request_message.apply_async.assert_called_once_with((swap.pk,)) @patch("apps.api.views.shift_swap.write_resource_insight_log") @patch("apps.api.views.shift_swap.update_shift_swap_request_message") @pytest.mark.django_db def test_partial_update( mock_update_shift_swap_request_message, mock_write_resource_insight_log, make_organization_and_user_with_token, setup_swap, ): organization, user, token = make_organization_and_user_with_token() swap = setup_swap(organization) assert swap.description is None insights_log_prev_state = swap.insight_logs_serialized data = {"description": "Taking a few days off"} client = APIClient() url = reverse("api-public:shift_swap-detail", kwargs={"pk": swap.public_primary_key}) response = client.patch(url, data, format="json", HTTP_AUTHORIZATION=f"{token}") assert response.status_code == status.HTTP_200_OK assert_swap_response(response, data) swap.refresh_from_db() mock_write_resource_insight_log.assert_called_once_with( instance=swap, author=user, event=EntityEvent.UPDATED, prev_state=insights_log_prev_state, new_state=swap.insight_logs_serialized, ) mock_update_shift_swap_request_message.apply_async.assert_called_once_with((swap.pk,)) @pytest.mark.django_db def test_details( make_organization_and_user_with_token, make_on_call_shift, setup_swap, ): organization, _, token = make_organization_and_user_with_token() swap = setup_swap(organization) schedule = swap.schedule user = swap.beneficiary today = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) start = today + timezone.timedelta(days=1) duration = timezone.timedelta(hours=8) data = { "start": start, "rotation_start": start, "duration": duration, "priority_level": 1, "frequency": CustomOnCallShift.FREQUENCY_DAILY, "schedule": schedule, } on_call_shift = make_on_call_shift( organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data ) on_call_shift.add_rolling_users([[user]]) client = APIClient() url = reverse("api-public:shift_swap-detail", kwargs={"pk": swap.public_primary_key}) response = client.get(url, HTTP_AUTHORIZATION=f"{token}") assert response.status_code == status.HTTP_200_OK assert_swap_response(response, {}) # include involved shifts information shifts_data = response.json()["shifts"] assert len(shifts_data) == 1 expected = [ # start, end, user, swap request ID ( start.strftime("%Y-%m-%dT%H:%M:%SZ"), (start + duration).strftime("%Y-%m-%dT%H:%M:%SZ"), user.public_primary_key, swap.public_primary_key, ), ] returned_events = [ (e["start"], e["end"], e["users"][0]["pk"], e["users"][0]["swap_request"]["pk"]) for e in shifts_data ] assert returned_events == expected @patch("apps.api.views.shift_swap.write_resource_insight_log") @patch("apps.api.views.shift_swap.update_shift_swap_request_message") @pytest.mark.django_db def test_delete( mock_update_shift_swap_request_message, mock_write_resource_insight_log, make_organization_and_user_with_token, setup_swap, ): organization, user, token = make_organization_and_user_with_token() swap = setup_swap(organization) client = APIClient() url = reverse("api-public:shift_swap-detail", kwargs={"pk": swap.public_primary_key}) response = client.delete(url, HTTP_AUTHORIZATION=f"{token}") assert response.status_code == status.HTTP_204_NO_CONTENT response = client.get(url, HTTP_AUTHORIZATION=f"{token}") assert response.status_code == status.HTTP_404_NOT_FOUND mock_write_resource_insight_log.assert_called_once_with( instance=swap, author=user, event=EntityEvent.DELETED, ) mock_update_shift_swap_request_message.apply_async.assert_called_once_with((swap.pk,)) @pytest.mark.django_db def test_take(make_organization_and_user_with_token, make_user_for_organization, setup_swap): organization, _, token = make_organization_and_user_with_token() another_user = make_user_for_organization(organization) swap = setup_swap(organization) client = APIClient() url = reverse("api-public:shift_swap-take", kwargs={"pk": swap.public_primary_key}) data = {"benefactor": another_user.public_primary_key} response = client.post(url, data, format="json", HTTP_AUTHORIZATION=f"{token}") assert response.status_code == status.HTTP_200_OK assert_swap_response(response, data) swap.refresh_from_db() assert swap.status == ShiftSwapRequest.Statuses.TAKEN assert swap.benefactor == another_user @pytest.mark.django_db def test_take_requires_benefactor(make_organization_and_user_with_token, setup_swap): organization, _, token = make_organization_and_user_with_token() swap = setup_swap(organization) client = APIClient() url = reverse("api-public:shift_swap-take", kwargs={"pk": swap.public_primary_key}) data = {} response = client.post(url, data, format="json", HTTP_AUTHORIZATION=f"{token}") assert response.status_code == status.HTTP_400_BAD_REQUEST swap.refresh_from_db() assert swap.status == ShiftSwapRequest.Statuses.OPEN assert swap.benefactor is None @pytest.mark.django_db def test_take_errors(make_organization_and_user_with_token, make_user_for_organization, setup_swap): organization, _, token = make_organization_and_user_with_token() another_user = make_user_for_organization(organization) swap = setup_swap(organization) client = APIClient() url = reverse("api-public:shift_swap-take", kwargs={"pk": swap.public_primary_key}) # same user taking the swap data = {"benefactor": swap.beneficiary.public_primary_key} response = client.post(url, data, format="json", HTTP_AUTHORIZATION=f"{token}") assert response.status_code == status.HTTP_400_BAD_REQUEST # already taken swap.take(another_user) data = {"benefactor": another_user.public_primary_key} response = client.post(url, data, format="json", HTTP_AUTHORIZATION=f"{token}") assert response.status_code == status.HTTP_400_BAD_REQUEST # deleted swap = setup_swap(organization) swap.delete() data = {"benefactor": another_user.public_primary_key} response = client.post(url, data, format="json", HTTP_AUTHORIZATION=f"{token}") assert response.status_code == status.HTTP_400_BAD_REQUEST # past due swap = setup_swap(organization) swap.swap_start = timezone.now() - timezone.timedelta(days=2) swap.save() data = {"benefactor": another_user.public_primary_key} response = client.post(url, data, format="json", HTTP_AUTHORIZATION=f"{token}") assert response.status_code == status.HTTP_400_BAD_REQUEST