From b94f03197891c5ec6081c056cab547c88520d3c3 Mon Sep 17 00:00:00 2001 From: Adams Pierre David <57180807+adamspd@users.noreply.github.com> Date: Sun, 21 Apr 2024 20:46:24 +0200 Subject: [PATCH 1/7] Remove all tests to start over with a clean state --- appointment/tests/models/__init__.py | 0 .../test_appointment_reschedule_history.py | 64 - .../tests/models/test_model_appointment.py | 269 ---- .../models/test_model_appointment_request.py | 168 --- appointment/tests/models/test_model_config.py | 84 -- .../tests/models/test_model_day_off.py | 53 - .../models/test_model_email_verification.py | 61 - .../models/test_model_password_reset_token.py | 187 --- .../tests/models/test_model_payment_info.py | 64 - .../tests/models/test_model_service.py | 254 ---- .../tests/models/test_model_staff_member.py | 180 --- .../tests/models/test_model_working_hours.py | 93 -- appointment/tests/test_availability_slot.py | 59 - appointment/tests/test_services.py | 730 ----------- appointment/tests/test_settings.py | 45 - appointment/tests/test_tasks.py | 44 - appointment/tests/test_utils.py | 246 ---- appointment/tests/test_views.py | 1108 ---------------- appointment/tests/utils/__init__.py | 0 appointment/tests/utils/test_date_time.py | 397 ------ appointment/tests/utils/test_db_helpers.py | 1145 ----------------- appointment/tests/utils/test_email_ops.py | 170 --- appointment/tests/utils/test_json_context.py | 68 - appointment/tests/utils/test_permissions.py | 60 - appointment/tests/utils/test_session.py | 94 -- 25 files changed, 5643 deletions(-) delete mode 100644 appointment/tests/models/__init__.py delete mode 100644 appointment/tests/models/test_appointment_reschedule_history.py delete mode 100644 appointment/tests/models/test_model_appointment.py delete mode 100644 appointment/tests/models/test_model_appointment_request.py delete mode 100644 appointment/tests/models/test_model_config.py delete mode 100644 appointment/tests/models/test_model_day_off.py delete mode 100644 appointment/tests/models/test_model_email_verification.py delete mode 100644 appointment/tests/models/test_model_password_reset_token.py delete mode 100644 appointment/tests/models/test_model_payment_info.py delete mode 100644 appointment/tests/models/test_model_service.py delete mode 100644 appointment/tests/models/test_model_staff_member.py delete mode 100644 appointment/tests/models/test_model_working_hours.py delete mode 100644 appointment/tests/test_availability_slot.py delete mode 100644 appointment/tests/test_services.py delete mode 100644 appointment/tests/test_settings.py delete mode 100644 appointment/tests/test_tasks.py delete mode 100644 appointment/tests/test_utils.py delete mode 100644 appointment/tests/test_views.py delete mode 100644 appointment/tests/utils/__init__.py delete mode 100644 appointment/tests/utils/test_date_time.py delete mode 100644 appointment/tests/utils/test_db_helpers.py delete mode 100644 appointment/tests/utils/test_email_ops.py delete mode 100644 appointment/tests/utils/test_json_context.py delete mode 100644 appointment/tests/utils/test_permissions.py delete mode 100644 appointment/tests/utils/test_session.py diff --git a/appointment/tests/models/__init__.py b/appointment/tests/models/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/appointment/tests/models/test_appointment_reschedule_history.py b/appointment/tests/models/test_appointment_reschedule_history.py deleted file mode 100644 index 702d44d..0000000 --- a/appointment/tests/models/test_appointment_reschedule_history.py +++ /dev/null @@ -1,64 +0,0 @@ -from datetime import timedelta - -from django.core.exceptions import ValidationError -from django.utils import timezone - -from appointment.models import AppointmentRescheduleHistory -from appointment.tests.base.base_test import BaseTest - - -class AppointmentRescheduleHistoryTestCase(BaseTest): - def setUp(self): - super().setUp() - self.appointment_request = self.create_appt_request_for_sm1() - - def test_successful_creation(self): - reschedule_history = AppointmentRescheduleHistory.objects.create( - appointment_request=self.appointment_request, - date=timezone.now().date() + timedelta(days=1), # Future date - start_time=timezone.now().time(), - end_time=(timezone.now() + timedelta(hours=1)).time(), - staff_member=self.staff_member1, - reason_for_rescheduling="Client request", - reschedule_status='pending' - ) - self.assertIsNotNone(reschedule_history.id_request) # Auto-generated id_request - self.assertTrue(reschedule_history.still_valid()) - - def test_date_in_past_validation(self): - with self.assertRaises(ValidationError): - AppointmentRescheduleHistory.objects.create( - appointment_request=self.appointment_request, - date=timezone.now().date() - timedelta(days=1), # Past date - start_time=timezone.now().time(), - end_time=(timezone.now() + timedelta(hours=1)).time(), - staff_member=self.staff_member1 - ) - - def test_invalid_date_validation(self): - with self.assertRaises(TypeError): - AppointmentRescheduleHistory.objects.create( - appointment_request=self.appointment_request, - date="invalid-date", # Invalid date format - start_time=timezone.now().time(), - end_time=(timezone.now() + timedelta(hours=1)).time(), - staff_member=self.staff_member1 - ) - - def test_still_valid(self): - reschedule_history = AppointmentRescheduleHistory.objects.create( - appointment_request=self.appointment_request, - date=timezone.now().date() + timedelta(days=1), - start_time=timezone.now().time(), - end_time=(timezone.now() + timedelta(hours=1)).time(), - staff_member=self.staff_member1, - reason_for_rescheduling="Client request", - reschedule_status='pending' - ) - # Directly test the still_valid method - self.assertTrue(reschedule_history.still_valid()) - - # Simulate passages of time beyond the validity window - reschedule_history.created_at -= timedelta(minutes=6) - reschedule_history.save() - self.assertFalse(reschedule_history.still_valid()) diff --git a/appointment/tests/models/test_model_appointment.py b/appointment/tests/models/test_model_appointment.py deleted file mode 100644 index 7c40028..0000000 --- a/appointment/tests/models/test_model_appointment.py +++ /dev/null @@ -1,269 +0,0 @@ -from datetime import datetime, time, timedelta - -from django.core.exceptions import ValidationError -from django.db import IntegrityError -from django.utils import timezone - -from appointment.models import Appointment, DayOff, WorkingHours -from appointment.tests.base.base_test import BaseTest -from appointment.utils.date_time import get_weekday_num - - -class AppointmentModelTestCase(BaseTest): - def setUp(self): - super().setUp() - self.ar = self.create_appt_request_for_sm1() - self.appointment = self.create_appointment_for_user1(appointment_request=self.ar) - - # Test appointment creation - def test_appointment_creation(self): - """Test if an appointment can be created.""" - appointment = Appointment.objects.get(appointment_request=self.ar) - self.assertIsNotNone(appointment) - self.assertEqual(appointment.client, self.client1) - self.assertEqual(appointment.phone, "1234567890") - self.assertEqual(appointment.address, "Some City, Some State") - - # Test str representation - def test_str_representation(self): - """Test if an appointment's string representation is correct.""" - expected_str = f"{self.client1} - {self.ar.start_time.strftime('%Y-%m-%d %H:%M')} to " \ - f"{self.ar.end_time.strftime('%Y-%m-%d %H:%M')}" - self.assertEqual(str(self.appointment), expected_str) - - # Test start time - def test_get_start_time(self): - """Test if an appointment's start time is correct.""" - expected_start_time = datetime.combine(self.ar.date, - self.ar.start_time) - self.assertEqual(self.appointment.get_start_time(), expected_start_time) - - # Test end time - def test_get_end_time(self): - """Test if an appointment's end time is correct.""" - expected_end_time = datetime.combine(self.ar.date, - self.ar.end_time) - self.assertEqual(self.appointment.get_end_time(), expected_end_time) - - # Test service name retrieval - def test_get_service_name(self): - """Test if an appointment's service name is correct.""" - self.assertEqual(self.appointment.get_service_name(), "Test Service") - - # Test service price retrieval - def test_get_service_price(self): - """Test if an appointment's service price is correct.""" - self.assertEqual(self.appointment.get_service_price(), 100) - - # Test phone retrieval - def test_get_phone(self): - """Test if an appointment's phone number is correct.""" - self.assertEqual(self.appointment.phone, "1234567890") - - # Test address retrieval - def test_get_address(self): - """Test if an appointment's address is correct.""" - self.assertEqual(self.appointment.address, "Some City, Some State") - - # Test reminder retrieval - def test_get_want_reminder(self): - """Test if an appointment's reminder status is correct.""" - self.assertFalse(self.appointment.want_reminder) - - # Test additional info retrieval - def test_get_additional_info(self): - """Test if an appointment's additional info is correct.""" - self.assertIsNone(self.appointment.additional_info) - - # Test paid status retrieval - def test_is_paid(self): - """Test if an appointment's paid status is correct.""" - self.assertFalse(self.appointment.is_paid()) - - def test_is_paid_text(self): - """Test if an appointment's paid status is correct.""" - self.assertEqual(self.appointment.is_paid_text(), "No") - - # Test appointment amount to pay - def test_get_appointment_amount_to_pay(self): - """Test if an appointment's amount to pay is correct.""" - self.assertEqual(self.appointment.get_appointment_amount_to_pay(), 100) - - # Test appointment currency retrieval - def test_get_appointment_currency(self): - """Test if an appointment's currency is correct.""" - self.assertEqual(self.appointment.get_appointment_currency(), "USD") - - # Test appointment ID request retrieval - def test_get_appointment_id_request(self): - """Test if an appointment's ID request is correct.""" - self.assertIsNotNone(self.appointment.get_appointment_id_request()) - - # Test created at retrieval - def test_created_at(self): - """Test if an appointment's created at date is correct.""" - self.assertIsNotNone(self.appointment.created_at) - - # Test updated at retrieval - def test_updated_at(self): - """Test if an appointment's updated at date is correct.""" - self.assertIsNotNone(self.appointment.updated_at) - - # Test paid status setting - def test_set_appointment_paid_status(self): - """Test if an appointment's paid status can be set.""" - self.appointment.set_appointment_paid_status(True) - self.assertTrue(self.appointment.is_paid()) - self.appointment.set_appointment_paid_status(False) - self.assertFalse(self.appointment.is_paid()) - - # Test invalid phone number - def test_invalid_phone(self): - """Test that an appointment cannot be created with an invalid phone number.""" - self.appointment.phone = "1234" # Invalid phone number - with self.assertRaises(ValidationError): - self.appointment.full_clean() - - # Test service down payment retrieval - def test_get_service_down_payment(self): - """Test if an appointment's service down payment is correct.""" - self.assertEqual(self.appointment.get_service_down_payment(), self.service1.get_down_payment()) - - # Test service description retrieval - def test_service_description(self): - """Test if an appointment's service description is correct.""" - self.assertEqual(self.appointment.get_service_description(), self.service1.description) - - # Test appointment date retrieval - def test_get_appointment_date(self): - """Test if an appointment's date is correct.""" - self.assertEqual(self.appointment.get_appointment_date(), self.ar.date) - - # Test save function with down payment type - def test_save_with_down_payment(self): - """Test if an appointment can be saved with a down payment.""" - self.ar.payment_type = 'down' - self.ar.save() - self.appointment.save() - self.assertEqual(self.appointment.get_service_down_payment(), self.service1.get_down_payment()) - - def test_appointment_without_appointment_request(self): - """Test that an appointment cannot be created without an appointment request.""" - with self.assertRaises(ValidationError): # Assuming model validation prevents this - Appointment.objects.create(client=self.client1) - - def test_appointment_without_client(self): - """Test that an appointment cannot be created without a client.""" - with self.assertRaises(IntegrityError): # Assuming model validation prevents this - Appointment.objects.create(appointment_request=self.ar) - - def test_appointment_amount_to_pay_calculation(self): - """ - Test if an appointment's amount_to_pay field is correctly calculated based on the associated AppointmentRequest. - """ - self.assertEqual(self.appointment.get_appointment_amount_to_pay(), self.ar.get_service_price()) - - def test_update_appointment_paid_status(self): - """Simulate appointment's paid status being updated.""" - self.appointment.set_appointment_paid_status(True) - self.assertTrue(self.appointment.is_paid()) - self.appointment.set_appointment_paid_status(False) - self.assertFalse(self.appointment.is_paid()) - - def test_appointment_rescheduling(self): - """Simulate appointment rescheduling by changing the appointment date and times.""" - new_date = self.ar.date + timedelta(days=1) - new_start_time = time(10, 0) - new_end_time = time(11, 0) - self.ar.date = new_date - self.ar.start_time = new_start_time - self.ar.end_time = new_end_time - self.ar.save() - - self.assertEqual(self.appointment.get_date(), new_date) - self.assertEqual(self.appointment.get_start_time().time(), new_start_time) - self.assertEqual(self.appointment.get_end_time().time(), new_end_time) - - def test_create_appointment_without_required_fields(self): - """Test that an appointment cannot be created without the required fields.""" - with self.assertRaises(ValidationError): - Appointment.objects.create() - - def test_get_service_duration(self): - """Test if an appointment's service duration is correct.""" - self.assertEqual(self.appointment.get_service_duration(), "1 hour") - - def test_appt_to_dict(self): - response = { - 'id': 1, - 'client_name': 'Client1', - 'client_email': 'client1@gmail.com', - 'start_time': '1900-01-01 09:00', - 'end_time': '1900-01-01 10:00', - 'service_name': 'Test Service', - 'address': 'Some City, Some State', - 'want_reminder': False, - 'additional_info': None, - 'paid': False, - 'amount_to_pay': 100, - } - actual_response = self.appointment.to_dict() - actual_response.pop('id_request', None) - self.assertEqual(actual_response, response) - - def test_get_staff_member_name_with_staff_member(self): - """Test if you get_staff_member_name method returns the correct name when a staff member is associated.""" - expected_name = self.staff_member1.get_staff_member_name() - actual_name = self.appointment.get_staff_member_name() - self.assertEqual(actual_name, expected_name) - - def test_get_staff_member_name_without_staff_member(self): - """Test if you get_staff_member_name method returns an empty string when no staff member is associated.""" - self.appointment.appointment_request.staff_member = None - self.appointment.appointment_request.save() - self.assertEqual(self.appointment.get_staff_member_name(), "") - - def test_get_service_img_url_no_image(self): - """Service should handle cases where no image is provided gracefully.""" - self.assertEqual(self.appointment.get_service_img_url(), "") - - -class AppointmentValidDateTestCase(BaseTest): - def setUp(self): - super().setUp() - self.weekday = "Monday" # Example weekday - self.weekday_num = get_weekday_num(self.weekday) - WorkingHours.objects.create(staff_member=self.staff_member1, day_of_week=self.weekday_num, - start_time=time(9, 0), - end_time=time(17, 0)) - self.appt_date = timezone.now().date() + timedelta(days=(self.weekday_num - timezone.now().weekday()) % 7) - self.start_time = timezone.now().replace(hour=10, minute=0, second=0, microsecond=0) - self.current_appointment_id = None - - def test_staff_member_works_on_given_day(self): - is_valid, message = Appointment.is_valid_date(self.appt_date, self.start_time, self.staff_member1, - self.current_appointment_id, self.weekday) - self.assertTrue(is_valid) - - def test_staff_member_does_not_work_on_given_day(self): - non_working_day = "Sunday" - non_working_day_num = get_weekday_num(non_working_day) - appt_date = self.appt_date + timedelta(days=(non_working_day_num - self.weekday_num) % 7) - is_valid, message = Appointment.is_valid_date(appt_date, self.start_time, self.staff_member1, - self.current_appointment_id, non_working_day) - self.assertFalse(is_valid) - self.assertIn("does not work on this day", message) - - def test_start_time_outside_working_hours(self): - early_start_time = timezone.now().replace(hour=8, minute=0) # Before working hours - is_valid, message = Appointment.is_valid_date(self.appt_date, early_start_time, self.staff_member1, - self.current_appointment_id, self.weekday) - self.assertFalse(is_valid) - self.assertIn("outside of", message) - - def test_staff_member_has_day_off(self): - DayOff.objects.create(staff_member=self.staff_member1, start_date=self.appt_date, end_date=self.appt_date) - is_valid, message = Appointment.is_valid_date(self.appt_date, self.start_time, self.staff_member1, - self.current_appointment_id, self.weekday) - self.assertFalse(is_valid) - self.assertIn("has a day off on this date", message) diff --git a/appointment/tests/models/test_model_appointment_request.py b/appointment/tests/models/test_model_appointment_request.py deleted file mode 100644 index 134e9aa..0000000 --- a/appointment/tests/models/test_model_appointment_request.py +++ /dev/null @@ -1,168 +0,0 @@ -import datetime -from datetime import date, time, timedelta - -from django.core.exceptions import ValidationError - -from appointment.tests.base.base_test import BaseTest - - -class AppointmentRequestModelTestCase(BaseTest): - def setUp(self): - super().setUp() - self.ar = self.create_appointment_request_(self.service1, self.staff_member1) - self.today = date.today() - - def test_appointment_request_creation(self): - """Test if an appointment request can be created.""" - self.assertIsNotNone(self.ar) - self.assertEqual(self.ar.start_time, time(9, 0)) - self.assertEqual(self.ar.end_time, time(10, 0)) - - def test_service_name_retrieval(self): - """Test if an appointment request's service name is correct.""" - self.assertEqual(self.ar.get_service_name(), "Test Service") - - def test_service_price_retrieval(self): - self.assertEqual(self.ar.get_service_price(), 100) - - def test_invalid_start_time(self): - """Start time must be before end time""" - self.ar.start_time = time(11, 0) - self.ar.end_time = time(9, 0) - with self.assertRaises(ValidationError): - self.ar.full_clean() - - def test_invalid_payment_type(self): - """Payment type must be either 'full' or 'down'""" - self.ar.payment_type = "invalid" - with self.assertRaises(ValidationError): - self.ar.full_clean() - - def test_get_date(self): - """Test if an appointment request's date is correct.""" - self.assertEqual(self.ar.date, date.today()) - - def test_get_start_time(self): - """Test if an appointment request's start time is correct.""" - self.assertEqual(self.ar.start_time, time(9, 0)) - - def test_get_end_time(self): - """Test if an appointment request's end time is correct.""" - self.assertEqual(self.ar.end_time, time(10, 0)) - - def test_get_service_down_payment(self): - """Test if an appointment request's service down payment is correct.""" - self.assertEqual(self.ar.get_service_down_payment(), 0) - - def test_get_service_image(self): - """ test_get_service_image not implemented yet.""" - # self.assertIsNone(self.ar.get_service_image()) - pass - - def test_get_service_image_url(self): - """test_get_service_image_url's implementation not finished yet.""" - pass - - def test_get_service_description(self): - """Test if an appointment request's service description is correct.""" - self.assertIsNone(self.ar.get_service_description()) - - def test_get_id_request(self): - """Test if an appointment request's ID request is correct.""" - self.assertIsNotNone(self.ar.get_id_request()) - self.assertIsInstance(self.ar.get_id_request(), str) - - def test_is_a_paid_service(self): - """Test if an appointment request's service is a paid service.""" - self.assertTrue(self.ar.is_a_paid_service()) - - def test_accepts_down_payment_false(self): - """Test if an appointment request's service accepts down payment.""" - self.assertFalse(self.ar.accepts_down_payment()) - - def test_get_payment_type(self): - """Test if an appointment request's payment type is correct.""" - self.assertEqual(self.ar.payment_type, 'full') - - def test_created_at(self): - """Test if an appointment request's created at date is correctly set upon creation.""" - self.assertIsNotNone(self.ar.created_at) - - def test_updated_at(self): - """Test if an appointment request's updated at date is correctly set upon creation.""" - self.assertIsNotNone(self.ar.updated_at) - - def test_appointment_with_same_start_and_end_time(self): - """ - Test the situation where an appointment's start time is equal to the end time. - The model will prevent this. - """ - with self.assertRaises(ValidationError): - self.create_appointment_request_(self.service1, self.staff_member1, start_time=time(9, 0), - end_time=time(9, 0)) - - def test_appointment_in_past(self): - """Test that an appointment cannot be created in the past.""" - past_date = date.today() - timedelta(days=1) - with self.assertRaises(ValidationError): - self.create_appointment_request_(self.service1, self.staff_member1, date_=past_date) - - def test_appointment_duration_exceeds_service_time(self): - """Test that an appointment cannot be created with a duration greater than the service duration.""" - long_duration = timedelta(hours=3) - self.service1.duration = long_duration - self.service1.save() - - # Assuming the appointment request uses the service duration - with self.assertRaises(ValidationError): - self.create_appointment_request_(self.service1, self.staff_member1, start_time=time(9, 0), - end_time=time(13, 0)) - - def test_reschedule_attempts_limit(self): - """Test appointment request's ability to be rescheduled based on service's limit.""" - self.service1.reschedule_limit = 2 - self.service1.save() - - # Simulate rescheduling attempts - self.ar.increment_reschedule_attempts() - self.assertTrue(self.ar.can_be_rescheduled()) - - self.ar.increment_reschedule_attempts() - self.assertFalse(self.ar.can_be_rescheduled(), - "Should not be reschedulable after reaching the limit") - - def test_appointment_request_with_invalid_date(self): - """Appointment date should be valid and not in the past.""" - invalid_date = self.today - timedelta(days=1) - with self.assertRaises(ValidationError, msg="Date cannot be in the past"): - self.create_appointment_request_( - self.service1, self.staff_member1, date_=invalid_date, start_time=time(10, 0), end_time=time(11, 0) - ) - with self.assertRaises(ValidationError, msg="The date is not valid"): - date_ = datetime.datetime.strptime("31-03-2021", "%d-%m-%Y").date() - self.create_appointment_request_( - self.service1, self.staff_member1, date_=date_, - start_time=time(10, 0), end_time=time(11, 0) - ) - - def test_start_time_after_end_time(self): - """Start time should not be after end time.""" - with self.assertRaises(ValueError, msg="Start time must be before end time"): - self.create_appointment_request_( - self.service1, self.staff_member1, date_=self.today, start_time=time(11, 0), end_time=time(10, 0) - ) - - def test_start_time_equals_end_time(self): - """Start time and end time should not be the same.""" - with self.assertRaises(ValidationError, msg="Start time and end time cannot be the same"): - self.create_appointment_request_( - self.service1, self.staff_member1, date_=self.today, start_time=time(10, 0), end_time=time(10, 0) - ) - - def test_appointment_duration_not_exceed_service(self): - """Appointment duration should not exceed the service's duration.""" - extended_end_time = time(11, 30) # 2.5 hours, exceeding the 1-hour service duration - with self.assertRaises(ValidationError, msg="Duration cannot exceed the service duration"): - self.create_appointment_request_( - self.service1, self.staff_member1, date_=self.today, start_time=time(9, 0), end_time=extended_end_time - ) diff --git a/appointment/tests/models/test_model_config.py b/appointment/tests/models/test_model_config.py deleted file mode 100644 index 9d5c5f3..0000000 --- a/appointment/tests/models/test_model_config.py +++ /dev/null @@ -1,84 +0,0 @@ -from datetime import time - -from django.core.exceptions import ValidationError -from django.test import TestCase - -from appointment.models import Config - - -class ConfigModelTestCase(TestCase): - def setUp(self): - self.config = Config.objects.create(slot_duration=30, lead_time=time(9, 0), - finish_time=time(17, 0), appointment_buffer_time=2.0, - website_name="My Website") - - def test_config_creation(self): - """Test if a configuration can be created.""" - self.assertIsNotNone(self.config) - self.assertEqual(self.config.slot_duration, 30) - self.assertEqual(self.config.lead_time, time(9, 0)) - self.assertEqual(self.config.finish_time, time(17, 0)) - self.assertEqual(self.config.appointment_buffer_time, 2.0) - self.assertEqual(self.config.website_name, "My Website") - - def test_config_str_method(self): - """Test that the string representation of a configuration is correct.""" - expected_str = f"Config {self.config.pk}: slot_duration=30, lead_time=09:00:00, finish_time=17:00:00" - self.assertEqual(str(self.config), expected_str) - - def test_invalid_slot_duration(self): - """Test that a configuration cannot be created with a negative slot duration.""" - with self.assertRaises(ValidationError): - Config.objects.create(slot_duration=-10, lead_time=time(9, 0), finish_time=time(17, 0)) - - def test_multiple_config_creation(self): - """Test that only one configuration can be created.""" - with self.assertRaises(ValidationError): - Config.objects.create(slot_duration=20, lead_time=time(8, 0), finish_time=time(18, 0)) - - def test_lead_time_greater_than_finish_time(self): - """Test that lead time cannot be greater than finish time.""" - self.config.lead_time = time(18, 0) - self.config.finish_time = time(9, 0) - with self.assertRaises(ValidationError): - self.config.full_clean() - - def test_editing_existing_config(self): - """Test that an existing configuration can be edited.""" - self.config.slot_duration = 40 - self.config.save() - - def test_slot_duration_of_zero(self): - """Test that a configuration cannot be created with a slot duration of zero.""" - with self.assertRaises(ValidationError): - Config.objects.create(slot_duration=0, lead_time=time(9, 0), finish_time=time(17, 0)) - - def test_same_lead_and_finish_time(self): - """Test that a configuration cannot be created with the same lead time and finish time.""" - with self.assertRaises(ValidationError): - Config.objects.create(slot_duration=30, lead_time=time(9, 0), finish_time=time(9, 0)) - - def test_lead_time_one_minute_before_finish_time(self): - """Test that a configuration cannot be created with a lead time one minute before the finish time.""" - with self.assertRaises(ValidationError): - Config.objects.create(slot_duration=30, lead_time=time(8, 59), finish_time=time(9, 0)) - - def test_negative_appointment_buffer_time(self): - """Test that a configuration cannot be created with a negative appointment buffer time.""" - with self.assertRaises(ValidationError): - Config.objects.create(slot_duration=30, lead_time=time(9, 0), finish_time=time(17, 0), - appointment_buffer_time=-2.0) - - def test_update_website_name(self): - """Simulate changing the website name in the configuration.""" - new_name = "Updated Website Name" - self.config.website_name = new_name - self.config.save() - - updated_config = Config.objects.get(pk=self.config.pk) - self.assertEqual(updated_config.website_name, new_name) - - def test_cant_delete_config(self): - """Test that a configuration cannot be deleted.""" - self.config.delete() - self.assertIsNotNone(Config.objects.first()) diff --git a/appointment/tests/models/test_model_day_off.py b/appointment/tests/models/test_model_day_off.py deleted file mode 100644 index 15cebdc..0000000 --- a/appointment/tests/models/test_model_day_off.py +++ /dev/null @@ -1,53 +0,0 @@ -from datetime import date, timedelta - -from django.core.exceptions import ValidationError -from django.db import IntegrityError - -from appointment.models import DayOff -from appointment.tests.base.base_test import BaseTest - - -class DayOffModelTestCase(BaseTest): - def setUp(self): - super().setUp() - self.day_off = DayOff.objects.create( - staff_member=self.staff_member1, - start_date=date.today(), - end_date=date.today() + timedelta(days=1) - ) - - def test_day_off_creation(self): - """Test basic creation of DayOff.""" - self.assertIsNotNone(self.day_off) - self.assertEqual(self.day_off.staff_member, self.staff_member1) - - def test_day_off_start_date_before_end_date(self): - """Test that start date must be before end date upon day off creation.""" - with self.assertRaises(ValidationError): - DayOff.objects.create( - staff_member=self.staff_member1, - start_date=date.today() + timedelta(days=1), - end_date=date.today() - ).clean() - - def test_day_off_is_owner(self): - """Test that is_owner method in day off model works as expected.""" - self.assertTrue(self.day_off.is_owner(self.user1.id)) - self.assertFalse(self.day_off.is_owner(9999)) # Assuming 9999 is not a valid user ID in your tests - - def test_day_off_without_staff_member(self): - """Test that a day off cannot be created without a staff member.""" - with self.assertRaises(IntegrityError): - DayOff.objects.create( - start_date=date.today(), - end_date=date.today() + timedelta(days=1) - ) - - def test_day_off_str_method(self): - """Test that the string representation of a day off is correct.""" - self.assertEqual(str(self.day_off), f"{date.today()} to {date.today() + timedelta(days=1)} - Day off") - - # Testing with a description - self.day_off.description = "Vacation" - self.day_off.save() - self.assertEqual(str(self.day_off), f"{date.today()} to {date.today() + timedelta(days=1)} - Vacation") diff --git a/appointment/tests/models/test_model_email_verification.py b/appointment/tests/models/test_model_email_verification.py deleted file mode 100644 index b967a08..0000000 --- a/appointment/tests/models/test_model_email_verification.py +++ /dev/null @@ -1,61 +0,0 @@ -import string - -from django.test import TestCase - -from appointment.models import EmailVerificationCode -from appointment.tests.mixins.base_mixin import UserMixin - - -class EmailVerificationCodeModelTestCase(TestCase, UserMixin): - def setUp(self): - self.user = self.create_user_() - self.code = EmailVerificationCode.generate_code(self.user) - - def test_code_creation(self): - """Test if a verification code can be generated.""" - verification_code = EmailVerificationCode.objects.get(user=self.user) - self.assertIsNotNone(verification_code) - self.assertEqual(verification_code.code, self.code) - - def test_code_length(self): - """Test that the code is six characters long.""" - self.assertEqual(len(self.code), 6) - - def test_code_content(self): - """Test that the code only contains uppercase letters and digits.""" - valid_characters = set(string.ascii_uppercase + string.digits) - self.assertTrue(all(char in valid_characters for char in self.code)) - - def test_code_str_representation(self): - """Test that the string representation of a verification code is correct.""" - verification_code = EmailVerificationCode.objects.get(user=self.user) - self.assertEqual(str(verification_code), self.code) - - def test_created_at(self): - """Test if the created_at field is set when a code is generated.""" - verification_code = EmailVerificationCode.objects.get(user=self.user) - self.assertIsNotNone(verification_code.created_at) - - def test_updated_at(self): - """Test if the updated_at field is set when a code is generated.""" - verification_code = EmailVerificationCode.objects.get(user=self.user) - self.assertIsNotNone(verification_code.updated_at) - - def test_multiple_codes_for_user(self): - """ - Test if multiple verification codes can be generated for a user. - This should ideally create a new code, but the old one will still exist. - """ - new_code = EmailVerificationCode.generate_code(self.user) - self.assertNotEqual(self.code, new_code) - - def test_code_verification_match(self): - """The check_code method returns True when the code matches.""" - code = EmailVerificationCode.objects.get(user=self.user) - self.assertTrue(code.check_code(self.code)) - - def test_code_verification_mismatch(self): - """The check_code method returns False when the code does not match.""" - mismatched_code = "ABCDEF" - code = EmailVerificationCode.objects.get(user=self.user) - self.assertFalse(code.check_code(mismatched_code)) diff --git a/appointment/tests/models/test_model_password_reset_token.py b/appointment/tests/models/test_model_password_reset_token.py deleted file mode 100644 index c6646ce..0000000 --- a/appointment/tests/models/test_model_password_reset_token.py +++ /dev/null @@ -1,187 +0,0 @@ -import datetime -import time - -from django.utils import timezone - -from appointment.models import PasswordResetToken -from appointment.tests.base.base_test import BaseTest - - -class PasswordResetTokenTests(BaseTest): - def setUp(self): - super().setUp() - self.user = self.create_user_(username='test_user', email='test@example.com', password='test_pass123') - self.expired_time = timezone.now() - datetime.timedelta(minutes=5) - - def test_create_token(self): - """Test token creation for a user.""" - token = PasswordResetToken.create_token(user=self.user) - self.assertIsNotNone(token) - self.assertFalse(token.is_expired) - self.assertFalse(token.is_verified) - - def test_str_representation(self): - """Test the string representation of the token.""" - token = PasswordResetToken.create_token(self.user) - expected_str = (f"Password reset token for {self.user} " - f"[{token.token} status: {token.status} expires at {token.expires_at}]") - self.assertEqual(str(token), expected_str) - - def test_is_verified_property(self): - """Test the is_verified property to check if the token status is correctly identified as verified.""" - token = PasswordResetToken.create_token(self.user) - self.assertFalse(token.is_verified, "Newly created token should not be verified.") - token.mark_as_verified() - self.assertTrue(token.is_verified, "Token should be marked as verified after calling mark_as_verified.") - - def test_is_active_property(self): - """Test the is_active property to check if the token status is correctly identified as active.""" - token = PasswordResetToken.create_token(self.user) - self.assertTrue(token.is_active, "Newly created token should be active.") - token.mark_as_verified() - token.refresh_from_db() - self.assertFalse(token.is_active, "Token should not be active after being verified.") - - # Invalidate the token and check is_active property - token.status = PasswordResetToken.TokenStatus.INVALIDATED - token.save() - self.assertFalse(token.is_active, "Token should not be active after being invalidated.") - - def test_is_invalidated_property(self): - """Test the is_invalidated property to check if the token status is correctly identified as invalidated.""" - token = PasswordResetToken.create_token(self.user) - self.assertFalse(token.is_invalidated, "Newly created token should not be invalidated.") - - # Invalidate the token and check is_invalidated property - token.status = PasswordResetToken.TokenStatus.INVALIDATED - token.save() - self.assertTrue(token.is_invalidated, "Token should be marked as invalidated after status change.") - - def test_token_expiration(self): - """Test that a token is considered expired after the expiration time.""" - token = PasswordResetToken.create_token(user=self.user, expiration_minutes=-1) # Token already expired - self.assertTrue(token.is_expired) - - def test_verify_token_success(self): - """Test successful token verification.""" - token = PasswordResetToken.create_token(user=self.user) - verified_token = PasswordResetToken.verify_token(user=self.user, token=token.token) - self.assertIsNotNone(verified_token) - - def test_verify_token_failure_expired(self): - """Test token verification fails if the token has expired.""" - token = PasswordResetToken.create_token(user=self.user, expiration_minutes=-1) # Token already expired - verified_token = PasswordResetToken.verify_token(user=self.user, token=token.token) - self.assertIsNone(verified_token) - - def test_verify_token_failure_wrong_user(self): - """Test token verification fails if the token does not belong to the given user.""" - another_user = self.create_user_(username='another_user', email='another@example.com', - password='test_pass456') - token = PasswordResetToken.create_token(user=self.user) - verified_token = PasswordResetToken.verify_token(user=another_user, token=token.token) - self.assertIsNone(verified_token) - - def test_verify_token_failure_already_verified(self): - """Test token verification fails if the token has already been verified.""" - token = PasswordResetToken.create_token(user=self.user) - token.mark_as_verified() - verified_token = PasswordResetToken.verify_token(user=self.user, token=token.token) - self.assertIsNone(verified_token) - - def test_mark_as_verified(self): - """Test marking a token as verified.""" - token = PasswordResetToken.create_token(user=self.user) - self.assertFalse(token.is_verified) - token.mark_as_verified() - token.refresh_from_db() # Refresh the token object from the database - self.assertTrue(token.is_verified) - - def test_verify_token_invalid_token(self): - """Test token verification fails if the token does not exist.""" - PasswordResetToken.create_token(user=self.user) - invalid_token_uuid = "12345678-1234-1234-1234-123456789012" # An invalid token UUID - verified_token = PasswordResetToken.verify_token(user=self.user, token=invalid_token_uuid) - self.assertIsNone(verified_token) - - def test_token_expiration_boundary(self): - """Test token verification at the exact moment of expiration.""" - token = PasswordResetToken.create_token(user=self.user, expiration_minutes=0) # Token expires now - # Assuming there might be a very slight delay before verification, we wait a second - time.sleep(1) - verified_token = PasswordResetToken.verify_token(user=self.user, token=token.token) - self.assertIsNone(verified_token) - - def test_create_multiple_tokens_for_user(self): - """Test that multiple tokens can be created for a single user and only the latest is valid.""" - old_token = PasswordResetToken.create_token(user=self.user) - new_token = PasswordResetToken.create_token(user=self.user) - - old_verified = PasswordResetToken.verify_token(user=self.user, token=old_token.token) - new_verified = PasswordResetToken.verify_token(user=self.user, token=new_token.token) - - self.assertIsNone(old_verified, "Old token should not be valid after creating a new one") - self.assertIsNotNone(new_verified, "New token should be valid") - - def test_expired_token_does_not_verify(self): - """Test that an expired token does not verify even if correct.""" - token = PasswordResetToken.create_token(user=self.user, expiration_minutes=-5) # Already expired - # Fast-forward time to after expiration - token.expires_at = timezone.now() - datetime.timedelta(minutes=5) - token.save() - - verified_token = PasswordResetToken.verify_token(user=self.user, token=token.token) - self.assertIsNone(verified_token, "Expired token should not verify") - - def test_mark_as_verified_is_idempotent(self): - """Test that marking a token as verified multiple times has no adverse effect.""" - token = PasswordResetToken.create_token(user=self.user) - token.mark_as_verified() - first_verification_time = token.updated_at - - time.sleep(1) # Ensure time has passed - token.mark_as_verified() - token.refresh_from_db() - - self.assertTrue(token.is_verified) - self.assertEqual(first_verification_time, token.updated_at, - "Token verification time should not update on subsequent calls") - - def test_deleting_user_cascades_to_tokens(self): - """Test that deleting a user deletes associated password reset tokens.""" - token = PasswordResetToken.create_token(user=self.user) - self.user.delete() - - with self.assertRaises(PasswordResetToken.DoesNotExist): - PasswordResetToken.objects.get(pk=token.pk) - - def test_token_verification_resets_after_expiration(self): - """Test that an expired token cannot be verified after its expiration, even if marked as verified.""" - token = PasswordResetToken.create_token(user=self.user, expiration_minutes=-1) # Already expired - token.mark_as_verified() - - verified_token = PasswordResetToken.verify_token(user=self.user, token=token.token) - self.assertIsNone(verified_token, "Expired token should not verify, even if marked as verified") - - def test_verify_token_invalidated(self): - """Test token verification fails if the token has been invalidated.""" - token = PasswordResetToken.create_token(self.user) - # Invalidate the token by creating a new one - PasswordResetToken.create_token(self.user) - verified_token = PasswordResetToken.verify_token(self.user, token.token) - self.assertIsNone(verified_token) - - def test_expired_token_verification(self): - """Test that an expired token cannot be verified.""" - token = PasswordResetToken.objects.create(user=self.user, expires_at=self.expired_time, - status=PasswordResetToken.TokenStatus.ACTIVE) - self.assertTrue(token.is_expired) - verified_token = PasswordResetToken.verify_token(self.user, token.token) - self.assertIsNone(verified_token, "Expired token should not verify") - - def test_token_verification_after_user_deletion(self): - """Test that a token cannot be verified after the associated user is deleted.""" - token = PasswordResetToken.create_token(self.user) - self.user.delete() - verified_token = PasswordResetToken.verify_token(None, token.token) - self.assertIsNone(verified_token, "Token should not verify after user deletion") diff --git a/appointment/tests/models/test_model_payment_info.py b/appointment/tests/models/test_model_payment_info.py deleted file mode 100644 index 2fe88d3..0000000 --- a/appointment/tests/models/test_model_payment_info.py +++ /dev/null @@ -1,64 +0,0 @@ -from appointment.models import PaymentInfo -from appointment.tests.base.base_test import BaseTest - - -class PaymentInfoModelTestCase(BaseTest): - def setUp(self): - super().setUp() - self.ar = self.create_appt_request_for_sm1() - self.appointment = self.create_appointment_for_user1(appointment_request=self.ar) - self.payment_info = PaymentInfo.objects.create(appointment=self.appointment) - - def test_payment_info_creation(self): - """Test if a payment info can be created.""" - payment_info = PaymentInfo.objects.get(appointment=self.appointment) - self.assertIsNotNone(payment_info) - self.assertEqual(payment_info.appointment, self.appointment) - - def test_str_representation(self): - """Test if a payment info's string representation is correct.""" - self.assertEqual(str(self.payment_info), f"{self.service1.name} - {self.service1.price}") - - def test_get_id_request(self): - """Test if a payment info's id request is correct.""" - self.assertEqual(self.payment_info.get_id_request(), self.appointment.get_appointment_id_request()) - - def test_get_amount_to_pay(self): - """Test if a payment info's amount to pay is correct.""" - self.assertEqual(self.payment_info.get_amount_to_pay(), self.appointment.get_appointment_amount_to_pay()) - - def test_get_currency(self): - """Test if a payment info's currency is correct.""" - self.assertEqual(self.payment_info.get_currency(), self.appointment.get_appointment_currency()) - - def test_get_name(self): - """Test if payment info's name is correct.""" - self.assertEqual(self.payment_info.get_name(), self.appointment.get_service_name()) - - def test_get_img_url(self): - """test_get_img_url's implementation not finished yet.""" - pass - # self.assertEqual(self.payment_info.get_img_url(), self.appointment.get_service_img_url()) - - def test_set_paid_status(self): - """Test if a payment info's paid status can be set correctly.""" - self.payment_info.set_paid_status(True) - self.assertTrue(self.appointment.is_paid()) - self.payment_info.set_paid_status(False) - self.assertFalse(self.appointment.is_paid()) - - def test_get_user_name(self): - """Test if payment info's username is correct.""" - self.assertEqual(self.payment_info.get_user_name(), self.client1.first_name) - - def test_get_user_email(self): - """Test if payment info's user email is correct.""" - self.assertEqual(self.payment_info.get_user_email(), self.client1.email) - - def test_created_at(self): - """Test if payment info's created at date is correctly set upon creation.""" - self.assertIsNotNone(self.payment_info.created_at) - - def test_updated_at(self): - """Test if payment info's updated at date is correctly set upon creation.""" - self.assertIsNotNone(self.payment_info.updated_at) diff --git a/appointment/tests/models/test_model_service.py b/appointment/tests/models/test_model_service.py deleted file mode 100644 index c4915ec..0000000 --- a/appointment/tests/models/test_model_service.py +++ /dev/null @@ -1,254 +0,0 @@ -from datetime import timedelta - -from django.conf import settings -from django.core.exceptions import ValidationError -from django.core.files.uploadedfile import SimpleUploadedFile -from django.test import TestCase - -from appointment.models import Service - - -class ServiceModelTestCase(TestCase): - def setUp(self): - self.service = Service.objects.create(name="Test Service", duration=timedelta(hours=1, minutes=30), price=100) - - def test_service_creation(self): - """Test if a service can be created.""" - self.assertIsNotNone(self.service) - self.assertEqual(self.service.duration, timedelta(hours=1, minutes=30)) - self.assertEqual(self.service.price, 100) - - def test_is_a_paid_service(self): - """Test if a service is a paid service.""" - self.assertTrue(self.service.is_a_paid_service()) - - def test_service_name(self): - """Test if a service can be created with a name.""" - self.assertEqual(self.service.name, "Test Service") - - def test_get_service_description(self): - """Test if a service can be created with a description.""" - self.assertEqual(self.service.description, None) - self.service.description = "Test Service - 1 hour - 100.0" - self.assertEqual(self.service.description, "Test Service - 1 hour - 100.0") - - def test_get_service_duration_day(self): - """Test that the get_duration method returns the correct string for a service with a duration of 1 day.""" - self.service.duration = timedelta(days=1) - self.assertEqual(self.service.get_duration(), '1 day') - - def test_get_service_duration_hour(self): - """Test that the get_duration method returns the correct string for a service with a duration of 1 hour.""" - self.service.duration = timedelta(hours=1) - self.assertEqual(self.service.get_duration(), '1 hour') - - def test_get_service_duration_minute(self): - """Test that the get_duration method returns the correct string for a service with a duration of 30 minutes.""" - self.service.duration = timedelta(minutes=30) - self.assertEqual(self.service.get_duration(), '30 minutes') - - def test_get_service_duration_second(self): - """Test that the get_duration method returns the correct string for a service with a duration of 30 seconds.""" - self.service.duration = timedelta(seconds=30) - self.assertEqual(self.service.get_duration(), '30 seconds') - - def test_get_service_duration_hour_minute(self): - """Test that the get_duration method returns the correct string for a service with - a duration of 1 hour 30 minutes.""" - self.service.duration = timedelta(hours=2, minutes=20) - self.assertEqual(self.service.get_duration(), '2 hours 20 minutes') - - def test_get_service_price(self): - """Test that the get_price method returns the correct price for a service.""" - self.assertEqual(self.service.get_price(), 100) - - def test_get_service_price_display(self): - """Test that the get_price_text method returns the correct string price including the currency symbol.""" - self.assertEqual(self.service.get_price_text(), "100$") - - def test_get_service_price_display_cent(self): - """Test that the method returns the correct string price including the currency symbol for a service with a - price of 100.50.""" - self.service.price = 100.50 - self.assertEqual(self.service.get_price_text(), "100.5$") - - def test_get_service_price_display_free(self): - """Test that if the price is 0, the method returns the string 'Free'.""" - self.service.price = 0 - self.assertEqual(self.service.get_price_text(), "Free") - - def test_get_service_down_payment_none(self): - """Test that the get_down_payment method returns 0 if the service has no down payment.""" - self.assertEqual(self.service.get_down_payment(), 0) - - def test_get_service_down_payment(self): - """Test that the get_down_payment method returns the correct down payment for a service.""" - self.service.down_payment = 50 - self.assertEqual(self.service.get_down_payment(), 50) - - def test_service_currency(self): - """Test if a service can be created with a currency.""" - self.assertEqual(self.service.currency, "USD") - - def test_service_created_at(self): - """Test if a service can be created with a created at date.""" - self.assertIsNotNone(self.service.created_at) - - def test_get_service_updated_at(self): - """Test if a service can be created with an updated at date.""" - self.assertIsNotNone(self.service.updated_at) - - def test_accepts_down_payment_false(self): - """Test that the accepts_down_payment method returns False if the service has no down payment.""" - self.assertFalse(self.service.accepts_down_payment()) - - def test_accepts_down_payment_true(self): - """Test that the accepts_down_payment method returns True if the service has a down payment.""" - self.service.down_payment = 50 - self.assertTrue(self.service.accepts_down_payment()) - - # Negative test cases - def test_invalid_service_name(self): - """Test that the max_length of the name field is 100 characters.""" - self.service.name = "A" * 101 # Exceeding the max_length - with self.assertRaises(ValidationError): - self.service.full_clean() - - def test_invalid_service_price_negative(self): - """A service cannot be created with a negative price.""" - self.service.price = -100 - with self.assertRaises(ValidationError): - self.service.full_clean() - - def test_invalid_service_down_payment_negative(self): - """A service cannot be created with a negative down payment.""" - self.service.down_payment = -50 - with self.assertRaises(ValidationError): - self.service.full_clean() - - def test_invalid_service_currency_length(self): - """A service cannot be created with a currency of less or more than three characters.""" - self.service.currency = "US" # Less than 3 characters - with self.assertRaises(ValidationError): - self.service.full_clean() - self.service.currency = "USDD" # More than 3 characters - with self.assertRaises(ValidationError): - self.service.full_clean() - - def test_service_duration_zero(self): - """A service cannot be created with a duration of zero.""" - service = Service(name="Test Service", duration=timedelta(0), price=100) - self.assertRaises(ValidationError, service.full_clean) - - def test_price_and_down_payment_same(self): - """A service can be created with a price and down payment of the same value.""" - service = Service.objects.create(name="Service Name", duration=timedelta(hours=1), price=100, down_payment=100) - self.assertEqual(service.price, service.down_payment) - - def test_service_with_no_name(self): - """A service cannot be created with no name.""" - with self.assertRaises(ValidationError): - Service.objects.create(name="", duration=timedelta(hours=1), price=100).full_clean() - - def test_service_with_invalid_duration(self): - """Service should not be created with a negative or zero duration.""" - service = Service(name="Invalid Duration Service", duration=timedelta(seconds=-1), price=50) - self.assertRaises(ValidationError, service.full_clean) - service = Service(name="Zero Duration Service", duration=timedelta(seconds=0), price=50) - self.assertRaises(ValidationError, service.full_clean) - - def test_service_with_empty_name(self): - """Service should not be created with an empty name.""" - service = Service.objects.create(name="", duration=timedelta(hours=1), price=50) - self.assertRaises(ValidationError, service.full_clean) - - def test_service_with_negative_price(self): - """Service should not be created with a negative price.""" - service = Service(name="Negative Price Service", duration=timedelta(hours=1), price=-1) - self.assertRaises(ValidationError, service.full_clean) - - def test_service_with_negative_down_payment(self): - """Service should not have a negative down payment.""" - with self.assertRaises(ValidationError): - service = Service(name="Service with Negative Down Payment", duration=timedelta(hours=1), price=50, - down_payment=-1) - service.full_clean() - - def test_service_auto_generate_background_color(self): - """Service should auto-generate a background color if none is provided.""" - service = Service.objects.create(name="Service with Auto Background", duration=timedelta(hours=1), price=50) - self.assertIsNotNone(service.background_color) - self.assertNotEqual(service.background_color, "") - - def test_reschedule_limit_and_allowance(self): - """Service should correctly handle reschedule limits and rescheduling allowance.""" - service = Service.objects.create(name="Reschedulable Service", duration=timedelta(hours=1), price=50, - reschedule_limit=3, allow_rescheduling=True) - self.assertEqual(service.reschedule_limit, 3) - self.assertTrue(service.allow_rescheduling) - - def test_get_service_image_url_no_image(self): - """Service should handle cases where no image is provided gracefully.""" - service = Service.objects.create(name="Service without Image", duration=timedelta(hours=1), price=50) - self.assertEqual(service.get_image_url(), "") - - def test_to_dict_method(self): - """Test the to_dict method returns the correct dictionary representation of the Service instance.""" - service = Service.objects.create(name="Test Service", duration=timedelta(hours=1), price=150, - description="A test service") - expected_dict = { - "id": service.id, - "name": "Test Service", - "description": "A test service", - "price": "150" - } - self.assertEqual(service.to_dict(), expected_dict) - - def test_get_down_payment_as_integer(self): - """Test the get_down_payment method returns an integer if the down payment has no decimal part.""" - service = Service.objects.create(name="Test Service", duration=timedelta(hours=1), price=100, down_payment=50) - self.assertEqual(service.get_down_payment(), 50) - - def test_get_down_payment_as_decimal(self): - """Test the get_down_payment method returns the original decimal value if it has a decimal part.""" - service = Service.objects.create(name="Test Service", duration=timedelta(hours=1), price=100, - down_payment=50.50) - self.assertEqual(service.get_down_payment(), 50.50) - - def test_get_down_payment_text_free(self): - """Test the get_down_payment_text method returns 'Free' if the down payment is 0.""" - service = Service.objects.create(name="Free Service", duration=timedelta(hours=1), price=100, down_payment=0) - self.assertEqual(service.get_down_payment_text(), "Free") - - def test_get_down_payment_text_with_value(self): - """Test the get_down_payment_text method returns the down payment amount followed by the currency icon.""" - service = Service.objects.create(name="Paid Service", duration=timedelta(hours=1), price=100, down_payment=25) - # Assuming get_currency_icon method returns "$" for USD - expected_text = "25$" - self.assertEqual(service.get_down_payment_text(), expected_text) - - def test_get_down_payment_text_with_decimal(self): - """Test the get_down_payment_text method for a service with a decimal down payment.""" - service = Service.objects.create(name="Service with Decimal Down Payment", duration=timedelta(hours=1), - price=100, down_payment=25.75) - # Assuming get_currency_icon method returns "$" for USD - expected_text = "25.75$" - self.assertEqual(service.get_down_payment_text(), expected_text) - - def test_str_method(self): - """Test the string representation of the Service model.""" - service_name = "Test Service" - service = Service.objects.create(name=service_name, duration=timedelta(hours=1), price=100) - self.assertEqual(str(service), service_name) - - def test_get_service_image_url_with_image(self): - """Service should return the correct URL for the image if provided.""" - # Create an image and attach it to the service - image_path = settings.BASE_DIR / 'appointment/static/img/texture.webp' # Adjust the path as necessary - image = SimpleUploadedFile(name='test_image.png', content=open(image_path, 'rb').read(), - content_type='image/png') - service = Service.objects.create(name="Service with Image", duration=timedelta(hours=1), price=50, image=image) - - # Assuming you have MEDIA_URL set in your settings for development like '/media/' - expected_url = f"{settings.MEDIA_URL}{service.image}" - self.assertTrue(service.get_image_url().endswith(expected_url)) diff --git a/appointment/tests/models/test_model_staff_member.py b/appointment/tests/models/test_model_staff_member.py deleted file mode 100644 index 84a2464..0000000 --- a/appointment/tests/models/test_model_staff_member.py +++ /dev/null @@ -1,180 +0,0 @@ -import datetime -from datetime import timedelta - -from django.db import IntegrityError -from django.test import TestCase -from django.utils.translation import gettext as _ - -from appointment.models import DayOff, Service, StaffMember, WorkingHours -from appointment.tests.mixins.base_mixin import ConfigMixin, ServiceMixin, StaffMemberMixin, UserMixin - - -class StaffMemberModelTestCase(TestCase, UserMixin, ServiceMixin, StaffMemberMixin, ConfigMixin): - def setUp(self): - self.user = self.create_user_() - self.service = self.create_service_() - self.staff_member = self.create_staff_member_(self.user, self.service) - self.config = self.create_config_(lead_time=datetime.time(9, 0), finish_time=datetime.time(17, 0), - slot_duration=30) - - def test_staff_member_creation(self): - """Test if a staff member can be created.""" - self.assertIsNotNone(self.staff_member) - self.assertEqual(self.staff_member.user, self.user) - self.assertEqual(list(self.staff_member.get_services_offered()), [self.service]) - self.assertIsNone(self.staff_member.lead_time) - self.assertIsNone(self.staff_member.finish_time) - self.assertIsNone(self.staff_member.slot_duration) - self.assertIsNone(self.staff_member.appointment_buffer_time) - - def test_staff_member_without_user(self): - """A staff member cannot be created without a user.""" - with self.assertRaises(IntegrityError): - StaffMember.objects.create() - - def test_staff_member_without_service(self): - """A staff member can be created without a service.""" - self.staff_member.delete() - new_staff_member = StaffMember.objects.create(user=self.user) - self.assertIsNotNone(new_staff_member) - self.assertEqual(new_staff_member.services_offered.count(), 0) - - def test_date_joined_auto_creation(self): - """Test if the date_joined field is automatically set upon creation.""" - self.assertIsNotNone(self.staff_member.created_at) - - # Edge cases - def test_staff_member_multiple_services(self): - """A staff member can offer multiple services.""" - service2 = Service.objects.create(name="Test Service 2", duration=timedelta(hours=2), price=200) - self.staff_member.services_offered.add(service2) - self.assertIn(service2, self.staff_member.services_offered.all()) - - def test_staff_member_with_non_existent_service(self): - """A staff member cannot offer a non-existent service.""" - # Create a new staff member without any services - self.staff_member.delete() - new_staff_member = StaffMember.objects.create(user=self.user) - - # Trying to add a non-existent service to the staff member's services_offered - with self.assertRaises(ValueError): - new_staff_member.services_offered.add( - Service(id=9999, name="Non-existent Service", duration=timedelta(hours=2), price=200)) - - def test_str_representation(self): - """Test the string representation of a StaffMember.""" - expected_str = self.staff_member.get_staff_member_name() - self.assertEqual(str(self.staff_member), expected_str) - - def test_get_slot_duration_with_config(self): - """Test get_slot_duration method with Config set.""" - self.config.slot_duration = 30 - self.config.save() - self.assertEqual(self.staff_member.get_slot_duration(), 30) - - def test_get_slot_duration_text(self): - """Test get_slot_duration_text method.""" - self.staff_member.slot_duration = 45 - self.assertEqual(self.staff_member.get_slot_duration_text(), "45 minutes") - - def test_get_lead_time(self): - """Test get_lead_time method.""" - self.config.lead_time = datetime.time(9, 0) - self.config.save() - self.assertIsNone(self.staff_member.lead_time) - self.assertEqual(self.staff_member.get_lead_time(), datetime.time(9, 0)) - - def test_works_on_both_weekends_day(self): - """Test works_on_both_weekends_day method.""" - self.staff_member.work_on_saturday = True - self.staff_member.work_on_sunday = True - self.assertTrue(self.staff_member.works_on_both_weekends_day()) - - def test_get_non_working_days(self): - """Test get_non_working_days method.""" - self.staff_member.work_on_saturday = False - self.staff_member.work_on_sunday = False - self.assertEqual(self.staff_member.get_non_working_days(), - [6, 0]) # [6, 0] represents Saturday and Sunday - - def test_get_services_offered(self): - """Test get_services_offered method.""" - self.assertIn(self.service, self.staff_member.get_services_offered()) - - def test_get_service_offered_text(self): - """Test get_service_offered_text method.""" - self.assertEqual(self.staff_member.get_service_offered_text(), self.service.name) - - def test_get_appointment_buffer_time(self): - """Test get_appointment_buffer_time method.""" - self.config.appointment_buffer_time = 15 - self.config.save() - self.assertIsNone(self.staff_member.appointment_buffer_time) - self.assertEqual(self.staff_member.get_appointment_buffer_time(), 15) - - def test_get_finish_time(self): - """Test that the finish time is correctly returned from staff member or config.""" - # Case 1: Staff member has a defined finish time - self.staff_member.finish_time = datetime.time(18, 0) - self.staff_member.save() - self.assertEqual(self.staff_member.get_finish_time(), datetime.time(18, 0)) - - # Case 2: Staff member does not have a defined finish time, use config's - self.staff_member.finish_time = None - self.staff_member.save() - self.assertEqual(self.staff_member.get_finish_time(), self.config.finish_time) - - def test_get_staff_member_first_name(self): - """Test that the staff member's first name is returned.""" - self.assertEqual(self.staff_member.get_staff_member_first_name(), self.user.first_name) - - def test_get_weekend_days_worked_text(self): - """Test various combinations of weekend work.""" - self.staff_member.work_on_saturday = True - self.staff_member.work_on_sunday = False - self.staff_member.save() - self.assertEqual(self.staff_member.get_weekend_days_worked_text(), _("Saturday")) - - self.staff_member.work_on_sunday = True - self.staff_member.save() - self.assertEqual(self.staff_member.get_weekend_days_worked_text(), _("Saturday and Sunday")) - - self.staff_member.work_on_saturday = False - self.staff_member.save() - self.assertEqual(self.staff_member.get_weekend_days_worked_text(), _("Sunday")) - - self.staff_member.work_on_saturday = False - self.staff_member.work_on_sunday = False - self.staff_member.save() - self.assertEqual(self.staff_member.get_weekend_days_worked_text(), _("None")) - - def test_get_appointment_buffer_time_text(self): - """Test the textual representation of the appointment buffer time.""" - self.staff_member.appointment_buffer_time = 45 # 45 minutes - self.assertEqual(self.staff_member.get_appointment_buffer_time_text(), "45 minutes") - - def test_get_days_off(self): - """Test retrieval of days off.""" - DayOff.objects.create(staff_member=self.staff_member, start_date="2023-01-01", end_date="2023-01-02") - self.assertEqual(len(self.staff_member.get_days_off()), 1) - - def test_get_working_hours(self): - """Test retrieval of working hours.""" - WorkingHours.objects.create(staff_member=self.staff_member, day_of_week=1, start_time=datetime.time(9, 0), - end_time=datetime.time(17, 0)) - self.assertEqual(len(self.staff_member.get_working_hours()), 1) - - def test_update_upon_working_hours_deletion(self): - """Test the update of work_on_saturday and work_on_sunday upon working hours deletion.""" - self.staff_member.update_upon_working_hours_deletion(6) - self.assertFalse(self.staff_member.work_on_saturday) - self.staff_member.update_upon_working_hours_deletion(0) - self.assertFalse(self.staff_member.work_on_sunday) - - def test_is_working_day(self): - """Test whether a day is considered a working day.""" - self.staff_member.work_on_saturday = False - self.staff_member.work_on_sunday = False - self.staff_member.save() - self.assertFalse(self.staff_member.is_working_day(6)) # Saturday - self.assertTrue(self.staff_member.is_working_day(1)) # Monday diff --git a/appointment/tests/models/test_model_working_hours.py b/appointment/tests/models/test_model_working_hours.py deleted file mode 100644 index 1d4d879..0000000 --- a/appointment/tests/models/test_model_working_hours.py +++ /dev/null @@ -1,93 +0,0 @@ -from datetime import time - -from django.core.exceptions import ValidationError -from django.db import IntegrityError -from django.test import TestCase - -from appointment.models import WorkingHours -from appointment.tests.mixins.base_mixin import ServiceMixin, StaffMemberMixin, UserMixin - - -class WorkingHoursModelTestCase(TestCase, UserMixin, ServiceMixin, StaffMemberMixin): - def setUp(self): - self.user = self.create_user_() - self.service = self.create_service_() - self.staff_member = self.create_staff_member_(self.user, self.service) - self.working_hours = WorkingHours.objects.create( - staff_member=self.staff_member, - day_of_week=1, - start_time=time(9, 0), - end_time=time(17, 0) - ) - - def test_working_hours_creation(self): - """Test if a WorkingHours instance can be created.""" - self.assertIsNotNone(self.working_hours) - self.assertEqual(self.working_hours.staff_member, self.staff_member) - - def test_working_hours_start_time_before_end_time(self): - """A WorkingHours instance cannot be created if start_time is after end_time.""" - with self.assertRaises(ValidationError): - WorkingHours.objects.create( - staff_member=self.staff_member, - day_of_week=2, - start_time=time(17, 0), - end_time=time(9, 0) - ).clean() - - def test_working_hours_is_owner(self): - """Test that is_owner method in WorkingHours model works as expected.""" - self.assertTrue(self.working_hours.is_owner(self.user.id)) - self.assertFalse(self.working_hours.is_owner(9999)) # Assuming 9999 is not a valid user ID in your tests - - def test_working_hours_without_staff_member(self): - """A WorkingHours instance cannot be created without a staff member.""" - with self.assertRaises(IntegrityError): - WorkingHours.objects.create( - day_of_week=3, - start_time=time(9, 0), - end_time=time(17, 0) - ) - - def test_working_hours_duplicate_day(self): - """A WorkingHours instance cannot be created if the staff member already has a working hours on that day.""" - with self.assertRaises(IntegrityError): - WorkingHours.objects.create( - staff_member=self.staff_member, - day_of_week=1, # Same day as the working_hours created in setUp - start_time=time(9, 0), - end_time=time(17, 0) - ) - - def test_working_hours_str_method(self): - """Test that the string representation of a WorkingHours instance is correct.""" - self.assertEqual(str(self.working_hours), "Monday - 09:00:00 to 17:00:00") - - def test_get_day_of_week_str(self): - """Test that the get_day_of_week_str method in WorkingHours model works as expected.""" - self.assertEqual(self.working_hours.get_day_of_week_str(), "Monday") - - def test_staff_member_weekend_status_update(self): - """Test that the staff member's weekend status is updated when a WorkingHours instance is created.""" - WorkingHours.objects.create( - staff_member=self.staff_member, - day_of_week=6, # Saturday - start_time=time(9, 0), - end_time=time(12, 0) - ) - self.staff_member.refresh_from_db() - self.assertTrue(self.staff_member.work_on_saturday) - - WorkingHours.objects.create( - staff_member=self.staff_member, - day_of_week=0, # Sunday - start_time=time(9, 0), - end_time=time(12, 0) - ) - self.staff_member.refresh_from_db() - self.assertTrue(self.staff_member.work_on_sunday) - - def test_get_start_time_and_get_end_time(self): - """Test that the get_start_time and get_end_time methods in WorkingHours model work as expected.""" - self.assertEqual(self.working_hours.get_start_time(), time(9, 0)) - self.assertEqual(self.working_hours.get_end_time(), time(17, 0)) diff --git a/appointment/tests/test_availability_slot.py b/appointment/tests/test_availability_slot.py deleted file mode 100644 index aebe35e..0000000 --- a/appointment/tests/test_availability_slot.py +++ /dev/null @@ -1,59 +0,0 @@ -# test_availability_slot.py -# Path: appointment/tests/test_availability_slot.py - -from datetime import date, time, timedelta - -from django.test import TestCase - -from appointment.models import Appointment, AppointmentRequest -from appointment.tests.mixins.base_mixin import ( - AppointmentMixin, AppointmentRequestMixin, ConfigMixin, ServiceMixin, StaffMemberMixin, UserMixin) -from appointment.views import get_appointments_and_slots - - -class SlotAvailabilityTest(TestCase, UserMixin, ServiceMixin, StaffMemberMixin, AppointmentRequestMixin, - AppointmentMixin, ConfigMixin): - def setUp(self): - self.user = self.create_user_() - self.service = self.create_service_(duration=timedelta(hours=2)) - self.staff_member = self.create_staff_member_(self.user, self.service) - self.ar = self.create_appointment_request_(self.service, self.staff_member) - self.appointment = self.create_appointment_(self.user, self.ar) - self.config = self.create_config_(lead_time=time(11, 0), finish_time=time(15, 0), slot_duration=120) - self.test_date = date.today() + timedelta(days=1) # Use tomorrow's date for the tests - - def test_slot_availability_without_appointments(self): - """Test if the available slots are correct when there are no appointments.""" - _, available_slots = get_appointments_and_slots(self.test_date, self.service) - expected_slots = ['11:00 AM', '01:00 PM'] - self.assertEqual(available_slots, expected_slots) - - def test_slot_availability_with_first_slot_booked(self): - """Available slots (total 2) should be one when the first slot is booked.""" - ar = AppointmentRequest.objects.create(date=self.test_date, start_time=time(11, 0), end_time=time(13, 0), - service=self.service, staff_member=self.staff_member) - Appointment.objects.create(client=self.user, appointment_request=ar) - _, available_slots = get_appointments_and_slots(self.test_date, self.service) - expected_slots = ['01:00 PM'] - self.assertEqual(available_slots, expected_slots) - - def test_slot_availability_with_second_slot_booked(self): - """Available slots (total 2) should be one when the second slot is booked.""" - ar = AppointmentRequest.objects.create(date=self.test_date, start_time=time(13, 0), end_time=time(15, 0), - service=self.service, staff_member=self.staff_member) - Appointment.objects.create(client=self.user, appointment_request=ar) - _, available_slots = get_appointments_and_slots(self.test_date, self.service) - expected_slots = ['11:00 AM'] - self.assertEqual(available_slots, expected_slots) - - def test_slot_availability_with_both_slots_booked(self): - """Available slots (total 2) should be zero when both slots are booked.""" - ar1 = AppointmentRequest.objects.create(date=self.test_date, start_time=time(11, 0), end_time=time(13, 0), - service=self.service, staff_member=self.staff_member) - ar2 = AppointmentRequest.objects.create(date=self.test_date, start_time=time(13, 0), end_time=time(15, 0), - service=self.service, staff_member=self.staff_member) - Appointment.objects.create(client=self.user, appointment_request=ar1) - Appointment.objects.create(client=self.user, appointment_request=ar2) - _, available_slots = get_appointments_and_slots(self.test_date, self.service) - expected_slots = [] - self.assertEqual(available_slots, expected_slots) diff --git a/appointment/tests/test_services.py b/appointment/tests/test_services.py deleted file mode 100644 index 6818219..0000000 --- a/appointment/tests/test_services.py +++ /dev/null @@ -1,730 +0,0 @@ -# test_services.py -# Path: appointment/tests/test_services.py - -import datetime -import json -from _decimal import Decimal -from unittest.mock import patch - -from django.core.cache import cache -from django.test import Client -from django.test.client import RequestFactory -from django.utils.translation import gettext as _ - -from appointment.forms import StaffDaysOffForm -from appointment.services import create_staff_member_service, email_change_verification_service, \ - fetch_user_appointments, get_available_slots_for_staff, handle_day_off_form, handle_entity_management_request, \ - handle_service_management_request, handle_working_hours_form, prepare_appointment_display_data, \ - prepare_user_profile_data, save_appointment, save_appt_date_time, update_personal_info_service -from appointment.tests.base.base_test import BaseTest -from appointment.utils.date_time import convert_str_to_time, get_ar_end_time -from appointment.utils.db_helpers import Config, DayOff, EmailVerificationCode, StaffMember, WorkingHours - - -class FetchUserAppointmentsTests(BaseTest): - """Test suite for the `fetch_user_appointments` service function.""" - - def setUp(self): - super().setUp() - - # Create some appointments for testing purposes - self.appointment_for_user1 = self.create_appointment_for_user1() - self.appointment_for_user2 = self.create_appointment_for_user2() - self.staff_user = self.create_user_(username='staff_user', password='test') - self.staff_user.is_staff = True - self.staff_user.save() - - def test_fetch_appointments_for_superuser(self): - """Test that a superuser can fetch all appointments.""" - # Make user1 a superuser - self.user1.is_superuser = True - self.user1.save() - - # Fetch appointments for superuser - appointments = fetch_user_appointments(self.user1) - - # Assert that the superuser sees all appointments - self.assertIn(self.appointment_for_user1, appointments, - "Superuser should be able to see all appointments, including those created for user1.") - self.assertIn(self.appointment_for_user2, appointments, - "Superuser should be able to see all appointments, including those created for user2.") - - def test_fetch_appointments_for_staff_member(self): - """Test that a staff member can only fetch their own appointments.""" - # Fetch appointments for staff member (user1 in this case) - appointments = fetch_user_appointments(self.user1) - - # Assert that the staff member sees only their own appointments - self.assertIn(self.appointment_for_user1, appointments, - "Staff members should only see appointments linked to them. User1's appointment is missing.") - self.assertNotIn(self.appointment_for_user2, appointments, - "Staff members should not see appointments not linked to them. User2's appointment was found.") - - def test_fetch_appointments_for_regular_user(self): - """Test that a regular user (not a user with staff member instance or staff) cannot fetch appointments.""" - # Fetching appointments for a regular user (client1 in this case) should raise ValueError - with self.assertRaises(ValueError, - msg="Regular users without staff or superuser status should raise a ValueError."): - fetch_user_appointments(self.client1) - - def test_fetch_appointments_for_staff_user_without_staff_member_instance(self): - """Test that a staff user without a staff member instance gets an empty list of appointments.""" - appointments = fetch_user_appointments(self.staff_user) - # Check that the returned value is an empty list - self.assertEqual(appointments, [], "Expected an empty list for a staff user without a staff member instance.") - - -class PrepareAppointmentDisplayDataTests(BaseTest): - """Test suite for the `prepare_appointment_display_data` service function.""" - - def setUp(self): - super().setUp() - - # Create an appointment for testing purposes - self.appointment = self.create_appointment_for_user1() - - def test_non_existent_appointment(self): - """Test that the function handles a non-existent appointment correctly.""" - # Fetch data for a non-existent appointment - x, y, error_message, status_code = prepare_appointment_display_data(self.user2, 9999) - - self.assertEqual(status_code, 404, "Expected status code to be 404 for a non-existent appointment.") - self.assertEqual(error_message, _("Appointment does not exist.")) - - def test_unauthorized_user(self): - """A user who doesn't own the appointment cannot view it.""" - # Fetch data for an appointment that user2 doesn't own - x, y, error_message, status_code = prepare_appointment_display_data(self.client1, self.appointment.id) - - self.assertEqual(status_code, 403, "Expected status code to be 403 for an unauthorized user.") - self.assertEqual(error_message, _("You are not authorized to view this appointment.")) - - def test_authorized_user(self): - """An authorized user can view the appointment.""" - # Fetch data for the appointment owned by user1 - appointment, page_title, error_message, status_code = prepare_appointment_display_data(self.user1, - self.appointment.id) - - self.assertEqual(status_code, 200, "Expected status code to be 200 for an authorized user.") - self.assertIsNone(error_message) - self.assertEqual(appointment, self.appointment) - self.assertTrue(self.client1.first_name in page_title) - - def test_superuser(self): - """A superuser can view any appointment and sees the staff member name in the title.""" - self.user1.is_superuser = True - self.user1.save() - - # Fetch data for the appointment as a superuser - appointment, page_title, error_message, status_code = prepare_appointment_display_data(self.user1, - self.appointment.id) - - self.assertEqual(status_code, 200, "Expected status code to be 200 for a superuser.") - self.assertIsNone(error_message) - self.assertEqual(appointment, self.appointment) - self.assertTrue(self.client1.first_name in page_title) - self.assertTrue(self.user1.first_name in page_title) - - -class PrepareUserProfileDataTests(BaseTest): - - def setUp(self): - super().setUp() - - def test_superuser_without_staff_user_id(self): - """A superuser without a staff_user_id should see the staff list page.""" - self.user1.is_superuser = True - self.user1.save() - data = prepare_user_profile_data(self.user1, None) - self.assertFalse(data['error']) - self.assertEqual(data['template'], 'administration/staff_list.html') - self.assertIn('btn_staff_me', data['extra_context']) - - def test_regular_user_with_mismatched_staff_user_id(self): - """A regular user cannot view another user's profile.""" - data = prepare_user_profile_data(self.user1, self.user2.pk) - self.assertTrue(data['error']) - self.assertEqual(data['status_code'], 403) - - def test_superuser_with_non_existent_staff_user_id(self): - """A superuser with a non-existent staff_user_id cannot view the staff's profile.""" - self.user1.is_superuser = True - self.user1.save() - data = prepare_user_profile_data(self.user1, 9999) - self.assertTrue(data['error']) - self.assertEqual(data['status_code'], 403) - - def test_regular_user_with_matching_staff_user_id(self): - """A regular user can view their own profile.""" - data = prepare_user_profile_data(self.user1, self.user1.pk) - self.assertFalse(data['error']) - self.assertEqual(data['template'], 'administration/user_profile.html') - self.assertIn('user', data['extra_context']) - self.assertEqual(data['extra_context']['user'], self.user1) - - def test_regular_user_with_non_existent_staff_user_id(self): - """A regular user with a non-existent staff_user_id cannot view their profile.""" - data = prepare_user_profile_data(self.user1, 9999) - self.assertTrue(data['error']) - self.assertEqual(data['status_code'], 403) - - -class HandleEntityManagementRequestTests(BaseTest): - - def setUp(self): - super().setUp() - self.client = Client() - self.factory = RequestFactory() - - # Setup request object - self.request = self.factory.post('/') - self.request.user = self.user1 - - def test_staff_member_none(self): - """A day off cannot be created for a staff member that doesn't exist.""" - response = handle_entity_management_request(self.request, None, 'day_off') - self.assertEqual(response.status_code, 403) - - def test_day_off_get(self): - """Test if a day off can be fetched.""" - self.request.method = 'GET' - response = handle_entity_management_request(self.request, self.staff_member1, 'day_off') - self.assertEqual(response.status_code, 200) - - def test_working_hours_get(self): - """Test if working hours can be fetched.""" - self.request.method = 'GET' - response = handle_entity_management_request(request=self.request, staff_member=self.staff_member1, - entity_type='working_hours', staff_user_id=self.staff_member1.id) - self.assertEqual(response.status_code, 200) - - def test_day_off_post_conflicting_dates(self): - """A day off cannot be created if the staff member already has a day off on the same dates.""" - DayOff.objects.create(staff_member=self.staff_member1, start_date='2022-01-01', end_date='2022-01-07') - self.request.method = 'POST' - self.request.POST = { - 'start_date': '2022-01-01', - 'end_date': '2022-01-07' - } - response = handle_entity_management_request(self.request, self.staff_member1, 'day_off') - self.assertEqual(response.status_code, 400) - - def test_day_off_post_non_conflicting_dates(self): - """A day off can be created if the staff member doesn't have a day off on the same dates.""" - self.request.method = 'POST' - self.request.POST = { - 'start_date': '2022-01-08', - 'end_date': '2022-01-14' - } - response = handle_entity_management_request(self.request, self.staff_member1, 'day_off') - content = json.loads(response.content) - self.assertEqual(content['success'], True) - - def test_working_hours_post(self): - """Test if working hours can be created with valid data.""" - # Assuming handle_working_hours_form always returns a JsonResponse - self.request.method = 'POST' - self.request.POST = { - 'day_of_week': '2', - 'start_time': '08:00 AM', - 'end_time': '12:00 PM' - } - # Create a WorkingHours instance for self.staff_member1 - working_hours_instance = WorkingHours.objects.create(staff_member=self.staff_member1, day_of_week=1, - start_time=datetime.time(8, 0), - end_time=datetime.time(12, 0)) - - # Now, pass this instance to your function - response = handle_entity_management_request(request=self.request, staff_member=self.staff_member1, - entity_type='working_hours', - staff_user_id=self.staff_member1.user.id, - instance=working_hours_instance) - content = json.loads(response.content) - self.assertTrue(content['success']) - - -class HandleWorkingHoursFormTest(BaseTest): - - def setUp(self): - super().setUp() - - def test_add_working_hours(self): - """Test if working hours can be added.""" - response = handle_working_hours_form(self.staff_member1, 1, '09:00 AM', '05:00 PM', True) - self.assertEqual(response.status_code, 200) - - def test_update_working_hours(self): - """Test if working hours can be updated.""" - wh = WorkingHours.objects.create(staff_member=self.staff_member1, day_of_week=2, start_time='09:00', - end_time='17:00') - response = handle_working_hours_form(self.staff_member1, 3, '10:00 AM', '06:00 PM', False, wh_id=wh.id) - self.assertEqual(response.status_code, 200) - - def test_invalid_data(self): - """If the form is invalid, the function should return a JsonResponse with appropriate error message.""" - response = handle_working_hours_form(None, 1, '09:00 AM', '05:00 PM', True) # Missing staff_member - self.assertEqual(response.status_code, 400) - self.assertFalse(json.loads(response.getvalue())['success']) - - def test_invalid_time(self): - """If the start time is after the end time, the function should return a JsonResponse with appropriate error""" - response = handle_working_hours_form(self.staff_member1, 1, '05:00 PM', '09:00 AM', True) - self.assertEqual(response.status_code, 400) - content = json.loads(response.getvalue()) - self.assertEqual(content['errorCode'], 5) - self.assertFalse(content['success']) - - def test_working_hours_conflict(self): - """A staff member cannot have two working hours on the same day.""" - WorkingHours.objects.create(staff_member=self.staff_member1, day_of_week=4, start_time='09:00', - end_time='17:00') - response = handle_working_hours_form(self.staff_member1, 4, '10:00 AM', '06:00 PM', True) - self.assertEqual(response.status_code, 400) - content = json.loads(response.getvalue()) - self.assertEqual(content['errorCode'], 11) - self.assertFalse(content['success']) - - def test_invalid_working_hours_id(self): - """If the working hours ID is invalid, the function should return a JsonResponse with appropriate error""" - response = handle_working_hours_form(self.staff_member1, 1, '10:00 AM', '06:00 PM', False, wh_id=9999) - self.assertEqual(response.status_code, 400) - content = json.loads(response.getvalue()) - self.assertEqual(content['success'], False) - self.assertEqual(content['errorCode'], 10) - - def test_no_working_hours_id(self): - """If the working hours ID is not provided, the function should return a JsonResponse with appropriate error""" - response = handle_working_hours_form(self.staff_member1, 1, '10:00 AM', '06:00 PM', False) - self.assertEqual(response.status_code, 400) - content = json.loads(response.getvalue()) - self.assertEqual(content['success'], False) - self.assertEqual(content['errorCode'], 5) - - -class HandleDayOffFormTest(BaseTest): - - def setUp(self): - super().setUp() - - def test_valid_day_off_form(self): - """Test if a valid day off form is handled correctly.""" - data = { - 'start_date': '2023-01-01', - 'end_date': '2023-01-05' - } - day_off_form = StaffDaysOffForm(data) - response = handle_day_off_form(day_off_form, self.staff_member1) - self.assertEqual(response.status_code, 200) - content = json.loads(response.content) - self.assertTrue(content['success']) - - def test_invalid_day_off_form(self): - """A day off form with invalid data should return a JsonResponse with the appropriate error message.""" - data = { - 'start_date': '2023-01-01', - 'end_date': '' # Missing end_date - } - day_off_form = StaffDaysOffForm(data) - response = handle_day_off_form(day_off_form, self.staff_member1) - self.assertEqual(response.status_code, 400) - content = json.loads(response.content) - self.assertFalse(content['success']) - - -class SaveAppointmentTests(BaseTest): - - def setUp(self): - super().setUp() - - # Assuming self.create_default_appointment creates an appointment with default values - self.appt = self.create_appointment_for_user1() - self.factory = RequestFactory() - self.request = self.factory.get('/') - - def test_save_appointment(self): - """Test if an appointment can be saved with valid data.""" - client_name = "New Client Name" - client_email = "newclient@example.com" - start_time_str = "10:00 AM" - phone_number = "+1234567890" - client_address = "123 New St, TestCity" - service_id = self.service2.id - staff_member_id = self.staff_member2.id - - # Call the function - updated_appt = save_appointment(self.appt, client_name, client_email, start_time_str, phone_number, - client_address, service_id, self.request, staff_member_id) - - # Check client details - self.assertEqual(updated_appt.client.get_full_name(), client_name) - self.assertEqual(updated_appt.client.email, client_email) - - # Check appointment request details - self.assertEqual(updated_appt.appointment_request.service.id, service_id) - self.assertEqual(updated_appt.appointment_request.start_time, convert_str_to_time(start_time_str)) - end_time = get_ar_end_time(convert_str_to_time(start_time_str), self.service2.duration) - self.assertEqual(updated_appt.appointment_request.end_time, end_time) - - # Check appointment details - self.assertEqual(updated_appt.phone, phone_number) - self.assertEqual(updated_appt.address, client_address) - - -class SaveApptDateTimeTests(BaseTest): - - def setUp(self): - super().setUp() - - # Assuming create_appointment_for_user1 creates an appointment for user1 with default values - self.appt = self.create_appointment_for_user1() - self.factory = RequestFactory() - self.request = self.factory.get('/') - - def test_save_appt_date_time(self): - """Test if an appointment's date and time can be updated.""" - # Given new appointment date and time details - appt_start_time_str = "10:00:00.000000Z" - appt_date_str = (datetime.datetime.today() + datetime.timedelta(days=7)).strftime("%Y-%m-%d") - appt_id = self.appt.id - - # Call the function - updated_appt = save_appt_date_time(appt_start_time_str, appt_date_str, appt_id, self.request) - - # Convert given date and time strings to appropriate formats - time_format = "%H:%M:%S.%fZ" - appt_start_time_obj = datetime.datetime.strptime(appt_start_time_str, time_format).time() - appt_date_obj = datetime.datetime.strptime(appt_date_str, "%Y-%m-%d").date() - - # Calculate the expected end time - service = updated_appt.get_service() - end_time_obj = get_ar_end_time(appt_start_time_obj, service.duration) - - # Validate the updated appointment details - self.assertEqual(updated_appt.appointment_request.date, appt_date_obj) - self.assertEqual(updated_appt.appointment_request.start_time, appt_start_time_obj) - self.assertEqual(updated_appt.appointment_request.end_time, end_time_obj) - - -def get_next_weekday(d, weekday): - """ - Get the date of the next weekday from the given date. - This function uses python's weekday format, where Monday is 0, and Sunday is 6. - Remember that in my implementation for work days, I had to use a custom one where Monday is 1, and Sunday is 0. - So in the setup, I will use my format to create day-offs, working hours, etc. But when calling this function, I will - use the python format. - """ - days_of_week = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] - days_ahead = weekday - d.weekday() - if days_ahead <= 0: # Target day already happened this week - days_ahead += 7 - next_day = d + datetime.timedelta(days_ahead) - return next_day - - -class GetAvailableSlotsTests(BaseTest): - - def setUp(self): - super().setUp() - cache.clear() - self.today = datetime.date.today() - # Staff member1 works only on Mondays and Wednesday (day_of_week: 1, 3) - self.wh1 = WorkingHours.objects.create(staff_member=self.staff_member1, day_of_week=1, - start_time=datetime.time(9, 0), end_time=datetime.time(17, 0)) - self.wh2 = WorkingHours.objects.create(staff_member=self.staff_member1, day_of_week=3, - start_time=datetime.time(9, 0), end_time=datetime.time(17, 0)) - # But decides to take a day off next Monday - self.next_monday = get_next_weekday(self.today, 0) - self.next_tuesday = get_next_weekday(self.today, 1) - self.next_wednesday = get_next_weekday(self.today, 2) - self.next_thursday = get_next_weekday(self.today, 3) - self.next_friday = get_next_weekday(self.today, 4) - self.next_saturday = get_next_weekday(self.today, 5) - self.next_sunday = get_next_weekday(self.today, 6) - DayOff.objects.create(staff_member=self.staff_member1, start_date=self.next_monday, end_date=self.next_monday) - Config.objects.create(slot_duration=60, lead_time=datetime.time(9, 0), finish_time=datetime.time(17, 0), - appointment_buffer_time=0) - - def test_day_off(self): - """Test if a day off is handled correctly when getting available slots.""" - # Ask for slots for it, and it should return an empty list since next Monday is a day off - slots = get_available_slots_for_staff(self.next_monday, self.staff_member1) - self.assertEqual(slots, []) - - def test_staff_does_not_work(self): - """Test if a staff member who doesn't work on a given day is handled correctly when getting available slots.""" - # For next week, the staff member works only on Monday and Wednesday, but puts a day off on Monday - # So the staff member should not have any available slots except for Wednesday, which is day #2 (python weekday) - slots = get_available_slots_for_staff(self.next_monday, self.staff_member1) - self.assertEqual(slots, []) - slots = get_available_slots_for_staff(self.next_tuesday, self.staff_member1) - self.assertEqual(slots, []) - slots = get_available_slots_for_staff(self.next_thursday, self.staff_member1) - self.assertEqual(slots, []) - slots = get_available_slots_for_staff(self.next_friday, self.staff_member1) - self.assertEqual(slots, []) - slots = get_available_slots_for_staff(self.next_saturday, self.staff_member1) - self.assertEqual(slots, []) - slots = get_available_slots_for_staff(self.next_sunday, self.staff_member1) - self.assertEqual(slots, []) - - def test_available_slots(self): - """Test if available slots are returned correctly.""" - # On a Wednesday, the staff member should have slots from 9 AM to 5 PM - slots = get_available_slots_for_staff(self.next_wednesday, self.staff_member1) - expected_slots = [ - datetime.datetime(self.next_wednesday.year, self.next_wednesday.month, self.next_wednesday.day, hour) for - hour in range(9, 17)] - self.assertEqual(slots, expected_slots) - - def test_booked_slots(self): - """On a given day, if a staff member has an appointment, that time slot should not be available.""" - # Let's book a slot for the staff member on next Wednesday - start_time = datetime.time(10, 0) - end_time = datetime.time(11, 0) - - # Create an appointment request for that time - appt_request = self.create_appointment_request_(service=self.service1, staff_member=self.staff_member1, - date_=self.next_wednesday, start_time=start_time, - end_time=end_time) - # Create an appointment using that request - self.create_appointment_(user=self.client1, appointment_request=appt_request) - - # Now, the staff member should not have that slot available - slots = get_available_slots_for_staff(self.next_wednesday, self.staff_member1) - expected_slots = [ - datetime.datetime(self.next_wednesday.year, self.next_wednesday.month, self.next_wednesday.day, hour, 0) for - hour in range(9, 17) if hour != 10] - self.assertEqual(slots, expected_slots) - - def test_no_working_hours(self): - """If a staff member doesn't have working hours on a given day, no slots should be available.""" - # Let's ask for slots on a Thursday, which the staff member doesn't work - # Let's remove the config object also since it may contain default working days - Config.objects.all().delete() - # Now no slots should be available - slots = get_available_slots_for_staff(self.next_thursday, self.staff_member1) - self.assertEqual(slots, []) - - -class UpdatePersonalInfoServiceTest(BaseTest): - - def setUp(self): - super().setUp() - self.post_data_valid = { - 'first_name': 'UpdatedName', - 'last_name': 'UpdatedLastName', - 'email': self.user1.email - } - - def test_update_name(self): - """Test if the user's name can be updated.""" - user, is_valid, error_message = update_personal_info_service(self.staff_member1.user.id, self.post_data_valid, - self.user1) - self.assertTrue(is_valid) - self.assertIsNone(error_message) - self.assertEqual(user.first_name, 'UpdatedName') - self.assertEqual(user.last_name, 'UpdatedLastName') - - def test_update_invalid_user_id(self): - """Updating a user that doesn't exist should return an error message.""" - user, is_valid, error_message = update_personal_info_service(9999, self.post_data_valid, - self.user1) # Assuming 9999 is an invalid user ID - - self.assertFalse(is_valid) - self.assertEqual(error_message, _("User not found.")) - self.assertIsNone(user) - - def test_invalid_form(self): - """Updating a user with invalid form data should return an error message.""" - user, is_valid, error_message = update_personal_info_service(self.staff_member1.user.id, {}, self.user1) - self.assertFalse(is_valid) - self.assertEqual(error_message, _("Empty fields are not allowed.")) - - def test_invalid_form_(self): - """Updating a user with invalid form data should return an error message.""" - # remove email in post_data - del self.post_data_valid['email'] - user, is_valid, error_message = update_personal_info_service(self.staff_member1.user.id, self.post_data_valid, - self.user1) - self.assertFalse(is_valid) - self.assertEqual(error_message, "email: This field is required.") - - -class EmailChangeVerificationServiceTest(BaseTest): - - def setUp(self): - super().setUp() - self.valid_code = EmailVerificationCode.generate_code(self.client1) - self.invalid_code = "INVALID_CODE456" - - self.old_email = self.client1.email - self.new_email = "newemail@gmail.com" - - def test_valid_code_and_email(self): - """Test if a valid code and email can be verified.""" - is_verified = email_change_verification_service(self.valid_code, self.new_email, self.old_email) - - self.assertTrue(is_verified) - self.client1.refresh_from_db() # Refresh the user object to get the updated email - self.assertEqual(self.client1.email, self.new_email) - - def test_invalid_code(self): - """If the code is invalid, the email should not be updated.""" - is_verified = email_change_verification_service(self.invalid_code, self.new_email, self.old_email) - - self.assertFalse(is_verified) - self.client1.refresh_from_db() - self.assertEqual(self.client1.email, self.old_email) # Email should not change - - def test_valid_code_no_user(self): - """If the code is valid but the user doesn't exist, the email should not be updated.""" - is_verified = email_change_verification_service(self.valid_code, self.new_email, "nonexistent@gmail.com") - - self.assertFalse(is_verified) - - def test_code_doesnt_match_users_code(self): - """If the code is valid but doesn't match the user's code, the email should not be updated.""" - # Using valid code but for another user - is_verified = email_change_verification_service(self.valid_code, self.new_email, "anotheremail@gmail.com") - - self.assertFalse(is_verified) - - -class CreateStaffMemberServiceTest(BaseTest): - - def setUp(self): - super().setUp() - self.factory = RequestFactory() - - # Setup request object - self.request = self.factory.post('/') - - def test_valid_data(self): - """Test if a staff member can be created with valid data.""" - post_data = { - 'first_name': 'John', - 'last_name': 'Doe', - 'email': 'john.doe@gmail.com' - } - user, success, error_message = create_staff_member_service(post_data, self.request) - - self.assertTrue(success) - self.assertIsNotNone(user) - self.assertEqual(user.first_name, 'John') - self.assertEqual(user.last_name, 'Doe') - self.assertEqual(user.email, 'john.doe@gmail.com') - self.assertTrue(StaffMember.objects.filter(user=user).exists()) - - def test_invalid_data(self): - """Empty fields should not be allowed when creating a staff member.""" - post_data = { - 'first_name': '', # Missing first name - 'last_name': 'Doe', - 'email': 'john.doe@gmail.com' - } - user, success, error_message = create_staff_member_service(post_data, self.request) - - self.assertFalse(success) - self.assertIsNone(user) - self.assertIsNotNone(error_message) - - def test_email_already_exists(self): - """If the email already exists, the staff member should not be created.""" - self.create_user_(email="existing@gmail.com") - post_data = { - 'first_name': 'John', - 'last_name': 'Doe', - 'email': 'existing@gmail.com' # Using an email that already exists - } - user, success, error_message = create_staff_member_service(post_data, self.request) - - self.assertFalse(success) - self.assertIsNone(user) - self.assertEqual(error_message, "email: This email is already taken.") - - @patch('appointment.services.send_reset_link_to_staff_member') - def test_send_reset_link_to_new_staff_member(self, mock_send_reset_link): - """Test if a reset password link is sent to a new staff member.""" - post_data = { - 'first_name': 'Jane', - 'last_name': 'Smith', - 'email': 'jane.smith@gmail.com' - } - user, success, _ = create_staff_member_service(post_data, self.request) - self.assertTrue(success) - self.assertIsNotNone(user) - - # Check that the mock_send_reset_link function was called once - mock_send_reset_link.assert_called_once_with(user, self.request, user.email) - - -class HandleServiceManagementRequestTest(BaseTest): - - def setUp(self): - super().setUp() - - def test_create_new_service(self): - """Test if a new service can be created with valid data.""" - post_data = { - 'name': 'Test Service', - 'duration': '1:00:00', - 'price': '100', - 'currency': 'USD', - 'down_payment': '50', - } - service, success, message = handle_service_management_request(post_data) - self.assertTrue(success) - self.assertIsNotNone(service) - self.assertEqual(service.name, 'Test Service') - self.assertEqual(service.duration, datetime.timedelta(hours=1)) - self.assertEqual(service.price, Decimal('100')) - self.assertEqual(service.down_payment, Decimal('50')) - self.assertEqual(service.currency, 'USD') - - def test_update_existing_service(self): - """Test if an existing service can be updated with valid data.""" - existing_service = self.create_service_() - post_data = { - 'name': 'Updated Service Name', - 'duration': '2:00:00', - 'price': '150', - 'down_payment': '75', - 'currency': 'EUR' - } - service, success, message = handle_service_management_request(post_data, service_id=existing_service.id) - - self.assertTrue(success) - self.assertIsNotNone(service) - self.assertEqual(service.name, 'Updated Service Name') - self.assertEqual(service.duration, datetime.timedelta(hours=2)) - self.assertEqual(service.price, Decimal('150')) - self.assertEqual(service.currency, 'EUR') - - def test_invalid_data(self): - """Empty fields should not be allowed when creating a service.""" - post_data = { - 'name': '', # Missing name - 'duration': '1:00:00', - 'price': '100', - 'currency': 'USD', - 'down_payment': '50', - } - service, success, message = handle_service_management_request(post_data, service_id=self.service1.id) - - self.assertFalse(success) - self.assertIsNone(service) - self.assertEqual(message, "name: This field is required.") - - def test_service_not_found(self): - """If the service ID is invalid, the service should not be updated.""" - post_data = { - 'name': 'Another Test Service', - 'duration': '1:00:00', - 'price': '100', - 'currency': 'USD' - } - service, success, message = handle_service_management_request(post_data, service_id=9999) # Invalid service_id - - self.assertFalse(success) - self.assertIsNone(service) - self.assertIn(_("Service matching query does not exist"), message) diff --git a/appointment/tests/test_settings.py b/appointment/tests/test_settings.py deleted file mode 100644 index 5232c4f..0000000 --- a/appointment/tests/test_settings.py +++ /dev/null @@ -1,45 +0,0 @@ -# test_settings.py -# Path: appointment/tests/test_settings.py - -from unittest.mock import patch - -from django.test import TestCase - -from appointment.settings import check_q_cluster - - -class CheckQClusterTest(TestCase): - @patch('appointment.settings.settings') - @patch('appointment.settings.logger') - def test_check_q_cluster_with_django_q_missing(self, mock_logger, mock_settings): - # Simulate 'django_q' not being in INSTALLED_APPS - mock_settings.INSTALLED_APPS = [] - - # Call the function under test - result = check_q_cluster() - - # Check the result - self.assertFalse(result) - # Verify logger was called with the expected warning about 'django_q' not being installed - mock_logger.warning.assert_called_with( - "Django Q is not in settings.INSTALLED_APPS. Please add it to the list.\n" - "Example: \n\n" - "INSTALLED_APPS = [\n" - " ...\n" - " 'appointment',\n" - " 'django_q',\n" - "]\n") - - @patch('appointment.settings.settings') - @patch('appointment.settings.logger') - def test_check_q_cluster_with_all_configurations_present(self, mock_logger, mock_settings): - # Simulate both 'django_q' being in INSTALLED_APPS and 'Q_CLUSTER' configuration present - mock_settings.INSTALLED_APPS = ['django_q'] - mock_settings.Q_CLUSTER = {'name': 'DjangORM'} - - # Call the function under test - result = check_q_cluster() - - # Check the result and ensure no warnings are logged - self.assertTrue(result) - mock_logger.warning.assert_not_called() diff --git a/appointment/tests/test_tasks.py b/appointment/tests/test_tasks.py deleted file mode 100644 index 878f54d..0000000 --- a/appointment/tests/test_tasks.py +++ /dev/null @@ -1,44 +0,0 @@ -# test_tasks.py -# Path: appointment/tests/test_tasks.py - -from unittest.mock import patch - -from django.utils.translation import gettext as _ - -from appointment.tasks import send_email_reminder -from appointment.tests.base.base_test import BaseTest - - -class SendEmailReminderTest(BaseTest): - - @patch('appointment.tasks.send_email') - @patch('appointment.tasks.notify_admin') - def test_send_email_reminder(self, mock_notify_admin, mock_send_email): - # Use BaseTest setup to create an appointment - appointment_request = self.create_appt_request_for_sm1() - appointment = self.create_appointment_for_user1(appointment_request=appointment_request) - - # Extract necessary data for the test - to_email = appointment.client.email - first_name = appointment.client.first_name - appointment_id = appointment.id - - # Call the function under test - send_email_reminder(to_email, first_name, "", appointment_id) - - # Verify send_email was called with correct parameters - mock_send_email.assert_called_once_with( - recipient_list=[to_email], - subject=_("Reminder: Upcoming Appointment"), - template_url='email_sender/reminder_email.html', - context={'first_name': first_name, 'appointment': appointment, 'reschedule_link': "", - 'recipient_type': 'admin'} - ) - - # Verify notify_admin was called with correct parameters - mock_notify_admin.assert_called_once_with( - subject=_("Admin Reminder: Upcoming Appointment"), - template_url='email_sender/reminder_email.html', - context={'first_name': first_name, 'appointment': appointment, 'reschedule_link': "", - 'recipient_type': 'admin'} - ) diff --git a/appointment/tests/test_utils.py b/appointment/tests/test_utils.py deleted file mode 100644 index 7231ae5..0000000 --- a/appointment/tests/test_utils.py +++ /dev/null @@ -1,246 +0,0 @@ -# test_utils.py -# Path: appointment/tests/test_utils.py - -import datetime -import logging -from unittest.mock import patch - -from django.apps import apps -from django.conf import settings -from django.http import HttpRequest -from django.test import TestCase -from django.utils import timezone -from django.utils.translation import gettext_lazy as _ - -from appointment.models import Appointment, AppointmentRequest, Config, Service, StaffMember -from appointment.services import get_available_slots, get_finish_button_text -from appointment.settings import APPOINTMENT_BUFFER_TIME, APPOINTMENT_FINISH_TIME, APPOINTMENT_LEAD_TIME, \ - APPOINTMENT_SLOT_DURATION, APPOINTMENT_WEBSITE_NAME -from appointment.utils.date_time import combine_date_and_time, convert_str_to_date, get_current_year, get_timestamp -from appointment.utils.db_helpers import get_appointment_buffer_time, get_appointment_finish_time, \ - get_appointment_lead_time, get_appointment_slot_duration, get_user_model, get_website_name -from appointment.utils.view_helpers import generate_random_id, get_locale, is_ajax - - -class UtilityTestCase(TestCase): - # Test cases for generate_random_id - - def setUp(self) -> None: - self.test_service = Service.objects.create(name="Test Service", duration=datetime.timedelta(hours=1), price=100) - self.user_model = get_user_model() - self.user = self.user_model.objects.create_user(first_name="Tester", - email="testemail@gmail.com", - username="test_user", password="Kfdqi3!?n") - self.staff_member = StaffMember.objects.create(user=self.user) - self.staff_member.services_offered.add(self.test_service) - - def test_generate_random_id(self): - id1 = generate_random_id() - id2 = generate_random_id() - self.assertNotEqual(id1, id2) - - # Test cases for get_timestamp - def test_get_timestamp(self): - ts = get_timestamp() - self.assertIsNotNone(ts) - self.assertIsInstance(ts, str) - - # Test cases for is_ajax - def test_is_ajax_true(self): - request = HttpRequest() - request.META['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' - self.assertTrue(is_ajax(request)) - - def test_is_ajax_false(self): - request = HttpRequest() - self.assertFalse(is_ajax(request)) - - # Test cases for get_available_slots - def test_get_available_slots(self): - date_str = datetime.date.today().strftime('%Y-%m-%d') - date = convert_str_to_date(date_str) - ar = AppointmentRequest.objects.create( - date=date, - start_time=datetime.time(9, 0), - end_time=datetime.time(10, 0), - service=self.test_service, - staff_member=self.staff_member - ) - appointment = Appointment.objects.create(appointment_request=ar, client=self.user) - slots = get_available_slots(date, [appointment]) - self.assertIsInstance(slots, list) - logging.info(slots) - self.assertNotIn('09:00 AM', slots) - - def test_get_available_slots_with_config(self): - date_str = datetime.date.today().strftime('%Y-%m-%d') - date = convert_str_to_date(date_str) - lead_time = datetime.time(8, 0) - finish_time = datetime.time(17, 0) - slot_duration = 30 - appointment_buffer_time = 2.0 - Config.objects.create( - lead_time=lead_time, - finish_time=finish_time, - slot_duration=slot_duration, - appointment_buffer_time=appointment_buffer_time - ) - ar = AppointmentRequest.objects.create( - date=date, - start_time=datetime.time(9, 0), - end_time=datetime.time(10, 0), - service=self.test_service, - staff_member=self.staff_member - ) - appointment = Appointment.objects.create(appointment_request=ar, client=self.user) - slots = get_available_slots(date, [appointment]) - self.assertIsInstance(slots, list) - logging.info(slots) - self.assertNotIn('09:00 AM', slots) - # Additional assertions to verify that the slots are calculated correctly - - # Test cases for get_locale - def test_get_locale_en(self): - with self.settings(LANGUAGE_CODE='en'): - self.assertEqual(get_locale(), 'en') - - def test_get_locale_en_us(self): - with self.settings(LANGUAGE_CODE='en_US'): - self.assertEqual(get_locale(), 'en') - - def test_get_locale_fr(self): - # Set the local to French - with self.settings(LANGUAGE_CODE='fr'): - self.assertEqual(get_locale(), 'fr') - - def test_get_locale_fr_France(self): - # Set the local to French - with self.settings(LANGUAGE_CODE='fr_FR'): - self.assertEqual(get_locale(), 'fr') - - def test_get_locale_others(self): - with self.settings(LANGUAGE_CODE='de'): - self.assertEqual(get_locale(), 'de') - - # Test cases for get_current_year - def test_get_current_year(self): - self.assertEqual(get_current_year(), datetime.datetime.now().year) - - # Test cases for convert_str_to_date - def test_convert_str_to_date_valid(self): - date_str = '2023-07-31' - date_obj = convert_str_to_date(date_str) - self.assertEqual(date_obj, datetime.date(2023, 7, 31)) - - def test_convert_str_to_date_invalid(self): - date_str = 'invalid-date' - with self.assertRaises(ValueError): - convert_str_to_date(date_str) - - def test_get_website_name_no_config(self): - website_name = get_website_name() - self.assertEqual(website_name, APPOINTMENT_WEBSITE_NAME) - - def test_get_website_name_with_config(self): - Config.objects.create(website_name="Test Website") - website_name = get_website_name() - self.assertEqual(website_name, "Test Website") - - def test_get_appointment_slot_duration_no_config(self): - slot_duration = get_appointment_slot_duration() - self.assertEqual(slot_duration, APPOINTMENT_SLOT_DURATION) - - def test_get_appointment_slot_duration_with_config(self): - Config.objects.create(slot_duration=45) - slot_duration = get_appointment_slot_duration() - self.assertEqual(slot_duration, 45) - - # Test cases for get_appointment_lead_time - def test_get_appointment_lead_time_no_config(self): - lead_time = get_appointment_lead_time() - self.assertEqual(lead_time, APPOINTMENT_LEAD_TIME) - - def test_get_appointment_lead_time_with_config(self): - config_lead_time = datetime.time(hour=7, minute=30) - Config.objects.create(lead_time=config_lead_time) - lead_time = get_appointment_lead_time() - self.assertEqual(lead_time, config_lead_time) - - # Test cases for get_appointment_finish_time - def test_get_appointment_finish_time_no_config(self): - finish_time = get_appointment_finish_time() - self.assertEqual(finish_time, APPOINTMENT_FINISH_TIME) - - def test_get_appointment_finish_time_with_config(self): - config_finish_time = datetime.time(hour=18, minute=30) - Config.objects.create(finish_time=config_finish_time) - finish_time = get_appointment_finish_time() - self.assertEqual(finish_time, config_finish_time) - - # Test cases for get_appointment_buffer_time - def test_get_appointment_buffer_time_no_config(self): - buffer_time = get_appointment_buffer_time() - self.assertEqual(buffer_time, APPOINTMENT_BUFFER_TIME) - - def test_get_appointment_buffer_time_with_config(self): - config_buffer_time = 0.5 # 30 minutes - Config.objects.create(appointment_buffer_time=config_buffer_time) - buffer_time = get_appointment_buffer_time() - self.assertEqual(buffer_time, config_buffer_time) - - # Test cases for get_finish_button_text - def test_get_finish_button_text_free_service(self): - service = Service(price=0) - button_text = get_finish_button_text(service) - self.assertEqual(button_text, _("Finish")) - - def test_get_finish_button_text_paid_service(self): - with patch('appointment.services.APPOINTMENT_PAYMENT_URL', 'https://payment.com'): - service = Service(price=100) - button_text = get_finish_button_text(service) - self.assertEqual(button_text, _("Pay Now")) - - # Test cases for get_user_model - def test_get_client_model(self): - client_model = get_user_model() - self.assertEqual(client_model, apps.get_model(settings.AUTH_USER_MODEL)) - - -class UtilityDateTimeTestCase(TestCase): - def test_combine_date_and_time_success(self): - """ - Test combining a date and time into a datetime object successfully. - """ - date = datetime.date(2024, 2, 5) - time = datetime.time(14, 30) - expected_datetime = datetime.datetime(2024, 2, 5, 14, 30) - combined_datetime = combine_date_and_time(date, time) - self.assertEqual(combined_datetime, expected_datetime) - - def test_combine_date_and_time_with_timezone(self): - """ - Test combining a date and time into a timezone-aware datetime object. - """ - date = datetime.date(2024, 2, 5) - time = datetime.time(14, 30, tzinfo=datetime.timezone.utc) - combined_datetime = combine_date_and_time(date, time) - self.assertTrue(timezone.is_aware(combined_datetime), "The datetime object is not timezone-aware.") - self.assertEqual(combined_datetime.tzinfo, datetime.timezone.utc) - - def test_combine_date_and_time_with_invalid_date(self): - """ - Test handling of invalid date input. - """ - date = "2024-02-05" # Incorrect type. It should be datetime.date - time = datetime.time(14, 30) - with self.assertRaises(TypeError, msg="Expected TypeError when combining with invalid date type"): - combine_date_and_time(date, time) - - def test_combine_date_and_time_with_invalid_time(self): - """ - Test handling of invalid time input. - """ - date = datetime.date(2024, 2, 5) - time = "14:30" # Incorrect type. It should be datetime.time - with self.assertRaises(TypeError, msg="Expected TypeError when combining with invalid time type"): - combine_date_and_time(date, time) diff --git a/appointment/tests/test_views.py b/appointment/tests/test_views.py deleted file mode 100644 index 81197ac..0000000 --- a/appointment/tests/test_views.py +++ /dev/null @@ -1,1108 +0,0 @@ -# test_views.py -# Path: appointment/tests/test_views.py - -import datetime -import json -import uuid -from datetime import date, time, timedelta -from unittest.mock import MagicMock, patch - -from django.contrib import messages -from django.contrib.messages import get_messages -from django.contrib.messages.middleware import MessageMiddleware -from django.contrib.sessions.middleware import SessionMiddleware -from django.http import HttpResponseRedirect -from django.test import Client -from django.test.client import RequestFactory -from django.urls import reverse -from django.utils import timezone -from django.utils.encoding import force_bytes -from django.utils.http import urlsafe_base64_encode -from django.utils.translation import gettext as _ - -from appointment.forms import StaffMemberForm -from appointment.messages_ import passwd_error -from appointment.models import ( - Appointment, AppointmentRequest, AppointmentRescheduleHistory, Config, DayOff, EmailVerificationCode, - PasswordResetToken, StaffMember -) -from appointment.tests.base.base_test import BaseTest -from appointment.utils.db_helpers import Service, WorkingHours, create_user_with_username -from appointment.utils.error_codes import ErrorCode -from appointment.views import create_appointment, redirect_to_payment_or_thank_you_page, verify_user_and_login - - -class SlotTestCase(BaseTest): - def setUp(self): - super().setUp() - self.client = Client() - self.url = reverse('appointment:available_slots_ajax') - - def test_get_available_slots_ajax(self): - """get_available_slots_ajax view should return a JSON response with available slots for the selected date.""" - response = self.client.get(self.url, {'selected_date': date.today().isoformat(), 'staff_member': self.staff_member1.id}, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - response_data = response.json() - self.assertIn('date_chosen', response_data) - self.assertIn('available_slots', response_data) - self.assertFalse(response_data.get('error')) - - def test_get_available_slots_ajax_invalid_form(self): - """get_available_slots_ajax view should return an error if the selected date is in the past.""" - past_date = (date.today() - timedelta(days=1)).isoformat() - response = self.client.get(self.url, {'selected_date': past_date}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.json()['error'], True) - self.assertEqual(response.json()['message'], 'Date is in the past') - # invalid staff id - response = self.client.get(self.url, {'selected_date': date.today(), 'staff_member': 999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.json()['error'], True) - self.assertEqual(response.json()['message'], 'Staff member does not exist') - - -class AppointmentRequestTestCase(BaseTest): - def setUp(self): - super().setUp() - self.url = reverse('appointment:appointment_request_submit') - - def test_appointment_request(self): - """Test if the appointment request form can be rendered.""" - url = reverse('appointment:appointment_request', args=[self.service1.id]) - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - self.assertIn(self.service1.name, str(response.content)) - self.assertIn('all_staff_members', response.context) - self.assertIn('service', response.context) - - def test_appointment_request_submit_valid(self): - """Test if a valid appointment request can be submitted.""" - post_data = { - 'date': date.today().isoformat(), - 'start_time': time(9, 0), - 'end_time': time(10, 0), - 'service': self.service1.id, - 'staff_member': self.staff_member1.id, - } - response = self.client.post(self.url, post_data) - self.assertEqual(response.status_code, 302) # Redirect status - # Check if an AppointmentRequest object was created - self.assertTrue(AppointmentRequest.objects.filter(service=self.service1).exists()) - - def test_appointment_request_submit_invalid(self): - """Test if an invalid appointment request can be submitted.""" - post_data = {} # Missing required data - response = self.client.post(self.url, post_data) - self.assertEqual(response.status_code, 200) # Rendering the form with errors - self.assertIn('form', response.context) - self.assertTrue(response.context['form'].errors) # Ensure there are form errors - - -class VerificationCodeTestCase(BaseTest): - def setUp(self): - super().setUp() - self.factory = RequestFactory() - self.request = self.factory.get('/') - - # Simulate session middleware - middleware = SessionMiddleware(lambda req: None) - middleware.process_request(self.request) - self.request.session.save() - - # Attach message storage - middleware = MessageMiddleware(lambda req: None) - middleware.process_request(self.request) - self.request.session.save() - - self.ar = self.create_appt_request_for_sm1() - self.url = reverse('appointment:enter_verification_code', args=[self.ar.id, self.ar.id_request]) - - def test_verify_user_and_login_valid(self): - """Test if a user can be verified and logged in.""" - code = EmailVerificationCode.generate_code(user=self.user1) - result = verify_user_and_login(self.request, self.user1, code) - self.assertTrue(result) - - def test_verify_user_and_login_invalid(self): - """Test if a user cannot be verified and logged in with an invalid code.""" - invalid_code = '000000' # An invalid code - result = verify_user_and_login(self.request, self.user1, invalid_code) - self.assertFalse(result) - - def test_enter_verification_code_valid(self): - """Test if a valid verification code can be entered.""" - code = EmailVerificationCode.generate_code(user=self.user1) - post_data = {'code': code} # Assuming a valid code for the test setup - response = self.client.post(self.url, post_data) - self.assertEqual(response.status_code, 200) - - def test_enter_verification_code_invalid(self): - """Test if an invalid verification code can be entered.""" - post_data = {'code': '000000'} # Invalid code - response = self.client.post(self.url, post_data) - self.assertEqual(response.status_code, 200) # Stay on the same page - # Check for an error message - messages_list = list(messages.get_messages(response.wsgi_request)) - self.assertIn(_("Invalid verification code."), [str(msg) for msg in messages_list]) - - -class StaffMemberTestCase(BaseTest): - def setUp(self): - super().setUp() - self.staff_member = self.staff_member1 - self.appointment = self.create_appointment_for_user1() - - def remove_staff_member(self): - """Remove the StaffMember instance of self.user1.""" - self.clean_staff_member_objects() - StaffMember.objects.filter(user=self.user1).delete() - - def test_staff_user_without_staff_member_instance(self): - """Test that a staff user without a staff member instance receives an appropriate error message.""" - self.clean_staff_member_objects() - - # Now safely delete the StaffMember instance - StaffMember.objects.filter(user=self.user1).delete() - - self.user1.save() # Save the user to the database after updating - self.need_staff_login() - - url = reverse('appointment:get_user_appointments') - response = self.client.get(url) - - message_list = list(get_messages(response.wsgi_request)) - self.assertTrue(any( - message.message == "User doesn't have a staff member instance. Please contact the administrator." for - message in message_list), - "Expected error message not found in messages.") - - def test_remove_staff_member(self): - self.need_superuser_login() - self.clean_staff_member_objects() - - url = reverse('appointment:remove_staff_member', args=[self.staff_member.user_id]) - response = self.client.get(url) - - self.assertEqual(response.status_code, 302) # Redirect status code - self.assertRedirects(response, reverse('appointment:user_profile')) - - # Check for success messages - messages_list = list(get_messages(response.wsgi_request)) - self.assertTrue(any(_("Staff member deleted successfully!") in str(message) for message in messages_list)) - - # Check if staff member is deleted - staff_member_exists = StaffMember.objects.filter(pk=self.staff_member.id).exists() - self.assertFalse(staff_member_exists, "Appointment should be deleted but still exists.") - - def test_remove_staff_member_with_superuser(self): - self.need_superuser_login() - self.clean_staff_member_objects() - # Test removal of staff member by a superuser - response = self.client.get(reverse('appointment:remove_superuser_staff_member')) - - # Check if the StaffMember instance was deleted - self.assertFalse(StaffMember.objects.filter(user=self.user1).exists()) - - # Check if it redirects to the user profile - self.assertRedirects(response, reverse('appointment:user_profile')) - - def test_remove_staff_member_without_superuser(self): - # Log out superuser and log in as a regular user - self.need_staff_login() - response = self.client.get(reverse('appointment:remove_superuser_staff_member')) - - # Check for a forbidden status code, as only superusers should be able to remove staff members - self.assertEqual(response.status_code, 403) - - def test_make_staff_member_with_superuser(self): - self.need_superuser_login() - self.remove_staff_member() - # Test creating a staff member by a superuser - response = self.client.get(reverse('appointment:make_superuser_staff_member')) - - # Check if the StaffMember instance was created - self.assertTrue(StaffMember.objects.filter(user=self.user1).exists()) - - # Check if it redirects to the user profile - self.assertRedirects(response, reverse('appointment:user_profile')) - - def test_make_staff_member_without_superuser(self): - self.need_staff_login() - response = self.client.get(reverse('appointment:make_superuser_staff_member')) - - # Check for a forbidden status code, as only superusers should be able to create staff members - self.assertEqual(response.status_code, 403) - - def test_is_user_staff_admin_with_staff_member(self): - """Test that a user with a StaffMember instance is identified as a staff member.""" - self.need_staff_login() - - # Ensure the user has a StaffMember instance - if not StaffMember.objects.filter(user=self.user1).exists(): - StaffMember.objects.create(user=self.user1) - - url = reverse('appointment:is_user_staff_admin') - response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - - # Check the response status code and content - self.assertEqual(response.status_code, 200) - response_data = response.json() - self.assertIn('message', response_data) - self.assertEqual(response_data['message'], _("User is a staff member.")) - - def test_is_user_staff_admin_without_staff_member(self): - """Test that a user without a StaffMember instance is not identified as a staff member.""" - self.need_staff_login() - - # Ensure the user does not have a StaffMember instance - StaffMember.objects.filter(user=self.user1).delete() - - url = reverse('appointment:is_user_staff_admin') - response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - - # Check the response status code and content - self.assertEqual(response.status_code, 200) - response_data = response.json() - self.assertIn('message', response_data) - self.assertEqual(response_data['message'], _("User is not a staff member.")) - - -class AppointmentTestCase(BaseTest): - def setUp(self): - super().setUp() - self.appointment = self.create_appointment_for_user1() - - def test_delete_appointment(self): - self.need_staff_login() - - url = reverse('appointment:delete_appointment', args=[self.appointment.id]) - response = self.client.get(url) - - self.assertEqual(response.status_code, 302) # Redirect status code - self.assertRedirects(response, reverse('appointment:get_user_appointments')) - - # Check for success messages - messages_list = list(get_messages(response.wsgi_request)) - self.assertTrue(any(_("Appointment deleted successfully!") in str(message) for message in messages_list)) - - # Check if appointment is deleted - appointment_exists = Appointment.objects.filter(pk=self.appointment.id).exists() - self.assertFalse(appointment_exists, "Appointment should be deleted but still exists.") - - def test_delete_appointment_ajax(self): - self.need_staff_login() - - url = reverse('appointment:delete_appointment_ajax') - data = json.dumps({'appointment_id': self.appointment.id}) - response = self.client.post(url, data, content_type='application/json') - - self.assertEqual(response.status_code, 200) - # Expecting both 'message' and 'success' in the response - expected_response = {"message": "Appointment deleted successfully.", "success": True} - self.assertEqual(json.loads(response.content), expected_response) - - # Check if appointment is deleted - appointment_exists = Appointment.objects.filter(pk=self.appointment.id).exists() - self.assertFalse(appointment_exists, "Appointment should be deleted but still exists.") - - def test_delete_appointment_without_permission(self): - """Test that deleting an appointment without permission fails.""" - self.need_staff_login() # Login as a regular staff user - - # Try to delete an appointment belonging to a different staff member - different_appointment = self.create_appointment_for_user2() - url = reverse('appointment:delete_appointment', args=[different_appointment.id]) - - response = self.client.post(url) - - # Check that the user is redirected due to lack of permissions - self.assertEqual(response.status_code, 403) - - # Verify that the appointment still exists in the database - self.assertTrue(Appointment.objects.filter(id=different_appointment.id).exists()) - - def test_delete_appointment_ajax_without_permission(self): - """Test that deleting an appointment via AJAX without permission fails.""" - self.need_staff_login() # Login as a regular staff user - - # Try to delete an appointment belonging to a different staff member - different_appointment = self.create_appointment_for_user2() - url = reverse('appointment:delete_appointment_ajax') - - response = self.client.post(url, {'appointment_id': different_appointment.id}, content_type='application/json') - - # Check that the response indicates failure due to lack of permissions - self.assertEqual(response.status_code, 403) - response_data = response.json() - self.assertEqual(response_data['message'], _("You can only delete your own appointments.")) - self.assertFalse(response_data['success']) - - # Verify that the appointment still exists in the database - self.assertTrue(Appointment.objects.filter(id=different_appointment.id).exists()) - - -class UpdateAppointmentTestCase(BaseTest): - def setUp(self): - super().setUp() - self.appointment = self.create_appointment_for_user1() - self.tomorrow = date.today() + timedelta(days=1) - self.data = { - 'isCreating': False, 'service_id': self.service1.pk, 'appointment_id': self.appointment.id, - 'client_name': 'Bryan Zap', - 'client_email': 'bz@gmail.com', 'client_phone': '+33769992738', 'client_address': 'Naples, Florida', - 'want_reminder': 'false', 'additional_info': '', 'start_time': '15:00:26', - 'staff_id': self.staff_member1.id, - 'date': self.tomorrow.strftime('%Y-%m-%d') - } - - def test_update_appt_min_info_create(self): - self.need_staff_login() - - # Preparing data - self.data.update({'isCreating': True, 'appointment_id': None}) - url = reverse('appointment:update_appt_min_info') - - # Making the request - response = self.client.post(url, data=json.dumps(self.data), content_type='application/json', - **{'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}) - - # Check response status - self.assertEqual(response.status_code, 200) - - # Check response content - response_data = response.json() - self.assertIn('message', response_data) - self.assertEqual(response_data['message'], 'Appointment created successfully.') - self.assertIn('appt', response_data) - - # Verify appointment created in the database - appointment_id = response_data['appt'][0]['id'] - self.assertTrue(Appointment.objects.filter(id=appointment_id).exists()) - - def test_update_appt_min_info_update(self): - self.need_superuser_login() - - # Create an appointment to update - url = reverse('appointment:update_appt_min_info') - self.staff_member1.services_offered.add(self.service1) - - # Making the request - response = self.client.post(url, data=json.dumps(self.data), content_type='application/json', - **{'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}) - print(f"response: {response.content}") - - # Check response status - self.assertEqual(response.status_code, 200) - - # Check response content - response_data = response.json() - self.assertIn('message', response_data) - self.assertEqual(response_data['message'], 'Appointment updated successfully.') - self.assertIn('appt', response_data) - - # Verify appointment updated in the database - updated_appt = Appointment.objects.get(id=self.appointment.id) - self.assertEqual(updated_appt.client.email, self.data['client_email']) - - def test_update_nonexistent_appointment(self): - self.need_superuser_login() - - # Preparing data with a non-existent appointment ID - self.data['appointment_id'] = 999 - url = reverse('appointment:update_appt_min_info') - - # Making the request - response = self.client.post(url, data=json.dumps(self.data), content_type='application/json', - **{'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}) - - # Check response status and content - self.assertEqual(response.status_code, 404) - response_data = response.json() - self.assertIn('message', response_data) - self.assertEqual(response_data['message'], "Appointment does not exist.") - - def test_update_with_nonexistent_service(self): - self.need_superuser_login() - - # Preparing data with a non-existent service ID - self.data['service_id'] = 999 - url = reverse('appointment:update_appt_min_info') - - # Making the request - response = self.client.post(url, data=json.dumps(self.data), content_type='application/json', - **{'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}) - - # Check response status and content - self.assertEqual(response.status_code, 404) - response_data = response.json() - self.assertIn('message', response_data) - self.assertEqual(response_data['message'], "Service does not exist.") - - def test_update_with_invalid_data_causing_exception(self): - self.need_superuser_login() - - # Preparing invalid data to trigger an exception, for example here, no email address - data = { - 'isCreating': False, 'service_id': '1', 'appointment_id': self.appointment.id, - } - url = reverse('appointment:update_appt_min_info') - - # Making the request - response = self.client.post(url, data=json.dumps(data), content_type='application/json', - **{'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}) - - # Check response status and content - self.assertEqual(response.status_code, 400) - response_data = response.json() - self.assertIn('message', response_data) - - -class ServiceViewTestCase(BaseTest): - def setUp(self): - super().setUp() - - def test_fetch_service_list_for_staff(self): - self.need_staff_login() - - # Assuming self.service1 and self.service2 are services linked to self.staff_member1 - self.staff_member1.services_offered.add(self.service1, self.service2) - staff_member_services = [self.service1, self.service2] - - # Simulate a request without appointmentId - url = reverse('appointment:fetch_service_list_for_staff') - response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - response_data = response.json() - self.assertEqual(response_data["message"], "Successfully fetched services.") - self.assertCountEqual( - response_data["services_offered"], - [{"id": service.id, "name": service.name} for service in staff_member_services] - ) - - # Create a test appointment and link it to self.staff_member1 - test_appointment = self.create_appointment_for_user1() - - # Simulate a request with appointmentId - url_with_appointment = f"{url}?appointmentId={test_appointment.id}" - response_with_appointment = self.client.get(url_with_appointment, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response_with_appointment.status_code, 200) - response_data_with_appointment = response_with_appointment.json() - self.assertEqual(response_data_with_appointment["message"], "Successfully fetched services.") - # Assuming the staff member linked to the appointment offers the same services - self.assertCountEqual( - response_data_with_appointment["services_offered"], - [{"id": service.id, "name": service.name} for service in staff_member_services] - ) - - def test_fetch_service_list_for_staff_no_staff_member_instance(self): - """Test that a superuser without a StaffMember instance receives no inappropriate error message.""" - self.need_superuser_login() - - # Ensure the superuser does not have a StaffMember instance - StaffMember.objects.filter(user=self.user1).delete() - - url = reverse('appointment:fetch_service_list_for_staff') - response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - - # Check the response status code and content - self.assertEqual(response.status_code, 200) - response_data = response.json() - self.assertIn('message', response_data) - - def test_fetch_service_list_for_staff_no_services_offered(self): - """Test fetching services for a staff member who offers no services.""" - self.need_staff_login() - - # Assuming self.staff_member1 offers no services - self.staff_member1.services_offered.clear() - - url = reverse('appointment:fetch_service_list_for_staff') - response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - - # Check response status code and content - self.assertEqual(response.status_code, 404) - response_data = response.json() - self.assertIn('message', response_data) - self.assertEqual(response_data['message'], _("No services offered by this staff member.")) - self.assertFalse(response_data['success']) - - def test_delete_service_with_superuser(self): - self.need_superuser_login() - # Test deletion with a superuser - response = self.client.get(reverse('appointment:delete_service', args=[self.service1.id])) - - # Check if the service was deleted - self.assertFalse(Service.objects.filter(id=self.service1.id).exists()) - - # Check if the success message is added - messages_ = list(get_messages(response.wsgi_request)) - self.assertIn(_("Service deleted successfully!"), [m.message for m in messages_]) - - # Check if it redirects to the user profile - self.assertRedirects(response, reverse('appointment:user_profile')) - - def test_delete_service_without_superuser(self): - # Log in as a regular/staff user - self.need_staff_login() - - response = self.client.get(reverse('appointment:delete_service', args=[self.service1.id])) - - # Check for a forbidden status code, as only superusers should be able to delete services - self.assertEqual(response.status_code, 403) - - def test_delete_nonexistent_service(self): - self.need_superuser_login() - # Try to delete a service that does not exist - response = self.client.get(reverse('appointment:delete_service', args=[99999])) - - # Check for a 404-status code - self.assertEqual(response.status_code, 404) - - -class AppointmentDisplayViewTestCase(BaseTest): - def setUp(self): - super().setUp() - self.appointment = self.create_appointment_for_user1() - self.url_display_appt = reverse('appointment:display_appointment', args=[self.appointment.id]) - - def test_display_appointment_authenticated_staff_user(self): - # Log in as staff user - self.need_staff_login() - response = self.client.get(self.url_display_appt) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'administration/display_appointment.html') - - def test_display_appointment_authenticated_superuser(self): - # Log in as superuser - self.need_superuser_login() - response = self.client.get(self.url_display_appt) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'administration/display_appointment.html') - - def test_display_appointment_unauthenticated_user(self): - # Attempt access without logging in - response = self.client.get(self.url_display_appt) - self.assertNotEqual(response.status_code, 200) # Expect redirection or error - - def test_display_appointment_authenticated_unauthorized_user(self): - # Log in as a regular user - self.need_normal_login() - response = self.client.get(self.url_display_appt) - self.assertNotEqual(response.status_code, 200) # Expect redirection or error - - def test_display_appointment_non_existent(self): - # Log in as staff user - self.need_superuser_login() - non_existent_url = reverse('appointment:display_appointment', args=[99999]) # Non-existent appointment ID - response = self.client.get(non_existent_url) - self.assertEqual(response.status_code, 404) # Expect 404 error - - -class DayOffViewsTestCase(BaseTest): - def setUp(self): - super().setUp() - self.url_add_day_off = reverse('appointment:add_day_off', args=[self.staff_member1.user_id]) - self.other_staff_member = self.staff_member2 - self.day_off = DayOff.objects.create(staff_member=self.staff_member1, - start_date=date.today() + timedelta(days=1), - end_date=date.today() + timedelta(days=2), description="Day off") - - def test_add_day_off_authenticated_staff_user(self): - # Log in as staff user - self.need_staff_login() - response = self.client.post(self.url_add_day_off, data={'start_date': '2050-01-01', 'end_date': '2050-01-01', - 'description': 'Test reason'}) - self.assertEqual(response.status_code, 200) # Assuming success redirects or shows a success message - - def test_add_day_off_authenticated_superuser_for_other(self): - # Log in as superuser - self.need_superuser_login() - other_staff_user_id = self.other_staff_member.user.pk - response = self.client.post(reverse('appointment:add_day_off', args=[other_staff_user_id]), - data={'start_date': '2023-01-02', 'end_date': '2050-01-01', - 'description': 'Admin adding for staff'}) - self.assertEqual(response.status_code, 200) # Assuming superuser can add for others - - def test_add_day_off_unauthenticated_user(self): - # Attempt access without logging in - response = self.client.post(self.url_add_day_off, data={'start_date': '2050-01-01', 'end_date': '2050-01-01', - 'description': 'Test reason'}) - self.assertNotEqual(response.status_code, 200) # Expect redirection or error - - def test_add_day_off_authenticated_unauthorized_user(self): - # Log in as a regular user - self.need_normal_login() - unauthorized_staff_user_id = self.other_staff_member.user.pk - response = self.client.post(reverse('appointment:add_day_off', args=[unauthorized_staff_user_id]), - data={'start_date': '2050-01-01', 'end_date': '2050-01-01', - 'description': 'Trying to add for others'}) - self.assertNotEqual(response.status_code, 200) # Expect redirection or error due to unauthorized action - - def test_update_day_off_authenticated_staff_user(self): - # Log in as staff user who owns the day off - self.need_staff_login() - url = reverse('appointment:update_day_off', args=[self.day_off.id]) - response = self.client.post(url, {'start_date': '2050-01-01', 'end_date': '2050-01-01', - 'description': 'Updated reason'}) - self.assertEqual(response.status_code, 200) - - def test_update_day_off_unauthorized_user(self): - # Log in as another staff user - self.need_normal_login() - url = reverse('appointment:update_day_off', args=[self.day_off.id]) - response = self.client.post(url, {'start_date': '2050-01-01', 'end_date': '2050-01-01', - 'description': 'Trying unauthorized update'}, 'json') - self.assertEqual(response.status_code, 403) # Expect forbidden error - - def test_update_nonexistent_day_off(self): - self.need_staff_login() - non_existent_day_off_id = 99999 - url = reverse('appointment:update_day_off', args=[non_existent_day_off_id]) - response = self.client.post(url, data={'start_date': '2050-01-01', 'end_date': '2050-01-01', - 'description': 'Non existent day off'}) - self.assertEqual(response.status_code, 404) # Expect 404 error - - def test_delete_day_off_authenticated_super_user(self): - # Log in as staff user - self.need_superuser_login() - url = reverse('appointment:delete_day_off', args=[self.day_off.id]) - response = self.client.get(url) - self.assertEqual(response.status_code, 302) # Assuming success redirects to the user profile - - def test_delete_day_off_unauthorized_user(self): - # Log in as another staff user - self.need_normal_login() - url = reverse('appointment:delete_day_off', args=[self.day_off.id]) - response = self.client.get(url) - self.assertEqual(response.status_code, 403) # Expect access denied - - def test_delete_nonexistent_day_off(self): - self.need_staff_login() - non_existent_day_off_id = 99999 - url = reverse('appointment:delete_day_off', args=[non_existent_day_off_id]) - response = self.client.get(url) - self.assertEqual(response.status_code, 404) - - -class ViewsTestCase(BaseTest): - def setUp(self): - super().setUp() - self.client = Client() - self.factory = RequestFactory() - self.request = self.factory.get('/') - self.staff_member = self.staff_member1 - WorkingHours.objects.create(staff_member=self.staff_member1, day_of_week=0, - start_time=datetime.time(8, 0), end_time=datetime.time(12, 0)) - WorkingHours.objects.create(staff_member=self.staff_member1, day_of_week=2, - start_time=datetime.time(8, 0), end_time=datetime.time(12, 0)) - self.ar = self.create_appt_request_for_sm1() - self.user1.is_staff = True - self.request.user = self.user1 - - def test_get_next_available_date_ajax(self): - """get_next_available_date_ajax view should return a JSON response with the next available date.""" - data = {'staff_id': self.staff_member.id} - url = reverse('appointment:request_next_available_slot', args=[self.service1.id]) - response = self.client.get(url, data=data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - response_data = json.loads(response.content) - self.assertIsNotNone(response_data) - self.assertIsNotNone(response_data['next_available_date']) - - def test_default_thank_you(self): - """Test if the default thank you page can be rendered.""" - appointment = Appointment.objects.create(client=self.user1, appointment_request=self.ar) - url = reverse('appointment:default_thank_you', args=[appointment.id]) - response = self.client.get(url) - self.assertEqual(response.status_code, 200) - self.assertIn(appointment.get_service_name(), str(response.content)) - - -class AddStaffMemberInfoTestCase(ViewsTestCase): - - def setUp(self): - super().setUp() - self.staff_member = self.staff_member1 - self.url = reverse('appointment:add_staff_member_info') - self.user_test = self.create_user_( - first_name="Great Tester", email="great.tester@django-appointment.com", username="great_tester" - ) - self.data = { - "user": self.user_test.id, - "services_offered": [self.service1.id, self.service2.id], - "working_hours": [ - {"day_of_week": 0, "start_time": "08:00", "end_time": "12:00"}, - {"day_of_week": 2, "start_time": "08:00", "end_time": "12:00"} - ] - } - - def test_add_staff_member_info_access_by_superuser(self): - """Test that the add staff member page is accessible by a superuser.""" - self.need_superuser_login() - response = self.client.get(self.url) - self.assertEqual(response.status_code, 200) - self.assertIsInstance(response.context['form'], StaffMemberForm) - - def test_add_staff_member_info_access_by_non_superuser(self): - """Test that non-superusers cannot access the add staff member page.""" - self.need_staff_login() - response = self.client.get(self.url) - self.assertEqual(response.status_code, 403) - - def test_add_staff_member_info_successful_submission(self): - """Test successful submission of the "add staff member" form.""" - self.need_superuser_login() - response = self.client.post(self.url, data=self.data) - self.assertEqual(response.status_code, 302) # Expect a redirect - self.assertTrue(StaffMember.objects.filter(user=self.user_test).exists()) - - def test_add_staff_member_info_invalid_form_submission(self): - """Test submission of an invalid form.""" - self.need_superuser_login() - data = self.data.copy() - data.pop('user') - response = self.client.post(self.url, data=data) - self.assertEqual(response.status_code, 200) - self.assertIsInstance(response.context['form'], StaffMemberForm) - self.assertTrue(response.context['form'].errors) - - -class SetPasswordViewTests(BaseTest): - def setUp(self): - super().setUp() - user_data = { - 'username': 'test_user', 'email': 'test@example.com', 'password': 'oldpassword', 'first_name': 'John', - 'last_name': 'Doe' - } - self.user = create_user_with_username(user_data) - self.token = PasswordResetToken.create_token(user=self.user, expiration_minutes=2880) # 2 days expiration - self.ui_db64 = urlsafe_base64_encode(force_bytes(self.user.pk)) - self.relative_set_passwd_link = reverse('appointment:set_passwd', args=[self.ui_db64, self.token.token]) - self.valid_link = reverse('appointment:set_passwd', args=[self.ui_db64, str(self.token.token)]) - - def test_get_request_with_valid_token(self): - assert PasswordResetToken.objects.filter(user=self.user, token=self.token.token).exists(), ("Token not found " - "in database") - response = self.client.get(self.valid_link) - self.assertEqual(response.status_code, 200) - self.assertContains(response, "form") - self.assertNotContains(response, "The password reset link is invalid or has expired.") - - def test_post_request_with_valid_token_and_correct_password(self): - new_password_data = {'new_password1': 'newstrongpassword123', 'new_password2': 'newstrongpassword123'} - response = self.client.post(self.valid_link, new_password_data) - self.user.refresh_from_db() - self.assertTrue(self.user.check_password(new_password_data['new_password1'])) - messages_ = list(get_messages(response.wsgi_request)) - self.assertTrue(any(msg.message == _("Password reset successfully.") for msg in messages_)) - - def test_get_request_with_expired_token(self): - expired_token = PasswordResetToken.create_token(user=self.user, expiration_minutes=-60) - expired_token_link = reverse('appointment:set_passwd', args=[self.ui_db64, str(expired_token.token)]) - response = self.client.get(expired_token_link) - self.assertEqual(response.status_code, 200) - self.assertIn('messages', response.context) - self.assertEqual(response.context['page_message'], passwd_error) - - def test_get_request_with_invalid_token(self): - invalid_token = str(uuid.uuid4()) - invalid_token_link = reverse('appointment:set_passwd', args=[self.ui_db64, invalid_token]) - response = self.client.get(invalid_token_link, follow=True) - self.assertEqual(response.status_code, 200) - messages_ = list(get_messages(response.wsgi_request)) - self.assertTrue( - any(msg.message == _("The password reset link is invalid or has expired.") for msg in messages_)) - - def test_post_request_with_invalid_token(self): - invalid_token = str(uuid.uuid4()) - invalid_token_link = reverse('appointment:set_passwd', args=[self.ui_db64, invalid_token]) - new_password = 'newpassword123' - post_data = {'new_password1': new_password, 'new_password2': new_password} - response = self.client.post(invalid_token_link, post_data) - self.user.refresh_from_db() - self.assertFalse(self.user.check_password(new_password)) - messages_ = list(get_messages(response.wsgi_request)) - self.assertTrue(any(_("The password reset link is invalid or has expired.") in str(m) for m in messages_)) - - def test_post_request_with_expired_token(self): - expired_token = PasswordResetToken.create_token(user=self.user, expiration_minutes=-60) - expired_token_link = reverse('appointment:set_passwd', args=[self.ui_db64, str(expired_token.token)]) - new_password_data = {'new_password1': 'newpassword', 'new_password2': 'newpassword'} - response = self.client.post(expired_token_link, new_password_data) - self.assertEqual(response.status_code, 200) - messages_ = list(get_messages(response.wsgi_request)) - self.assertTrue( - any(msg.message == _("The password reset link is invalid or has expired.") for msg in messages_)) - - -class GetNonWorkingDaysAjaxTests(BaseTest): - def setUp(self): - super().setUp() - self.client = Client() - self.url = reverse('appointment:get_non_working_days_ajax') - - def test_no_staff_member_selected(self): - """Test the response when no staff member is selected.""" - response = self.client.get(self.url, {'staff_id': 'none'}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - response_data = response.json() - self.assertFalse(response_data['success']) - self.assertEqual(response_data['message'], _('No staff member selected')) - self.assertIn('errorCode', response_data) - self.assertEqual(response_data['errorCode'], ErrorCode.STAFF_ID_REQUIRED.value) - - def test_valid_staff_member_selected(self): - """Test the response for a valid staff member selection.""" - response = self.client.get(self.url, {'staff_id': self.staff_member1.id}, - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - response_data = response.json() - self.assertTrue(response_data['success']) - self.assertEqual(response_data['message'], _('Successfully retrieved non-working days')) - self.assertIn('non_working_days', response_data) - self.assertTrue(isinstance(response_data['non_working_days'], list)) - - def test_ajax_required(self): - """Ensure the view only responds to AJAX requests.""" - non_ajax_response = self.client.get(self.url, {'staff_id': self.staff_member1.id}) - self.assertEqual(non_ajax_response.status_code, 200) - - -class AppointmentClientInformationTest(BaseTest): - def setUp(self): - super().setUp() - self.client = Client() - self.ar = self.create_appt_request_for_sm1() - self.url = reverse('appointment:appointment_client_information', args=[self.ar.pk, self.ar.id_request]) - self.factory = RequestFactory() - self.request = self.factory.get('/') - self.valid_form_data = { - 'name': 'Test Client', - 'service_id': '1', - 'payment_type': 'full', - 'email': 'testuser@example.com', - 'phone': '+1234567890', - 'address': '123 Test St.', - } - - def test_get_request(self): - """Test the view with a GET request.""" - response = self.client.get(self.url) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'appointment/appointment_client_information.html') - - def test_post_request_invalid_form(self): - """Test the view with an invalid POST request.""" - response = self.client.post(self.url, {}) # Empty data for invalid form - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'appointment/appointment_client_information.html') - - def test_already_submitted_session(self): - """Test the view when the appointment has already been submitted.""" - session = self.client.session - session[f'appointment_submitted_{self.ar.id_request}'] = True - session.save() - - response = self.client.get(self.url) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'error_pages/304_already_submitted.html') - - -class PrepareRescheduleAppointmentViewTests(BaseTest): - def setUp(self): - super().setUp() - self.client = Client() - self.ar = self.create_appt_request_for_sm1() - self.url = reverse('appointment:prepare_reschedule_appointment', args=[self.ar.id_request]) - - @patch('appointment.utils.db_helpers.can_appointment_be_rescheduled', return_value=True) - def test_reschedule_appointment_allowed(self, mock_can_appointment_be_rescheduled): - response = self.client.get(self.url) - self.assertEqual(response.status_code, 200) - self.assertIn('all_staff_members', response.context) - self.assertIn('available_slots', response.context) - self.assertIn('service', response.context) - self.assertIn('staff_member', response.context) - - def test_reschedule_appointment_not_allowed(self): - self.service1.reschedule_limit = 0 - self.service1.allow_rescheduling = True - self.service1.save() - response = self.client.get(self.url) - self.assertEqual(response.status_code, 403) - self.assertTemplateUsed(response, 'error_pages/403_forbidden_rescheduling.html') - - def test_reschedule_appointment_context_data(self): - Config.objects.create(app_offered_by_label="Test Label") - response = self.client.get(self.url) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.context['label'], "Test Label") - self.assertEqual(response.context['page_title'], f"Rescheduling appointment for {self.service1.name}") - self.assertTrue('date_chosen' in response.context) - self.assertTrue('page_description' in response.context) - self.assertTrue('timezoneTxt' in response.context) - - -class RescheduleAppointmentSubmitViewTests(BaseTest): - def setUp(self): - super().setUp() - self.client = Client() - self.ar = self.create_appt_request_for_sm1(date_=timezone.now().date() + datetime.timedelta(days=1)) - self.appointment = self.create_appointment_for_user1(appointment_request=self.ar) - self.url = reverse('appointment:reschedule_appointment_submit') - self.post_data = { - 'appointment_request_id': self.ar.id_request, - 'date': (timezone.now().date() + datetime.timedelta(days=2)).isoformat(), - 'start_time': '10:00', - 'end_time': '11:00', - 'staff_member': self.staff_member1.id, - 'reason_for_rescheduling': 'Need a different time', - } - - def test_post_request_with_valid_form(self): - with patch('appointment.views.AppointmentRequestForm.is_valid', return_value=True), \ - patch('appointment.views.send_reschedule_confirmation_email') as mock_send_email: - response = self.client.post(self.url, self.post_data) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'appointment/rescheduling_thank_you.html') - mock_send_email.assert_called_once() - self.assertTrue(AppointmentRescheduleHistory.objects.exists()) - - def test_post_request_with_invalid_form(self): - # Simulate an invalid form submission - response = self.client.post(self.url, {}) - self.assertEqual(response.status_code, 404) - - def test_get_request(self): - response = self.client.get(self.url) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'appointment/appointments.html') - - def test_reschedule_not_allowed(self): - # Simulate the scenario where rescheduling is not allowed by setting the reschedule limit to 0 - self.service1.reschedule_limit = 0 - self.service1.allow_rescheduling = False - self.service1.save() - - response = self.client.post(self.url, self.post_data) - self.assertEqual(response.status_code, 200) - self.assertTemplateUsed(response, 'appointment/appointments.html') - messages_list = list(get_messages(response.wsgi_request)) - self.assertTrue(any( - _("There was an error in your submission. Please check the form and try again.") in str(message) for message - in messages_list)) - - -class ConfirmRescheduleViewTests(BaseTest): - def setUp(self): - super().setUp() - self.client = Client() - self.ar = self.create_appt_request_for_sm1() - self.create_appointment_for_user1(appointment_request=self.ar) - self.reschedule_history = AppointmentRescheduleHistory.objects.create( - appointment_request=self.ar, - date=timezone.now().date() + timezone.timedelta(days=2), - start_time='10:00', - end_time='11:00', - staff_member=self.staff_member1, - id_request='unique_id_request', - reschedule_status='pending' - ) - self.url = reverse('appointment:confirm_reschedule', args=[self.reschedule_history.id_request]) - - def test_confirm_reschedule_valid(self): - response = self.client.get(self.url) - self.reschedule_history.refresh_from_db() - self.ar.refresh_from_db() - self.assertEqual(self.reschedule_history.reschedule_status, 'confirmed') - self.assertEqual(response.status_code, 302) # Redirect to thank you page - self.assertRedirects(response, reverse('appointment:default_thank_you', args=[self.ar.appointment.id])) - - def test_confirm_reschedule_invalid_status(self): - self.reschedule_history.reschedule_status = 'confirmed' - self.reschedule_history.save() - response = self.client.get(self.url) - self.assertEqual(response.status_code, 404) # Render 404_not_found with error message - - def test_confirm_reschedule_no_longer_valid(self): - with patch('appointment.models.AppointmentRescheduleHistory.still_valid', return_value=False): - response = self.client.get(self.url) - self.assertEqual(response.status_code, 404) # Render 404_not_found with error message - - def test_confirm_reschedule_updates(self): - """Ensure that the appointment request and reschedule history are updated correctly.""" - self.client.get(self.url) - self.ar.refresh_from_db() - self.reschedule_history.refresh_from_db() - self.assertEqual(self.ar.staff_member, self.reschedule_history.staff_member) - self.assertEqual(self.reschedule_history.reschedule_status, 'confirmed') - - @patch('appointment.views.notify_admin_about_reschedule') - def test_notify_admin_about_reschedule_called(self, mock_notify_admin): - self.client.get(self.url) - mock_notify_admin.assert_called_once() - self.assertTrue(mock_notify_admin.called) - - -class RedirectToPaymentOrThankYouPageTests(BaseTest): - def setUp(self): - super().setUp() - self.appointment = self.create_appointment_for_user1() - - @patch('appointment.views.APPOINTMENT_PAYMENT_URL', 'http://example.com/payment/') - @patch('appointment.views.create_payment_info_and_get_url') - def test_redirect_to_payment_page(self, mock_create_payment_info_and_get_url): - """Test redirection to the payment page when APPOINTMENT_PAYMENT_URL is set.""" - mock_create_payment_info_and_get_url.return_value = 'http://example.com/payment/12345' - response = redirect_to_payment_or_thank_you_page(self.appointment) - - self.assertIsInstance(response, HttpResponseRedirect) - self.assertEqual(response.url, 'http://example.com/payment/12345') - - @patch('appointment.views.APPOINTMENT_PAYMENT_URL', '') - @patch('appointment.views.APPOINTMENT_THANK_YOU_URL', 'appointment:default_thank_you') - def test_redirect_to_custom_thank_you_page(self): - """Test redirection to a custom thank-you page when APPOINTMENT_THANK_YOU_URL is set.""" - response = redirect_to_payment_or_thank_you_page(self.appointment) - - self.assertIsInstance(response, HttpResponseRedirect) - self.assertTrue(response.url.startswith( - reverse('appointment:default_thank_you', kwargs={'appointment_id': self.appointment.id}))) - - @patch('appointment.views.APPOINTMENT_PAYMENT_URL', '') - @patch('appointment.views.APPOINTMENT_THANK_YOU_URL', '') - def test_redirect_to_default_thank_you_page(self): - """Test redirection to the default thank-you page when no specific URL is set.""" - response = redirect_to_payment_or_thank_you_page(self.appointment) - - self.assertIsInstance(response, HttpResponseRedirect) - self.assertTrue(response.url.startswith( - reverse('appointment:default_thank_you', kwargs={'appointment_id': self.appointment.id}))) - - -class CreateAppointmentTests(BaseTest): - def setUp(self): - super().setUp() - self.appointment_request = self.create_appt_request_for_sm1() - self.client_data = {'name': 'John Doe', 'email': 'john@example.com'} - self.appointment_data = {'phone': '1234567890', 'want_reminder': True, 'address': '123 Test St.', - 'additional_info': 'Test info'} - self.request = RequestFactory().get('/') - - @patch('appointment.views.create_and_save_appointment') - @patch('appointment.views.redirect_to_payment_or_thank_you_page') - def test_create_appointment_success(self, mock_redirect, mock_create_and_save): - """Test successful creation of an appointment and redirection.""" - # Mock the appointment creation to return an Appointment instance - mock_appointment = MagicMock() - mock_create_and_save.return_value = mock_appointment - - # Mock the redirection function to simulate a successful redirection - mock_redirect.return_value = MagicMock() - - create_appointment(self.request, self.appointment_request, self.client_data, self.appointment_data) - - # Verify that create_and_save_appointment was called with the correct arguments - mock_create_and_save.assert_called_once_with(self.appointment_request, self.client_data, self.appointment_data, - self.request) - - # Verify that the redirect_to_payment_or_thank_you_page was called with the created appointment - mock_redirect.assert_called_once_with(mock_appointment) diff --git a/appointment/tests/utils/__init__.py b/appointment/tests/utils/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/appointment/tests/utils/test_date_time.py b/appointment/tests/utils/test_date_time.py deleted file mode 100644 index 2dc5b4a..0000000 --- a/appointment/tests/utils/test_date_time.py +++ /dev/null @@ -1,397 +0,0 @@ -# test_date_time.py -# Path: appointment/tests/utils/test_date_time.py - -import datetime -from unittest.mock import Mock, patch - -from django.test import TestCase - -from appointment.utils.date_time import ( - combine_date_and_time, convert_12_hour_time_to_24_hour_time, convert_24_hour_time_to_12_hour_time, - convert_minutes_in_human_readable_format, convert_str_to_date, - convert_str_to_time, get_ar_end_time, get_current_year, get_timestamp, get_weekday_num, - time_difference -) - - -class Convert12HourTo24HourTimeTests(TestCase): - def test_valid_basic_conversion(self): - """Test basic 12-hour to 24-hour conversions.""" - self.assertEqual(convert_12_hour_time_to_24_hour_time("01:10 AM"), "01:10:00") - self.assertEqual(convert_12_hour_time_to_24_hour_time("01:20 PM"), "13:20:00") - - def test_convert_midnight_and_noon(self): - """Test conversion of midnight and noon times.""" - self.assertEqual(convert_12_hour_time_to_24_hour_time("12:00 AM"), "00:00:00") - self.assertEqual(convert_12_hour_time_to_24_hour_time("12:00 PM"), "12:00:00") - - def test_boundary_times(self): - """Test conversion of boundary times.""" - self.assertEqual(convert_12_hour_time_to_24_hour_time("12:00 AM"), "00:00:00") - self.assertEqual(convert_12_hour_time_to_24_hour_time("11:59 PM"), "23:59:00") - - def test_datetime_and_time_objects(self): - """Test conversion using datetime and time objects.""" - dt_obj = datetime.datetime(2023, 1, 1, 14, 30) - time_obj = datetime.time(14, 30) - self.assertEqual(convert_12_hour_time_to_24_hour_time(dt_obj), "14:30:00") - self.assertEqual(convert_12_hour_time_to_24_hour_time(time_obj), "14:30:00") - - def test_case_insensitivity_and_whitespace(self): - """Test conversion handling of different case formats and white-space.""" - self.assertEqual(convert_12_hour_time_to_24_hour_time(" 12:00 am "), "00:00:00") - self.assertEqual(convert_12_hour_time_to_24_hour_time("01:00 pM "), "13:00:00") - - def test_out_of_bounds_values(self): - """Test conversion with out-of-bounds values.""" - with self.assertRaises(ValueError): - convert_12_hour_time_to_24_hour_time("13:00 PM") - with self.assertRaises(ValueError): - convert_12_hour_time_to_24_hour_time("12:60 AM") - - def test_invalid_datatypes(self): - """Test conversion with invalid data types.""" - with self.assertRaises(ValueError): - convert_12_hour_time_to_24_hour_time(["12:00 AM"]) - with self.assertRaises(ValueError): - convert_12_hour_time_to_24_hour_time({"time": "12:00 AM"}) - - def test_invalid_inputs(self): - """Test conversion with various invalid inputs.""" - with self.assertRaises(ValueError): - convert_12_hour_time_to_24_hour_time("25:00 AM") - with self.assertRaises(ValueError): - convert_12_hour_time_to_24_hour_time("01:00") - with self.assertRaises(ValueError): - convert_12_hour_time_to_24_hour_time("Random String") - with self.assertRaises(ValueError): - convert_12_hour_time_to_24_hour_time("01:60 AM") - - -class Convert24HourTimeTo12HourTimeTests(TestCase): - - def test_valid_24_hour_time_strings(self): - self.assertEqual(convert_24_hour_time_to_12_hour_time("13:00"), "01:00 PM") - self.assertEqual(convert_24_hour_time_to_12_hour_time("00:00"), "12:00 AM") - self.assertEqual(convert_24_hour_time_to_12_hour_time("23:59"), "11:59 PM") - self.assertEqual(convert_24_hour_time_to_12_hour_time("12:00"), "12:00 PM") - self.assertEqual(convert_24_hour_time_to_12_hour_time("01:00"), "01:00 AM") - - def test_valid_24_hour_time_with_seconds(self): - self.assertEqual(convert_24_hour_time_to_12_hour_time("13:00:01"), "01:00:01 PM") - self.assertEqual(convert_24_hour_time_to_12_hour_time("00:00:59"), "12:00:59 AM") - - def test_datetime_time_object_input(self): - time_input = datetime.time(13, 15) - self.assertEqual(convert_24_hour_time_to_12_hour_time(time_input), "01:15 PM") - time_input = datetime.time(0, 0) - self.assertEqual(convert_24_hour_time_to_12_hour_time(time_input), "12:00 AM") - - def test_invalid_time_strings(self): - with self.assertRaises(ValueError): - convert_24_hour_time_to_12_hour_time("25:00") - with self.assertRaises(ValueError): - convert_24_hour_time_to_12_hour_time("-01:00") - with self.assertRaises(ValueError): - convert_24_hour_time_to_12_hour_time("13:60") - with self.assertRaises(ValueError): - convert_24_hour_time_to_12_hour_time("invalid") - - def test_incorrect_format(self): - with self.assertRaises(ValueError): - convert_24_hour_time_to_12_hour_time("1 PM") - with self.assertRaises(ValueError): - convert_24_hour_time_to_12_hour_time("13 PM") - with self.assertRaises(ValueError): - convert_24_hour_time_to_12_hour_time("24:00") - - def test_edge_cases(self): - self.assertEqual(convert_24_hour_time_to_12_hour_time("12:00"), "12:00 PM") - self.assertEqual(convert_24_hour_time_to_12_hour_time("00:00"), "12:00 AM") - self.assertEqual(convert_24_hour_time_to_12_hour_time("11:59"), "11:59 AM") - self.assertEqual(convert_24_hour_time_to_12_hour_time("23:59"), "11:59 PM") - - -class ConvertMinutesInHumanReadableFormatTests(TestCase): - def test_valid_basic_conversions(self): - """Test basic conversions""" - self.assertEqual(convert_minutes_in_human_readable_format(30), "30 minutes") - self.assertEqual(convert_minutes_in_human_readable_format(90), "1 hour and 30 minutes") - - def test_edge_cases(self): - """Test edgy cases""" - self.assertEqual(convert_minutes_in_human_readable_format(59), "59 minutes") - self.assertEqual(convert_minutes_in_human_readable_format(60), "1 hour") - self.assertEqual(convert_minutes_in_human_readable_format(1439), "23 hours and 59 minutes") - # '1440' minutes is the total number of minutes in a day, hence it should convert to "1 day". - self.assertEqual(convert_minutes_in_human_readable_format(1440), "1 day") - - def test_valid_combinations(self): - """Test various combinations""" - self.assertEqual(convert_minutes_in_human_readable_format(1441), "1 day and 1 minute") - self.assertEqual(convert_minutes_in_human_readable_format(1500), "1 day and 1 hour") - self.assertEqual(convert_minutes_in_human_readable_format(1560), "1 day and 2 hours") - self.assertEqual(convert_minutes_in_human_readable_format(1501), "1 day, 1 hour and 1 minute") - self.assertEqual(convert_minutes_in_human_readable_format(1562), "1 day, 2 hours and 2 minutes") - self.assertEqual(convert_minutes_in_human_readable_format(808), "13 hours and 28 minutes") - - def test_non_positive_values(self): - """Test that non-positive values are handled correctly""" - self.assertEqual(convert_minutes_in_human_readable_format(0), "Not set.") - # Expectation for negative values might depend on desired behavior, just an example below - with self.assertRaises(ValueError): - convert_minutes_in_human_readable_format(-5) - - def test_float_values(self): - """Test float values which should be correctly rounded down""" - self.assertEqual(convert_minutes_in_human_readable_format(2.5), "2 minutes") - self.assertEqual(convert_minutes_in_human_readable_format(2.9), "2 minutes") - - def test_invalid_inputs(self): - """Test invalid inputs which should raise an error""" - with self.assertRaises(TypeError): - convert_minutes_in_human_readable_format("30 minutes") - with self.assertRaises(TypeError): - convert_minutes_in_human_readable_format(["30"]) - with self.assertRaises(TypeError): - convert_minutes_in_human_readable_format({"minutes": 30}) - with self.assertRaises(TypeError): - convert_minutes_in_human_readable_format(None) - - -class ConvertStrToDateTests(TestCase): - - def test_valid_date_strings_with_hyphen_separator(self): - """Test valid date with hyphen separator works correctly""" - self.assertEqual(convert_str_to_date("2023-12-31"), datetime.date(2023, 12, 31)) - self.assertEqual(convert_str_to_date("2020-02-29"), datetime.date(2020, 2, 29)) # Leap year - self.assertEqual(convert_str_to_date("2021-02-28"), datetime.date(2021, 2, 28)) - - def test_valid_date_strings_with_slash_separator(self): - """Test valid date with slash separator works correctly""" - self.assertEqual(convert_str_to_date("2021/01/01"), datetime.date(2021, 1, 1)) - self.assertEqual(convert_str_to_date("2023/12/31"), datetime.date(2023, 12, 31)) - self.assertEqual(convert_str_to_date("2023.12.31"), datetime.date(2023, 12, 31)) - - def test_invalid_date_formats(self): - """The date format "MM-DD-YYY" & "DD/MM/YYYY" are not supported, hence it should raise an error.""" - with self.assertRaises(ValueError): - convert_str_to_date("12-31-2023") - with self.assertRaises(ValueError): - convert_str_to_date("31/12/2023") - - def test_nonexistent_dates(self): - """Test nonexistent dates""" - with self.assertRaises(ValueError): - convert_str_to_date("2023-02-30") - with self.assertRaises(ValueError): - convert_str_to_date("2021-02-29") # Not a leap year - - def test_other_invalid_inputs(self): - """Test other invalid inputs""" - with self.assertRaises(ValueError): - convert_str_to_date("") - with self.assertRaises(ValueError): - convert_str_to_date("RandomString") - with self.assertRaises(ValueError): - convert_str_to_date("0000-00-00") - - -class ConvertStrToTimeTests(TestCase): - def test_convert_12h_format_str_to_time(self): - """Test valid time strings""" - # These tests check if a 12-hour time format string converts correctly. - self.assertEqual(convert_str_to_time("10:00 AM"), datetime.time(10, 0)) - self.assertEqual(convert_str_to_time("12:00 PM"), datetime.time(12, 0)) - self.assertEqual(convert_str_to_time("01:30 PM"), datetime.time(13, 30)) - - def test_convert_24h_format_str_to_time(self): - """Test if a 24-hour time format string converts correctly.""" - # These tests check if a 24-hour time format string converts correctly. - self.assertEqual(convert_str_to_time("10:00:00"), datetime.time(10, 0)) - self.assertEqual(convert_str_to_time("12:00:00"), datetime.time(12, 0)) - self.assertEqual(convert_str_to_time("13:30:00"), datetime.time(13, 30)) - - def test_case_insensitivity_and_whitespace(self): - """Test conversion handling of different case formats and white-space.""" - self.assertEqual(convert_str_to_time(" 12:00 am "), datetime.time(0, 0)) - self.assertEqual(convert_str_to_time("01:00 pM "), datetime.time(13, 0)) - self.assertEqual(convert_str_to_time(" 13:00:00 "), datetime.time(13, 0)) - - def test_convert_str_to_time_invalid(self): - """Test invalid time strings""" - with self.assertRaises(ValueError): - convert_str_to_time("") - with self.assertRaises(ValueError): - convert_str_to_time("13:00 PM") - with self.assertRaises(ValueError): - convert_str_to_time("25:00 AM") - with self.assertRaises(ValueError): - convert_str_to_time("25:00:00") - with self.assertRaises(ValueError): - convert_str_to_time("10:60 AM") - with self.assertRaises(ValueError): - convert_str_to_time("10:60:00") - with self.assertRaises(ValueError): - convert_str_to_time("Random String") - - -class GetAppointmentRequestEndTimeTests(TestCase): - def test_get_ar_end_time_with_valid_inputs(self): - """Test positive cases""" - self.assertEqual(get_ar_end_time("10:00:00", 60), datetime.time(11, 0)) - self.assertEqual(get_ar_end_time(datetime.time(10, 0), 120), datetime.time(12, 0)) - self.assertEqual(get_ar_end_time(datetime.time(10, 0), datetime.timedelta(hours=2)), datetime.time(12, 0)) - - def test_negative_duration(self): - """Test negative duration""" - with self.assertRaises(ValueError): - get_ar_end_time("10:00:00", -60) - - def test_invalid_start_time_format(self): - """Test invalid start time format""" - with self.assertRaises(ValueError): - get_ar_end_time("25:00:00", 60) - - def test_invalid_duration_format(self): - """Test invalid duration format""" - with self.assertRaises(TypeError): - get_ar_end_time("10:00:00", "60") - - def test_end_time_past_midnight(self): - """Test end time past midnight""" - # If the end time goes past midnight, it should wrap around to the next day, - # hence "23:30:00" + 60 minutes = "00:30:00". - self.assertEqual(get_ar_end_time("23:30:00", 60), datetime.time(0, 30)) - - def test_invalid_start_time_type(self): - """Test that an invalid start_time type raises a TypeError.""" - with self.assertRaises(TypeError): - get_ar_end_time([], 60) # Passing a list instead of a datetime.time object or string - with self.assertRaises(TypeError): - get_ar_end_time(12345, 30) # Passing an integer - with self.assertRaises(TypeError): - get_ar_end_time(None, 30) # Passing None - - -class TimeDifferenceTests(TestCase): - - def test_valid_difference_with_time_objects(self): - """Test difference between two time objects""" - time1 = datetime.time(10, 0) - time2 = datetime.time(11, 0) - difference = time_difference(time1, time2) - self.assertEqual(difference, datetime.timedelta(hours=1)) - - def test_valid_difference_with_datetime_objects(self): - """Test difference between two datetime objects""" - datetime1 = datetime.datetime(2023, 1, 1, 10, 0) - datetime2 = datetime.datetime(2023, 1, 1, 11, 0) - difference = time_difference(datetime1, datetime2) - self.assertEqual(difference, datetime.timedelta(hours=1)) - - def test_negative_difference_with_time_objects(self): - """Two time objects cannot have a negative difference""" - time1 = datetime.time(10, 0) - time2 = datetime.time(11, 0) - with self.assertRaises(ValueError): - time_difference(time2, time1) - - def test_negative_difference_with_datetime_objects(self): - """Two datetime objects cannot have a negative difference""" - datetime1 = datetime.datetime(2023, 1, 1, 10, 0) - datetime2 = datetime.datetime(2023, 1, 1, 11, 0) - with self.assertRaises(ValueError): - time_difference(datetime2, datetime1) - - def test_mismatched_input_types(self): - """Test that providing one 'datetime.time' and one datetime.datetime raises a ValueError.""" - time_obj = datetime.time(10, 0) - datetime_obj = datetime.datetime(2023, 1, 1, 11, 0) - - with self.assertRaises(ValueError) as context: - time_difference(time_obj, datetime_obj) - - self.assertEqual(str(context.exception), - "Both inputs should be of the same type, either datetime.time or datetime.datetime") - - # Test the reverse case as well for completeness - with self.assertRaises(ValueError) as context: - time_difference(datetime_obj, time_obj) - - self.assertEqual(str(context.exception), - "Both inputs should be of the same type, either datetime.time or datetime.datetime") - - -class CombineDateAndTimeTests(TestCase): - def test_combine_valid_date_and_time(self): - """Test combining a valid date and time.""" - date = datetime.date(2023, 1, 1) - time = datetime.time(12, 30) - expected_datetime = datetime.datetime(2023, 1, 1, 12, 30) - result = combine_date_and_time(date, time) - self.assertEqual(result, expected_datetime) - - def test_combine_with_midnight(self): - """Test combining a date with a midnight time.""" - date = datetime.date(2023, 1, 1) - time = datetime.time(0, 0) - expected_datetime = datetime.datetime(2023, 1, 1, 0, 0) - result = combine_date_and_time(date, time) - self.assertEqual(result, expected_datetime) - - def test_combine_with_leap_year_date(self): - """Test combining a leap year date and time.""" - date = datetime.date(2024, 2, 29) - time = datetime.time(23, 59) - expected_datetime = datetime.datetime(2024, 2, 29, 23, 59) - result = combine_date_and_time(date, time) - self.assertEqual(result, expected_datetime) - - def test_combine_with_end_of_day(self): - """Test combining a date with 'end of day time'.""" - date = datetime.date(2023, 1, 1) - time = datetime.time(23, 59, 59) - expected_datetime = datetime.datetime(2023, 1, 1, 23, 59, 59) - result = combine_date_and_time(date, time) - self.assertEqual(result, expected_datetime) - - def test_combine_with_microseconds(self): - """Test combining a date and time with microseconds.""" - date = datetime.date(2023, 1, 1) - time = datetime.time(12, 30, 15, 123456) - expected_datetime = datetime.datetime(2023, 1, 1, 12, 30, 15, 123456) - result = combine_date_and_time(date, time) - self.assertEqual(result, expected_datetime) - - -class TimestampTests(TestCase): - @patch('appointment.utils.date_time.timezone.now') - def test_get_timestamp(self, mock_now): - """Test get_timestamp function""" - mock_datetime = Mock() - mock_datetime.timestamp.return_value = 1612345678.1234 # Sample timestamp with decimal - mock_now.return_value = mock_datetime - - self.assertEqual(get_timestamp(), "16123456781234") - - -class GeneralDateTimeTests(TestCase): - def test_get_current_year(self): - """Test get_current_year function""" - self.assertEqual(get_current_year(), datetime.datetime.now().year) - - def test_get_current_year_mocked(self): - """Test get_current_year function with a mocked year.""" - with patch('appointment.utils.date_time.datetime.datetime') as mock_date: - mock_date.now.return_value.year = 1999 # Setting year attribute of the mock object - self.assertEqual(get_current_year(), 1999) - - def test_get_weekday_num(self): - """Test get_weekday_num function with valid input""" - self.assertEqual(get_weekday_num("Monday"), 1) - self.assertEqual(get_weekday_num("Sunday"), 0) - - def test_invalid_get_weekday_num(self): - """Test get_weekday_num function with invalid input which should return -1""" - self.assertEqual(get_weekday_num("InvalidDay"), -1) diff --git a/appointment/tests/utils/test_db_helpers.py b/appointment/tests/utils/test_db_helpers.py deleted file mode 100644 index bee36da..0000000 --- a/appointment/tests/utils/test_db_helpers.py +++ /dev/null @@ -1,1145 +0,0 @@ -# test_db_helpers.py -# Path: appointment/tests/utils/test_db_helpers.py - -import datetime -from unittest.mock import MagicMock, PropertyMock, patch - -from django.apps import apps -from django.conf import settings -from django.core.cache import cache -from django.core.exceptions import FieldDoesNotExist -from django.test import TestCase -from django.test.client import RequestFactory -from django.urls import reverse -from django.utils import timezone -from django_q.models import Schedule - -from appointment.models import DayOff, PaymentInfo -from appointment.tests.base.base_test import BaseTest -from appointment.tests.mixins.base_mixin import ConfigMixin -from appointment.utils.db_helpers import ( - Config, WorkingHours, calculate_slots, calculate_staff_slots, can_appointment_be_rescheduled, - cancel_existing_reminder, check_day_off_for_staff, - create_and_save_appointment, create_new_user, create_payment_info_and_get_url, create_user_with_email, - day_off_exists_for_date_range, - exclude_booked_slots, exclude_pending_reschedules, generate_unique_username_from_email, get_absolute_url_, - get_all_appointments, - get_all_staff_members, - get_appointment_buffer_time, get_appointment_by_id, get_appointment_finish_time, get_appointment_lead_time, - get_appointment_slot_duration, get_appointments_for_date_and_time, get_config, get_day_off_by_id, - get_non_working_days_for_staff, get_staff_member_appointment_list, get_staff_member_buffer_time, - get_staff_member_by_user_id, get_staff_member_end_time, get_staff_member_from_user_id_or_logged_in, - get_staff_member_slot_duration, get_staff_member_start_time, get_times_from_config, get_user_by_email, - get_user_model, get_website_name, get_weekday_num_from_date, get_working_hours_by_id, - get_working_hours_for_staff_and_day, is_working_day, parse_name, schedule_email_reminder, - staff_change_allowed_on_reschedule, update_appointment_reminder, username_in_user_model, working_hours_exist -) - - -class TestCalculateSlots(TestCase): - def setUp(self): - self.start_time = datetime.datetime(2023, 10, 8, 8, 0) # 8:00 AM - self.end_time = datetime.datetime(2023, 10, 8, 12, 0) # 12:00 PM - self.slot_duration = datetime.timedelta(hours=1) # 1 hour - # Buffer time should've been datetime.datetime.now() but for the purpose of the tests, we'll use a fixed time. - self.buffer_time = datetime.datetime(2023, 10, 8, 8, 0) + self.slot_duration - - def test_multiple_slots(self): - """Buffer time goes 1 hour after the start time, it should only return three slots. - Start time: 08:00 AM\n - End time: 12:00 AM\n - Buffer time: 09:00 AM\n - Slot duration: 1 hour\n - """ - - expected = [ - datetime.datetime(2023, 10, 8, 9, 0), - datetime.datetime(2023, 10, 8, 10, 0), - datetime.datetime(2023, 10, 8, 11, 0) - ] - result = calculate_slots(self.start_time, self.end_time, self.buffer_time, self.slot_duration) - self.assertEqual(result, expected) - - def test_buffer_after_end_time(self): - """Buffer time goes beyond the end time, it should not then return any slots. - Start time: 08:00 AM\n - End time: 09:00 AM\n - Buffer time: 10:00 AM\n - """ - end_time = datetime.datetime(2023, 10, 8, 9, 0) - buffer_time = datetime.datetime(2023, 10, 8, 10, 0) - - expected = [] - result = calculate_slots(self.start_time, end_time, buffer_time, self.slot_duration) - self.assertEqual(result, expected) - - def test_one_slot_available(self): - """Buffer time goes beyond the end time, it should not then return any slots. - Start time: 08:00 AM\n - End time: 09:00 AM\n - Buffer time: 10:00 AM\n - """ - end_time = datetime.datetime(2023, 10, 8, 9, 0) - buffer_time = datetime.datetime(2023, 10, 8, 7, 30) - slot_duration = datetime.timedelta(minutes=30) - - expected = [datetime.datetime(2023, 10, 8, 8, 0), datetime.datetime(2023, 10, 8, 8, 30)] - result = calculate_slots(self.start_time, end_time, buffer_time, slot_duration) - self.assertEqual(result, expected) - - -class TestCalculateStaffSlots(BaseTest): - def setUp(self): - super().setUp() - self.slot_duration = datetime.timedelta(minutes=30) - # Not working today but tomorrow - self.date_not_working = datetime.date.today() - self.working_date = datetime.date.today() + datetime.timedelta(days=1) - weekday_num = get_weekday_num_from_date(self.working_date) - WorkingHours.objects.create( - staff_member=self.staff_member1, - day_of_week=weekday_num, - start_time=datetime.time(9, 0), - end_time=datetime.time(17, 0) - ) - self.staff_member1.appointment_buffer_time = 25.0 - self.buffer_time = datetime.timedelta(minutes=25) - - def test_calculate_slots_on_working_day_without_appointments(self): - slots = calculate_staff_slots(self.working_date, self.staff_member1) - # Slot duration is 30 minutes, so 8 working hours minus 25-minute buffer, divided by slot duration - expected_number_of_slots = int((8 * 60 - 25) / 30) - # 15 slots should be available instead of 16 because of the 25-minute buffer - self.assertEqual(len(slots), expected_number_of_slots) - - # Asserting the first slot starts at 9:30 AM because of the 25-minute buffer - self.assertEqual(slots[0].time(), datetime.time(9, 30)) - - # Asserting the last slot starts before the end time minus slot duration (16:30) - self.assertTrue((datetime.datetime.combine(self.working_date, slots[-1].time()) + - self.slot_duration).time() <= datetime.time(17, 0)) - - def test_calculate_slots_on_non_working_day(self): - """Test that no slots are returned on a day the staff member is not working.""" - slots = calculate_staff_slots(self.date_not_working, self.staff_member1) - self.assertEqual(slots, []) - - -class TestCheckDayOffForStaff(BaseTest): - def setUp(self): - super().setUp() # Call the parent class setup - # Specific setups for this test class - self.day_off1 = DayOff.objects.create(staff_member=self.staff_member1, start_date="2023-10-08", - end_date="2023-10-10") - self.day_off2 = DayOff.objects.create(staff_member=self.staff_member2, start_date="2023-10-05", - end_date="2023-10-05") - - def test_staff_member_has_day_off(self): - # Test for a date within the range of days off for staff_member1 - self.assertTrue(check_day_off_for_staff(self.staff_member1, "2023-10-09")) - - def test_staff_member_does_not_have_day_off(self): - # Test for a date outside the range of days off for staff_member1 - self.assertFalse(check_day_off_for_staff(self.staff_member1, "2023-10-11")) - - def test_another_staff_member_day_off(self): - # Test for a date within the range of days off for staff_member2 - self.assertTrue(check_day_off_for_staff(self.staff_member2, "2023-10-05")) - - def test_another_staff_member_no_day_off(self): - # Test for a date outside the range of days off for staff_member2 - self.assertFalse(check_day_off_for_staff(self.staff_member2, "2023-10-06")) - - -class TestCreateAndSaveAppointment(BaseTest): - - def setUp(self): - super().setUp() # Call the parent class setup - # Specific setups for this test class - self.ar = self.create_appt_request_for_sm1() - self.factory = RequestFactory() - self.request = self.factory.get('/') - - def test_create_and_save_appointment(self): - client_data = { - 'email': 'tester2@gmail.com', - 'name': 'Tester2', - } - appointment_data = { - 'phone': '123456789', - 'want_reminder': True, - 'address': '123 Main St', - 'additional_info': 'Additional Test Info' - } - - appointment = create_and_save_appointment(self.ar, client_data, appointment_data, self.request) - - self.assertIsNotNone(appointment) - self.assertEqual(appointment.client.email, client_data['email']) - self.assertEqual(appointment.phone, appointment_data['phone']) - self.assertEqual(appointment.want_reminder, appointment_data['want_reminder']) - self.assertEqual(appointment.address, appointment_data['address']) - self.assertEqual(appointment.additional_info, appointment_data['additional_info']) - - -def get_mock_reverse(url_name, **kwargs): - """A mocked version of the reverse function.""" - if url_name == "app:view": - return f'/mocked-url/{kwargs["kwargs"]["object_id"]}/{kwargs["kwargs"]["id_request"]}/' - return reverse(url_name, **kwargs) - - -class ScheduleEmailReminderTest(BaseTest): - def setUp(self): - super().setUp() - self.factory = RequestFactory() - self.request = self.factory.get('/') - - def test_schedule_email_reminder_cluster_running(self): - appointment = self.create_appointment_for_user1() - with patch('appointment.settings.check_q_cluster', return_value=True), \ - patch('appointment.utils.db_helpers.schedule') as mock_schedule: - schedule_email_reminder(appointment, self.request) - mock_schedule.assert_called_once() - # Further assertions can be made here based on the arguments passed to schedule - - def test_schedule_email_reminder_cluster_not_running(self): - appointment = self.create_appointment_for_user2() - with patch('appointment.settings.check_q_cluster', return_value=False), \ - patch('appointment.utils.db_helpers.logger') as mock_logger: - schedule_email_reminder(appointment, self.request) - mock_logger.warning.assert_called_with( - "Django-Q cluster is not running. Email reminder will not be scheduled.") - - -class UpdateAppointmentReminderTest(BaseTest): - def setUp(self): - super().setUp() - self.factory = RequestFactory() - self.request = self.factory.get('/') - self.appointment = self.create_appointment_for_user1() - - def test_update_appointment_reminder_date_time_changed(self): - appointment = self.create_appointment_for_user1() - new_date = timezone.now().date() + timezone.timedelta(days=10) - new_start_time = timezone.now().time() - - with patch('appointment.utils.db_helpers.schedule_email_reminder') as mock_schedule_email_reminder, \ - patch('appointment.utils.db_helpers.cancel_existing_reminder') as mock_cancel_existing_reminder: - update_appointment_reminder(appointment, new_date, new_start_time, self.request, True) - mock_cancel_existing_reminder.assert_called_once_with(appointment.id_request) - mock_schedule_email_reminder.assert_called_once() - - def test_update_appointment_reminder_no_change(self): - appointment = self.create_appointment_for_user2() - # Use existing date and time - new_date = appointment.appointment_request.date - new_start_time = appointment.appointment_request.start_time - - with patch('appointment.utils.db_helpers.schedule_email_reminder') as mock_schedule_email_reminder, \ - patch('appointment.utils.db_helpers.cancel_existing_reminder') as mock_cancel_existing_reminder: - update_appointment_reminder(appointment, new_date, new_start_time, self.request, appointment.want_reminder) - mock_cancel_existing_reminder.assert_not_called() - mock_schedule_email_reminder.assert_not_called() - - @patch('appointment.utils.db_helpers.logger') # Adjust the import path as necessary - def test_reminder_not_scheduled_due_to_user_preference(self, mock_logger): - # Scenario where user does not want a reminder - want_reminder = False - new_date = timezone.now().date() + datetime.timedelta(days=1) - new_start_time = timezone.now().time() - - update_appointment_reminder(self.appointment, new_date, new_start_time, self.request, want_reminder) - - # Check that the logger.info was called with the expected message - mock_logger.info.assert_called_once_with( - f"Reminder for appointment {self.appointment.id} is not scheduled per user's preference or past datetime." - ) - - @patch('appointment.utils.db_helpers.logger') # Adjust the import path as necessary - def test_reminder_not_scheduled_due_to_past_datetime(self, mock_logger): - # Scenario where the new datetime is in the past - want_reminder = True - new_date = timezone.now().date() - datetime.timedelta(days=1) # Date in the past - new_start_time = timezone.now().time() - - update_appointment_reminder(self.appointment, new_date, new_start_time, self.request, want_reminder) - - # Check that the logger.info was called with the expected message - mock_logger.info.assert_called_once_with( - f"Reminder for appointment {self.appointment.id} is not scheduled per user's preference or past datetime." - ) - - -# Helper method for modifying service rescheduling settings -def modify_service_rescheduling(service, **kwargs): - for key, value in kwargs.items(): - setattr(service, key, value) - service.save() - - -class CanAppointmentBeRescheduledTests(BaseTest, ConfigMixin): - def setUp(self): - super().setUp() - self.appointment = self.create_appointment_for_user1() - - @patch('appointment.models.Service.reschedule_limit', new_callable=PropertyMock) - @patch('appointment.models.Config.default_reschedule_limit', new=3) - def test_can_appointment_be_rescheduled(self, mock_reschedule_limit): - mock_reschedule_limit.return_value = 3 - self.assertTrue(can_appointment_be_rescheduled(self.appointment.appointment_request)) - - def test_appointment_cannot_be_rescheduled_due_to_service_limit(self): - modify_service_rescheduling(self.service1, allow_rescheduling=True, reschedule_limit=0) - self.assertFalse(can_appointment_be_rescheduled(self.appointment.appointment_request)) - - def test_rescheduling_allowed_exceeds_limit(self): - modify_service_rescheduling(self.service1, allow_rescheduling=True, reschedule_limit=3) - ar = self.create_appointment_request_with_histories(service=self.service1, count=4) - self.assertFalse(can_appointment_be_rescheduled(ar)) - - def test_rescheduling_with_default_limit(self): - ar = self.create_appointment_request_with_histories(service=self.service1, count=2, use_default_limit=True) - self.assertTrue(can_appointment_be_rescheduled(ar)) - self.create_appointment_reschedule_for_user1(appointment_request=ar) - self.assertFalse(can_appointment_be_rescheduled(ar)) - - # Helper method to create appointment request with rescheduled histories - def create_appointment_request_with_histories(self, service, count, use_default_limit=False): - ar = self.create_appointment_request_(service=service, staff_member=self.staff_member1) - for _ in range(count): - self.create_appointment_reschedule_for_user1(appointment_request=ar) - return ar - - -class StaffChangeAllowedOnRescheduleTests(TestCase): - @patch('appointment.models.Config.objects.first') - def test_staff_change_allowed(self, mock_config_first): - # Mock the Config object to return True for allow_staff_change_on_reschedule - mock_config = MagicMock() - mock_config.allow_staff_change_on_reschedule = True - mock_config_first.return_value = mock_config - - # Call the function and assert that staff change is allowed - self.assertTrue(staff_change_allowed_on_reschedule()) - - @patch('appointment.models.Config.objects.first') - def test_staff_change_not_allowed(self, mock_config_first): - # Mock the Config object to return False for allow_staff_change_on_reschedule - mock_config = MagicMock() - mock_config.allow_staff_change_on_reschedule = False - mock_config_first.return_value = mock_config - - # Call the function and assert that staff change is not allowed - self.assertFalse(staff_change_allowed_on_reschedule()) - - -class CreateUserWithEmailTests(TestCase): - def setUp(self): - self.User = get_user_model() - self.User.USERNAME_FIELD = 'email' - - # def test_create_user_with_full_data(self): - # """Test creating a user with complete client data.""" - # client_data = { - # 'email': 'test@example.com', - # 'first_name': 'Test', - # 'last_name': 'User', - # 'username': 'test_user', - # } - # user = create_user_with_email(client_data) - # - # # Verify that the user was created with the correct attributes - # self.assertEqual(user.email, 'test@example.com') - # self.assertEqual(user.first_name, 'Test') - # self.assertEqual(user.last_name, 'User') - # self.assertTrue(user.is_active) - # self.assertFalse(user.is_staff) - # self.assertFalse(user.is_superuser) - - # def test_create_user_with_partial_data(self): - # """Test creating a user with only an email provided.""" - # client_data = { - # 'email': 'partial@example.com', - # 'username': 'partial_user', - # # First name and last name are omitted - # } - # user = create_user_with_email(client_data) - # - # # Verify that the user was created with default values for missing attributes - # self.assertEqual(user.email, 'partial@example.com') - # self.assertEqual(user.first_name, '') - # self.assertEqual(user.last_name, '') - - # def test_create_user_with_duplicate_email(self): - # """Test attempting to create a user with a duplicate email.""" - # client_data = { - # 'email': 'duplicate@example.com', - # 'first_name': 'Original', - # 'last_name': 'User', - # 'username': 'original_user', - # } - # # Create the first user - # create_user_with_email(client_data) - # - # # Attempt to create another user with the same email - # with self.assertRaises(Exception) as context: - # create_user_with_email(client_data) - # - # # Verify that the expected exception is raised (e.g., IntegrityError for duplicate key) - # self.assertTrue('duplicate key value violates unique constraint' in str(context.exception) or - # 'UNIQUE constraint failed' in str(context.exception)) - - -class CancelExistingReminderTest(BaseTest): - def test_cancel_existing_reminder(self): - appointment = self.create_appointment_for_user1() - Schedule.objects.create(func='appointment.tasks.send_email_reminder', name=f"reminder_{appointment.id_request}") - - self.assertEqual(Schedule.objects.count(), 1) - cancel_existing_reminder(appointment.id_request) - self.assertEqual(Schedule.objects.filter(name=f"reminder_{appointment.id_request}").count(), 0) - - -class TestCreatePaymentInfoAndGetUrl(BaseTest): - - def setUp(self): - super().setUp() # Call the parent class setup - # Specific setups for this test class - self.ar = self.create_appt_request_for_sm1() - self.appointment = self.create_appointment_for_user2(appointment_request=self.ar) - - def test_create_payment_info_and_get_url_string(self): - expected_url = "https://payment.com/1/1234567890" - with patch('appointment.utils.db_helpers.APPOINTMENT_PAYMENT_URL', expected_url): - payment_url = create_payment_info_and_get_url(self.appointment) - self.assertEqual(payment_url, expected_url) - - def test_create_payment_info_and_get_url_application(self): - expected_url = "app:view" - - with patch('appointment.utils.db_helpers.APPOINTMENT_PAYMENT_URL', expected_url): - with patch('appointment.utils.db_helpers.reverse', side_effect=get_mock_reverse): - self.assertEqual(PaymentInfo.objects.count(), 0) - - # Call the function to create PaymentInfo and get the URL - payment_url = create_payment_info_and_get_url(self.appointment) - - # Now, there should be one PaymentInfo object - self.assertEqual(PaymentInfo.objects.count(), 1) - - # Fetch the newly created PaymentInfo object - payment_info = PaymentInfo.objects.first() - - # Construct the expected mocked URL - expected_mocked_url = f'/mocked-url/{payment_info.id}/{payment_info.get_id_request()}/' - - # Assert that the appointment in the PaymentInfo object matches the appointment we provided - self.assertEqual(payment_info.appointment, self.appointment) - self.assertEqual(payment_url, expected_mocked_url) - - -class TestExcludeBookedSlots(BaseTest): - - def setUp(self): - super().setUp() - self.appointment = self.create_appointment_for_user1() - - # Sample slots for testing - self.today = datetime.date.today() - - self.slots = [ - datetime.datetime.combine(self.today, datetime.time(8, 0)), - datetime.datetime.combine(self.today, datetime.time(9, 0)), - datetime.datetime.combine(self.today, datetime.time(10, 0)), - datetime.datetime.combine(self.today, datetime.time(11, 0)), - datetime.datetime.combine(self.today, datetime.time(12, 0)) - ] - self.slot_duration = datetime.timedelta(hours=1) - - def test_no_appointments(self): - result = exclude_booked_slots([], self.slots, self.slot_duration) - self.assertEqual(result, self.slots) - - def test_appointment_not_intersecting_slots(self): - self.appointment.appointment_request.start_time = datetime.time(13, 30) - self.appointment.appointment_request.end_time = datetime.time(14, 30) - self.appointment.save() - - result = exclude_booked_slots([self.appointment], self.slots, self.slot_duration) - self.assertEqual(result, self.slots) - - def test_appointment_intersecting_single_slot(self): - self.appointment.appointment_request.start_time = datetime.time(8, 0) - self.appointment.appointment_request.end_time = datetime.time(9, 0) - self.appointment.save() - - result = exclude_booked_slots([self.appointment], self.slots, self.slot_duration) - expected = [ - datetime.datetime.combine(self.today, datetime.time(9, 0)), - datetime.datetime.combine(self.today, datetime.time(10, 0)), - datetime.datetime.combine(self.today, datetime.time(11, 0)), - datetime.datetime.combine(self.today, datetime.time(12, 0)) - ] - self.assertEqual(result, expected) - - def test_multiple_overlapping_appointments(self): - ar2 = self.create_appt_request_for_sm2(start_time=datetime.time(10, 30), - end_time=datetime.time(11, 30)) - appointment2 = self.create_appointment_for_user2(appointment_request=ar2) - appointment2.save() - result = exclude_booked_slots([self.appointment, appointment2], self.slots, self.slot_duration) - expected = [ - datetime.datetime.combine(self.today, datetime.time(8, 0)), - datetime.datetime.combine(self.today, datetime.time(12, 0)) - ] - self.assertEqual(result, expected) - - -class TestDayOffExistsForDateRange(BaseTest): - - def setUp(self): - super().setUp() - self.user = self.create_user_(email="tester@gmail.com") - self.service = self.create_service_() - self.staff_member = self.create_staff_member_(user=self.user, service=self.service) - self.day_off1 = DayOff.objects.create(staff_member=self.staff_member, start_date="2023-10-08", - end_date="2023-10-10") - self.day_off2 = DayOff.objects.create(staff_member=self.staff_member, start_date="2023-10-15", - end_date="2023-10-17") - - def test_day_off_exists(self): - # Check for a date range that intersects with day_off1 - self.assertTrue(day_off_exists_for_date_range(self.staff_member, "2023-10-09", "2023-10-11")) - - def test_day_off_does_not_exist(self): - # Check for a date range that doesn't intersect with any day off - self.assertFalse(day_off_exists_for_date_range(self.staff_member, "2023-10-11", "2023-10-14")) - - def test_day_off_exists_but_excluded(self): - # Check for a date range that intersects with day_off1 but exclude day_off1 from the check using its ID - self.assertFalse( - day_off_exists_for_date_range(self.staff_member, "2023-10-09", "2023-10-11", days_off_id=self.day_off1.id)) - - def test_day_off_exists_for_other_range(self): - # Check for a date range that intersects with day_off2 - self.assertTrue(day_off_exists_for_date_range(self.staff_member, "2023-10-16", "2023-10-18")) - - -class TestGetAllAppointments(BaseTest): - - def setUp(self): - super().setUp() - self.appointment1 = self.create_appointment_for_user1() - self.appointment2 = self.create_appointment_for_user2() - - def test_get_all_appointments(self): - appointments = get_all_appointments() - self.assertEqual(len(appointments), 2) - self.assertIn(self.appointment1, appointments) - self.assertIn(self.appointment2, appointments) - - -class TestGetAllStaffMembers(BaseTest): - - def setUp(self): - super().setUp() - - def test_get_all_staff_members(self): - staff_members = get_all_staff_members() - self.assertEqual(len(staff_members), 2) - self.assertIn(self.staff_member1, staff_members) - self.assertIn(self.staff_member2, staff_members) - - -class TestGetAppointmentByID(BaseTest): - - def setUp(self): - super().setUp() - self.appointment = self.create_appointment_for_user1() - - def test_existing_appointment(self): - """Test fetching an existing appointment.""" - fetched_appointment = get_appointment_by_id(self.appointment.id) - self.assertEqual(fetched_appointment, self.appointment) - - def test_non_existing_appointment(self): - """Test attempting to fetch a non-existing appointment.""" - non_existent_id = 9999 # Assume this ID doesn't exist in the test database - fetched_appointment = get_appointment_by_id(non_existent_id) - self.assertIsNone(fetched_appointment) - - -@patch('appointment.utils.db_helpers.APPOINTMENT_BUFFER_TIME', 60) -@patch('appointment.utils.db_helpers.APPOINTMENT_LEAD_TIME', '07:00:00') -@patch('appointment.utils.db_helpers.APPOINTMENT_FINISH_TIME', '15:00:00') -@patch('appointment.utils.db_helpers.APPOINTMENT_SLOT_DURATION', 30) -@patch('appointment.utils.db_helpers.APPOINTMENT_WEBSITE_NAME', "django-appointment-website") -class TestGetAppointmentConfigTimes(TestCase): - def test_no_config_object(self): - """Test when there's no Config object in the database.""" - self.assertIsNone(Config.objects.first()) # Ensure no Config object exists - self.assertEqual(get_appointment_buffer_time(), 60) - self.assertEqual(get_appointment_lead_time(), '07:00:00') - self.assertEqual(get_appointment_finish_time(), '15:00:00') - self.assertEqual(get_appointment_slot_duration(), 30) - self.assertEqual(get_website_name(), "django-appointment-website") - - def test_config_object_no_time_set(self): - """Test with a Config object without 'slot_duration'; 'buffer', 'lead' and 'finish' time set.""" - Config.objects.create() - self.assertEqual(get_appointment_buffer_time(), 60) - self.assertEqual(get_appointment_lead_time(), '07:00:00') - self.assertEqual(get_appointment_finish_time(), '15:00:00') - self.assertEqual(get_appointment_slot_duration(), 30) - self.assertEqual(get_website_name(), "django-appointment-website") - - def test_config_object_with_finish_time(self): - """Test with a Config object with 'slot_duration'; 'buffer', 'lead' and 'finish' time set.""" - Config.objects.create(finish_time='17:00:00', lead_time='09:00:00', - appointment_buffer_time=60, slot_duration=30, website_name="config") - self.assertEqual(get_appointment_buffer_time(), 60) - self.assertEqual(get_appointment_lead_time().strftime('%H:%M:%S'), '09:00:00') - self.assertEqual(get_appointment_finish_time().strftime('%H:%M:%S'), '17:00:00') - self.assertEqual(get_appointment_slot_duration(), 30) - self.assertEqual(get_website_name(), "config") - - def test_config_not_set_but_constants_patched(self): - """Test with no Config object and patched constants.""" - self.assertEqual(get_appointment_buffer_time(), 60) - self.assertEqual(get_appointment_lead_time(), '07:00:00') - self.assertEqual(get_appointment_finish_time(), '15:00:00') - self.assertEqual(get_appointment_slot_duration(), 30) - self.assertEqual(get_website_name(), "django-appointment-website") - - -class TestGetAppointmentsForDateAndTime(BaseTest): - - def setUp(self): - super().setUp() - - # Setting up some appointment requests and appointments for testing - self.date_sample = datetime.datetime.today() - - # Creating overlapping appointments for staff_member1 - ar1 = self.create_appt_request_for_sm1(start_time=datetime.time(9, 0), end_time=datetime.time(10, 0)) - self.appointment1 = self.create_appointment_(user=self.client1, appointment_request=ar1) - - ar2 = self.create_appt_request_for_sm1(start_time=datetime.time(10, 30), end_time=datetime.time(11, 30)) - self.appointment2 = self.create_appointment_(user=self.client2, appointment_request=ar2) - - # Creating a non-overlapping appointment for staff_member1 - ar3 = self.create_appt_request_for_sm1(start_time=datetime.time(13, 0), end_time=datetime.time(14, 0)) - self.appointment3 = self.create_appointment_(user=self.client1, appointment_request=ar3) - - def test_get_appointments_overlapping_time_range(self): - """Test retrieving appointments overlapping with a specific time range.""" - appointments = get_appointments_for_date_and_time(self.date_sample, datetime.time(10, 0), datetime.time(12, 0), - self.staff_member1) - self.assertEqual(appointments.count(), 2) - self.assertIn(self.appointment1, appointments) - self.assertIn(self.appointment2, appointments) - - def test_get_appointments_outside_time_range(self): - """Test retrieving appointments outside a specific time range.""" - appointments = get_appointments_for_date_and_time(self.date_sample, datetime.time(7, 0), datetime.time(8, 30), - self.staff_member1) - self.assertEqual(appointments.count(), 0) - - def test_get_appointments_for_different_date(self): - """Test retrieving appointments for a different date.""" - appointments = get_appointments_for_date_and_time(datetime.date(2023, 10, 11), datetime.time(9, 0), - datetime.time(12, 0), self.staff_member1) - self.assertEqual(appointments.count(), 0) - - def test_get_appointments_for_different_staff_member(self): - """Test retrieving appointments for a different staff member.""" - appointments = get_appointments_for_date_and_time(self.date_sample, datetime.time(9, 0), datetime.time(12, 0), - self.staff_member2) - self.assertEqual(appointments.count(), 0) - - -class TestGetConfig(TestCase): - - def setUp(self): - # Clear the cache at the start of each test to ensure a clean state - cache.clear() - - def test_no_config_in_cache_or_db(self): - """Test when there's no Config in cache or the database.""" - config = get_config() - self.assertIsNone(config) - - def test_config_in_db_not_in_cache(self): - """Test when there's a Config object in the database but not in the cache.""" - db_config = Config.objects.create(finish_time='17:00:00') - config = get_config() - self.assertEqual(config, db_config) - - def test_config_in_cache(self): - """Test when there's a Config object in the cache.""" - db_config = Config.objects.create(finish_time='17:00:00') - cache.set('config', db_config) - # Use 'patch' to ensure the Config model isn't hit during the test - with patch('appointment.models.Config.objects.first') as mock_first: - config = get_config() - self.assertEqual(config, db_config) - mock_first.assert_not_called() # Ensure the database wasn't hit - - -class TestGetDayOffById(BaseTest): # Assuming you have a BaseTest class with some initial setups - def setUp(self): - super().setUp() # Call the parent class setup - - # Assuming you have already set up some StaffMember objects in the BaseTest - self.day_off = DayOff.objects.create(staff_member=self.staff_member1, start_date="2023-10-08", - end_date="2023-10-10") - - def test_retrieve_existing_day_off(self): - """Test retrieving an existing DayOff object.""" - retrieved_day_off = get_day_off_by_id(self.day_off.id) - self.assertEqual(retrieved_day_off, self.day_off) - - def test_nonexistent_day_off_id(self): - """Test trying to retrieve a DayOff object using a non-existent ID.""" - nonexistent_id = self.day_off.id + 1 # Just to ensure a non-existent ID, you can use any logic that suits you - retrieved_day_off = get_day_off_by_id(nonexistent_id) - self.assertIsNone(retrieved_day_off) - - -class TestGetNonWorkingDaysForStaff(BaseTest): - def setUp(self): - super().setUp() # Call the parent class setup - - self.staff_member_with_working_days = self.staff_member1 - WorkingHours.objects.create(staff_member=self.staff_member_with_working_days, day_of_week=0, - start_time=datetime.time(8, 0), end_time=datetime.time(12, 0)) - WorkingHours.objects.create(staff_member=self.staff_member_with_working_days, day_of_week=2, - start_time=datetime.time(8, 0), end_time=datetime.time(12, 0)) - - self.staff_member_without_working_days = self.staff_member2 - - def test_retrieve_non_working_days_for_staff_with_some_working_days(self): - """Test retrieving non-working days for a StaffMember with some working days set.""" - non_working_days = get_non_working_days_for_staff(self.staff_member_with_working_days.id) - expected_days = [1, 3, 4, 5, 6] - self.assertListEqual(non_working_days, expected_days) - - def test_retrieve_non_working_days_for_staff_without_working_days(self): - """Test retrieving non-working days for a StaffMember with no working days set.""" - non_working_days = get_non_working_days_for_staff(self.staff_member_without_working_days.id) - expected_days = [0, 1, 2, 3, 4, 5, 6] - self.assertListEqual(non_working_days, expected_days) - - def test_nonexistent_staff_member_id(self): - """Test trying to retrieve non-working days using a non-existent StaffMember ID.""" - nonexistent_id = self.staff_member_with_working_days.id + 100 # Just to ensure a non-existent ID - non_working_days = get_non_working_days_for_staff(nonexistent_id) - self.assertListEqual(non_working_days, []) - - -class TestGetStaffMemberAppointmentList(BaseTest): - def setUp(self): - super().setUp() - - # Creating appointments for each staff member. - self.appointment1_for_user1 = self.create_appointment_for_user1() - self.appointment2_for_user2 = self.create_appointment_for_user2() - - def test_retrieve_appointments_for_specific_staff_member(self): - """Test retrieving appointments for a specific StaffMember.""" - # Testing for staff_member1 - appointments_for_staff_member1 = get_staff_member_appointment_list(self.staff_member1) - self.assertIn(self.appointment1_for_user1, appointments_for_staff_member1) - self.assertNotIn(self.appointment2_for_user2, appointments_for_staff_member1) - - # Testing for staff_member2 - appointments_for_staff_member2 = get_staff_member_appointment_list(self.staff_member2) - self.assertIn(self.appointment2_for_user2, appointments_for_staff_member2) - self.assertNotIn(self.appointment1_for_user1, appointments_for_staff_member2) - - def test_retrieve_appointments_for_staff_member_with_no_appointments(self): - """Test retrieving appointments for a StaffMember with no appointments.""" - # Creating a new staff member with no appointments - staff_member_with_no_appointments = self.create_staff_member_(user=self.client1, service=self.service1) - appointments = get_staff_member_appointment_list(staff_member_with_no_appointments) - self.assertListEqual(list(appointments), []) - - -class TestGetWeekdayNumFromDate(TestCase): - def test_get_weekday_num_from_date(self): - """Test getting the weekday number from a date.""" - sample_dates = { - datetime.date(2023, 10, 9): 1, # Monday - datetime.date(2023, 10, 10): 2, # Tuesday - datetime.date(2023, 10, 11): 3, # Wednesday - datetime.date(2023, 10, 12): 4, # Thursday - datetime.date(2023, 10, 13): 5, # Friday - datetime.date(2023, 10, 14): 6, # Saturday - datetime.date(2023, 10, 15): 0, # Sunday - } - - for date, expected_weekday_num in sample_dates.items(): - self.assertEqual(get_weekday_num_from_date(date), expected_weekday_num) - - -@patch('appointment.utils.db_helpers.APPOINTMENT_BUFFER_TIME', 60) -@patch('appointment.utils.db_helpers.APPOINTMENT_SLOT_DURATION', 30) -class TestStaffMemberTimeFunctions(BaseTest): - """Test suite for staff member time functions.""" - - def setUp(self): - super().setUp() - - # Set staff member-specific settings - self.staff_member1.slot_duration = 15 - self.staff_member1.lead_time = datetime.time(8, 30) - self.staff_member1.finish_time = datetime.time(18, 0) - self.staff_member1.appointment_buffer_time = 45 - self.staff_member1.save() - - # Setting WorkingHours for staff_member1 for Monday - self.wh = WorkingHours.objects.create( - staff_member=self.staff_member1, - day_of_week=1, - start_time=datetime.time(9, 0), - end_time=datetime.time(17, 0) - ) - - def test_staff_member_buffer_time_with_global_setting(self): - """Test buffer time when staff member-specific setting is None.""" - self.staff_member1.appointment_buffer_time = None - self.staff_member1.save() - buffer_time = get_staff_member_buffer_time(self.staff_member1, datetime.date(2023, 10, 9)) - self.assertEqual(buffer_time, 60) # Global setting - - def test_staff_member_buffer_time_with_staff_member_setting(self): - """Test buffer time using staff member-specific setting.""" - buffer_time = get_staff_member_buffer_time(self.staff_member1, datetime.date(2023, 10, 9)) - self.assertEqual(buffer_time, 45) # Staff member specific setting - - def test_staff_member_slot_duration_with_global_setting(self): - """Test slot duration when staff member-specific setting is None.""" - self.staff_member1.slot_duration = None - self.staff_member1.save() - slot_duration = get_staff_member_slot_duration(self.staff_member1, datetime.date(2023, 10, 9)) - self.assertEqual(slot_duration, 30) # Global setting - - def test_staff_member_slot_duration_with_staff_member_setting(self): - """Test slot duration using staff member-specific setting.""" - slot_duration = get_staff_member_slot_duration(self.staff_member1, datetime.date(2023, 10, 9)) - self.assertEqual(slot_duration, 15) # Staff member specific setting - - def test_staff_member_start_time(self): - """Test start time based on WorkingHours.""" - start_time = get_staff_member_start_time(self.staff_member1, datetime.date(2023, 10, 9)) - self.assertEqual(start_time, datetime.time(9, 0)) - - def test_staff_member_end_time(self): - """Test end time based on WorkingHours.""" - end_time = get_staff_member_end_time(self.staff_member1, datetime.date(2023, 10, 9)) - self.assertEqual(end_time, datetime.time(17, 0)) - - def test_staff_member_start_time_with_lead_time(self): - """Test start time when both lead_time and WorkingHours are available.""" - start_time = get_staff_member_start_time(self.staff_member1, datetime.date(2023, 10, 9)) - self.assertEqual(start_time, self.wh.start_time) # lead_time should prevail - - def test_staff_member_end_time_with_finish_time(self): - """Test end time when both finish_time and WorkingHours are available.""" - end_time = get_staff_member_end_time(self.staff_member1, datetime.date(2023, 10, 9)) - self.assertEqual(end_time, self.wh.end_time) # finish_time should prevail - - def test_staff_member_buffer_time_with_working_hours_conflict(self): - """Test buffer time when it conflicts with WorkingHours.""" - self.staff_member1.appointment_buffer_time = 120 # Set a buffer time greater than WorkingHours start time - self.staff_member1.save() - buffer_time = get_staff_member_buffer_time(self.staff_member1, datetime.date(2023, 10, 9)) - self.assertEqual(buffer_time, 120) # Should still use staff member-specific setting even if it causes conflict - - -class TestDBHelpers(BaseTest): - def setUp(self): - super().setUp() - - def test_get_staff_member_by_user_id(self): - """Test retrieving a StaffMember object using a user id works as expected.""" - staff = get_staff_member_by_user_id(self.user1.id) - self.assertIsNotNone(staff) - self.assertEqual(staff, self.staff_member1) - - # Test for a non-existent user id - staff = get_staff_member_by_user_id(99999) - self.assertIsNone(staff) - - def test_get_staff_member_from_user_id_or_logged_in(self): - """Test retrieving a StaffMember object using a user id or the logged in user works as expected.""" - staff = get_staff_member_from_user_id_or_logged_in(self.user1) - self.assertIsNotNone(staff) - self.assertEqual(staff, self.staff_member1) - - staff = get_staff_member_from_user_id_or_logged_in(self.user1, self.user2.id) - self.assertIsNotNone(staff) - self.assertEqual(staff, self.staff_member2) - - # Test for a non-existent user id - staff = get_staff_member_from_user_id_or_logged_in(self.user1, 99999) - self.assertIsNone(staff) - - def test_get_user_model(self): - """Test retrieving the user model works as expected.""" - user_model = get_user_model() - user_model_in_settings = apps.get_model(settings.AUTH_USER_MODEL) - self.assertEqual(user_model, user_model_in_settings) - - def test_get_user_by_email(self): - """Retrieve a user by email.""" - user = get_user_by_email("tester1@gmail.com") - self.assertIsNotNone(user) - self.assertEqual(user, self.user1) - - # Test for a non-existent email - user = get_user_by_email("nonexistent@gmail.com") - self.assertIsNone(user) - - -class TestWorkingHoursFunctions(BaseTest): - def setUp(self): - super().setUp() - self.working_hours = WorkingHours.objects.create( - staff_member=self.staff_member1, - day_of_week=1, # Monday - start_time=datetime.time(9, 0), - end_time=datetime.time(17, 0) - ) - - def test_get_working_hours_by_id(self): - """Test retrieving a WorkingHours object by ID.""" - working_hours = get_working_hours_by_id(self.working_hours.id) - self.assertEqual(working_hours, self.working_hours) - - # Non-existent ID - working_hours = get_working_hours_by_id(99999) - self.assertIsNone(working_hours) - - def test_get_working_hours_for_staff_and_day(self): - """Test retrieving WorkingHours for a specific staff member and day.""" - # With set WorkingHours - result = get_working_hours_for_staff_and_day(self.staff_member1, 1) - self.assertEqual(result['start_time'], datetime.time(9, 0)) - self.assertEqual(result['end_time'], datetime.time(17, 0)) - - # Without set WorkingHours but with staff member's default times - self.staff_member1.lead_time = datetime.time(8, 0) - self.staff_member1.finish_time = datetime.time(18, 0) - self.staff_member1.save() - result = get_working_hours_for_staff_and_day(self.staff_member1, 2) # Tuesday - self.assertEqual(result['start_time'], datetime.time(8, 0)) - self.assertEqual(result['end_time'], datetime.time(18, 0)) - - def test_is_working_day(self): - """is_working_day() should return True if there are WorkingHours for the staff member and day, - False otherwise.""" - self.assertTrue(is_working_day(self.staff_member1, 1)) # Monday - self.assertFalse(is_working_day(self.staff_member1, 2)) # Tuesday - - def test_working_hours_exist(self): - """working_hours_exist() should return True if there are WorkingHours for the staff member and day,""" - self.assertTrue(working_hours_exist(1, self.staff_member1)) # Monday - self.assertFalse(working_hours_exist(2, self.staff_member1)) # Tuesday - - -@patch('appointment.utils.db_helpers.APPOINTMENT_LEAD_TIME', (7, 0)) -@patch('appointment.utils.db_helpers.APPOINTMENT_FINISH_TIME', (15, 0)) -@patch('appointment.utils.db_helpers.APPOINTMENT_SLOT_DURATION', 30) -@patch('appointment.utils.db_helpers.APPOINTMENT_BUFFER_TIME', 60) -class TestGetTimesFromConfig(TestCase): - def setUp(self): - self.sample_date = datetime.date(2023, 10, 9) - cache.clear() - - def test_times_from_config_object(self): - """Test retrieving times from a Config object.""" - # Create a Config object with custom values - Config.objects.create( - lead_time=datetime.time(9, 0), - finish_time=datetime.time(17, 0), - slot_duration=45, - appointment_buffer_time=90 - ) - - start_time, end_time, slot_duration, buff_time = get_times_from_config(self.sample_date) - - # Assert times from 'Config' object - self.assertEqual(start_time, datetime.datetime(2023, 10, 9, 9, 0)) - self.assertEqual(end_time, datetime.datetime(2023, 10, 9, 17, 0)) - self.assertEqual(slot_duration, datetime.timedelta(minutes=45)) - self.assertEqual(buff_time, datetime.timedelta(minutes=90)) - - def test_times_from_default_settings(self): - """Test retrieving times from default settings.""" - # Ensure no Config object exists - Config.objects.all().delete() - - start_time, end_time, slot_duration, buff_time = get_times_from_config(self.sample_date) - - # Assert times from default settings - self.assertEqual(start_time, datetime.datetime(2023, 10, 9, 7, 0)) - self.assertEqual(end_time, datetime.datetime(2023, 10, 9, 15, 0)) - self.assertEqual(slot_duration, datetime.timedelta(minutes=30)) - self.assertEqual(buff_time, datetime.timedelta(minutes=60)) - - -class CreateNewUserTest(TestCase): - def test_create_new_user_unique_username(self): - """Test creating a new user with a unique username.""" - client_data = {'name': 'John Doe', 'email': 'john.doe@example.com'} - user = create_new_user(client_data) - self.assertEqual(user.username, 'john.doe') - self.assertEqual(user.first_name, 'John') - self.assertEqual(user.email, 'john.doe@example.com') - - def test_create_new_user_duplicate_username(self): - """Test creating a new user with a duplicate username.""" - client_data1 = {'name': 'John Doe', 'email': 'john.doe@example.com'} - user1 = create_new_user(client_data1) - self.assertEqual(user1.username, 'john.doe') - - client_data2 = {'name': 'Jane Doe', 'email': 'john.doe@example.com'} - user2 = create_new_user(client_data2) - self.assertEqual(user2.username, 'john.doe01') # Suffix added - - client_data3 = {'name': 'James Doe', 'email': 'john.doe@example.com'} - user3 = create_new_user(client_data3) - self.assertEqual(user3.username, 'john.doe02') # Next suffix - - def test_generate_unique_username(self): - """Test if generate_unique_username_from_email function generates unique usernames.""" - email = 'john.doe@example.com' - username = generate_unique_username_from_email(email) - self.assertEqual(username, 'john.doe') - - # Assuming we have a user with the same username - CLIENT_MODEL = get_user_model() - CLIENT_MODEL.objects.create_user(username='john.doe', email=email) - new_username = generate_unique_username_from_email(email) - self.assertEqual(new_username, 'john.doe01') - - def test_parse_name(self): - """Test if parse_name function splits names correctly.""" - name = "John Doe" - first_name, last_name = parse_name(name) - self.assertEqual(first_name, 'John') - self.assertEqual(last_name, 'Doe') - - def test_create_new_user_check_password(self): - """Test creating a new user with a password.""" - client_data = {'name': 'John Doe', 'email': 'john.doe@example.com'} - user = create_new_user(client_data) - # Check that no password has been set - self.assertFalse(user.has_usable_password()) - - -class UsernameInUserModelTests(TestCase): - - @patch('django.contrib.auth.models.User._meta.get_field') - def test_username_field_exists(self, mock_get_field): - """ - Test that `username_in_user_model` returns True when the 'username' field exists. - """ - mock_get_field.return_value = True # Mocking as if 'username' field exists - self.assertTrue(username_in_user_model()) - - @patch('django.contrib.auth.models.User._meta.get_field') - def test_username_field_does_not_exist(self, mock_get_field): - """ - Test that `username_in_user_model` returns False when the 'username' field does not exist. - """ - mock_get_field.side_effect = FieldDoesNotExist # Simulating 'username' field does not exist - self.assertFalse(username_in_user_model()) - - -class ExcludePendingReschedulesTests(BaseTest): - - def setUp(self): - super().setUp() - self.date = timezone.now().date() + datetime.timedelta(minutes=5) - self.start_time = (timezone.now() - datetime.timedelta(minutes=4)).time() - self.end_time = (timezone.now() + datetime.timedelta(minutes=1)).time() - - self.slots = [ - datetime.datetime.combine(self.date, self.start_time), - datetime.datetime.combine(self.date, self.end_time) - ] - - def test_exclude_no_pending_reschedules(self): - """Slots should remain unchanged if there are no pending rescheduling.""" - filtered_slots = exclude_pending_reschedules(self.slots, self.staff_member1, self.date) - self.assertEqual(len(filtered_slots), len(self.slots)) - - def test_exclude_with_pending_reschedules_outside_last_5_minutes(self): - """Slots should remain unchanged if pending reschedules are outside the last 5 minutes.""" - appointment_request = self.create_appointment_request_(self.service1, self.staff_member1) - self.create_reschedule_history_( - appointment_request, - date_=self.date, - start_time=(timezone.now() - datetime.timedelta(minutes=10)).time(), - end_time=(timezone.now() - datetime.timedelta(minutes=5)).time(), - staff_member=self.staff_member1, - reason_for_rescheduling="Scheduling conflict" - ) - filtered_slots = exclude_pending_reschedules(self.slots, self.staff_member1, self.date) - self.assertEqual(len(filtered_slots), len(self.slots)) - - def test_exclude_with_pending_reschedules_within_last_5_minutes(self): - """Slots overlapping with pending rescheduling within the last 5 minutes should be excluded.""" - appointment_request = self.create_appointment_request_(self.service1, self.staff_member1) - reschedule_start_time = (timezone.now() - datetime.timedelta(minutes=4)).time() - reschedule_end_time = (timezone.now() + datetime.timedelta(minutes=1)).time() - self.create_reschedule_history_( - appointment_request, - date_=self.date, - start_time=reschedule_start_time, - end_time=reschedule_end_time, - staff_member=self.staff_member1, - reason_for_rescheduling="Client request" - ) - filtered_slots = exclude_pending_reschedules(self.slots, self.staff_member1, self.date) - self.assertEqual(len(filtered_slots), len(self.slots) - 1) # Assuming only one slot overlaps - - def test_exclude_with_non_pending_reschedules_within_last_5_minutes(self): - """Slots should remain unchanged if reschedules within the last 5 minutes are not pending.""" - appointment_request = self.create_appointment_request_(self.service1, self.staff_member1) - reschedule_start_time = (timezone.now() - datetime.timedelta(minutes=4)).time() - reschedule_end_time = (timezone.now() + datetime.timedelta(minutes=1)).time() - reschedule = self.create_reschedule_history_( - appointment_request, - date_=self.date, - start_time=reschedule_start_time, - end_time=reschedule_end_time, - staff_member=self.staff_member1, - reason_for_rescheduling="Urgent issue" - ) - reschedule.reschedule_status = 'confirmed' - reschedule.save() - filtered_slots = exclude_pending_reschedules(self.slots, self.staff_member1, self.date) - self.assertEqual(len(filtered_slots), len(self.slots)) - - -class GetAbsoluteUrlTests(TestCase): - - def setUp(self): - # Create a RequestFactory instance - self.factory = RequestFactory() - - def test_get_absolute_url_with_request(self): - # Create a request object using RequestFactory - request = self.factory.get('/some-path/') - relative_url = '/test-url/' - expected_url = request.build_absolute_uri(relative_url) - - # Call the function with the request object - result_url = get_absolute_url_(relative_url, request) - - # Assert the result matches the expected URL - self.assertEqual(result_url, expected_url) diff --git a/appointment/tests/utils/test_email_ops.py b/appointment/tests/utils/test_email_ops.py deleted file mode 100644 index e7bfcd0..0000000 --- a/appointment/tests/utils/test_email_ops.py +++ /dev/null @@ -1,170 +0,0 @@ -from unittest import mock -from unittest.mock import MagicMock, patch - -from django.test.client import RequestFactory -from django.utils import timezone -from django.utils.translation import gettext as _ - -from appointment.messages_ import thank_you_no_payment, thank_you_payment, thank_you_payment_plus_down -from appointment.models import AppointmentRescheduleHistory -from appointment.tests.base.base_test import BaseTest -from appointment.utils.email_ops import ( - get_thank_you_message, notify_admin_about_appointment, send_reschedule_confirmation_email, - send_reset_link_to_staff_member, send_thank_you_email, - send_verification_email -) - - -class SendResetLinkToStaffMemberTests(BaseTest): - - def setUp(self): - super().setUp() - self.user = self.user1 - self.user.is_staff = True - self.user.save() - self.factory = RequestFactory() - self.request = self.factory.get('/') - self.email = 'staff@example.com' - - @mock.patch('appointment.utils.email_ops.send_email') - @mock.patch('appointment.models.PasswordResetToken.create_token') - def test_send_reset_link(self, mock_create_token, mock_send_email): - # Setup the token - mock_token = mock.Mock() - mock_token.token = 'token123' - mock_create_token.return_value = mock_token - - # Assume get_absolute_url_ and get_website_name are utility functions you've defined somewhere - with mock.patch('appointment.utils.email_ops.get_absolute_url_') as mock_get_absolute_url: - with mock.patch('appointment.utils.email_ops.get_website_name') as mock_get_website_name: - mock_get_absolute_url.return_value = 'http://testserver/reset_password' - mock_get_website_name.return_value = 'TestCompany' - - send_reset_link_to_staff_member(self.user, self.request, self.email) - - # Check send_email was called with correct parameters - mock_send_email.assert_called_once() - args, kwargs = mock_send_email.call_args - self.assertEqual(kwargs['recipient_list'], [self.email]) - self.assertIn('TestCompany', kwargs['message']) - self.assertIn('http://testserver/reset_password', kwargs['message']) - - -class GetThankYouMessageTests(BaseTest): - def test_thank_you_no_payment(self): - with patch('appointment.utils.email_ops.APPOINTMENT_PAYMENT_URL', None): - ar = MagicMock() - message = get_thank_you_message(ar) - self.assertIn(thank_you_no_payment, message) - - def test_thank_you_payment_plus_down(self): - with patch('appointment.utils.email_ops.APPOINTMENT_PAYMENT_URL', "http://payment.url"): - ar = MagicMock() - ar.accepts_down_payment.return_value = True - message = get_thank_you_message(ar) - self.assertIn(thank_you_payment_plus_down, message) - - def test_thank_you_payment(self): - with patch('appointment.utils.email_ops.APPOINTMENT_PAYMENT_URL', "http://payment.url"): - ar = MagicMock() - ar.accepts_down_payment.return_value = False - message = get_thank_you_message(ar) - self.assertIn(thank_you_payment, message) - - -class SendThankYouEmailTests(BaseTest): - def setUp(self): - super().setUp() - self.factory = RequestFactory() - self.request = self.factory.get('/') - - @patch('appointment.utils.email_ops.send_email') - @patch('appointment.utils.email_ops.get_thank_you_message') - def test_send_thank_you_email(self, mock_get_thank_you_message, mock_send_email): - ar = self.create_appt_request_for_sm1() - email = "test@example.com" - appointment_details = "Details about the appointment" - account_details = "Details about the account" - - mock_get_thank_you_message.return_value = "Thank you message" - - send_thank_you_email(ar, self.user1, self.request, email, appointment_details, account_details) - - mock_send_email.assert_called_once() - args, kwargs = mock_send_email.call_args - self.assertIn(email, kwargs['recipient_list']) - self.assertIn("Thank you message", kwargs['context']['message_1']) - - -class NotifyAdminAboutAppointmentTests(BaseTest): - def setUp(self): - super().setUp() - self.appointment = self.create_appointment_for_user1() - - @patch('appointment.utils.email_ops.notify_admin') - @patch('appointment.utils.email_ops.send_email') - def test_notify_admin_about_appointment(self, mock_send_email, mock_notify_admin): - client_name = "John Doe" - - notify_admin_about_appointment(self.appointment, client_name) - - mock_notify_admin.assert_called_once() - mock_send_email.assert_called_once() - - -class SendVerificationEmailTests(BaseTest): - def setUp(self): - super().setUp() - self.appointment = self.create_appointment_for_user1() - - @patch('appointment.utils.email_ops.send_email') - @patch('appointment.models.EmailVerificationCode.generate_code', return_value="123456") - def test_send_verification_email(self, mock_generate_code, mock_send_email): - user = MagicMock() - email = "test@example.com" - - send_verification_email(user, email) - - mock_send_email.assert_called_once_with( - recipient_list=[email], - subject=_("Email Verification"), - message=mock.ANY - ) - self.assertIn("123456", mock_send_email.call_args[1]['message']) - - -class SendRescheduleConfirmationEmailTests(BaseTest): - def setUp(self): - # Setup test data - super().setUp() - self.user = self.user1 - self.appointment_request = self.create_appt_request_for_sm1() - self.reschedule_history = AppointmentRescheduleHistory.objects.create( - appointment_request=self.appointment_request, - date=self.appointment_request.date + timezone.timedelta(days=1), - start_time=self.appointment_request.start_time, - end_time=self.appointment_request.end_time, - staff_member=self.staff_member1, - reason_for_rescheduling="Test reason" - ) - self.first_name = "Test" - self.email = "test@example.com" - - @mock.patch('appointment.utils.email_ops.get_absolute_url_') - @mock.patch('appointment.utils.email_ops.send_email') - def test_send_reschedule_confirmation_email(self, mock_send_email, mock_get_absolute_url): - request = mock.MagicMock() - mock_get_absolute_url.return_value = "http://testserver/confirmation_link" - - send_reschedule_confirmation_email(request, self.reschedule_history, self.appointment_request, self.first_name, - self.email) - - # Check if `send_email` was called correctly - mock_send_email.assert_called_once() - call_args, call_kwargs = mock_send_email.call_args - - self.assertEqual(call_kwargs['recipient_list'], [self.email]) - self.assertEqual(call_kwargs['subject'], _("Confirm Your Appointment Rescheduling")) - self.assertIn('reschedule_date', call_kwargs['context']) - self.assertIn('confirmation_link', call_kwargs['context']) - self.assertEqual(call_kwargs['context']['confirmation_link'], "http://testserver/confirmation_link") diff --git a/appointment/tests/utils/test_json_context.py b/appointment/tests/utils/test_json_context.py deleted file mode 100644 index 2280a87..0000000 --- a/appointment/tests/utils/test_json_context.py +++ /dev/null @@ -1,68 +0,0 @@ -# test_json_context.py -# Path: appointment/tests/utils/test_json_context.py - -import json - -from django.test import RequestFactory - -from appointment.tests.base.base_test import BaseTest -from appointment.utils.json_context import (convert_appointment_to_json, get_generic_context, - get_generic_context_with_extra, handle_unauthorized_response, json_response) - - -class JsonContextTests(BaseTest): - - def setUp(self): - super().setUp() - self.factory = RequestFactory() - self.appointment = self.create_appointment_for_user1() - - def test_convert_appointment_to_json(self): - """Test if an appointment can be converted to JSON.""" - request = self.factory.get('/') - request.user = self.user1 - appointments = [self.appointment] - data = convert_appointment_to_json(request, appointments) - self.assertTrue(isinstance(data, list)) - self.assertEqual(len(data), 1) - self.assertTrue("id" in data[0]) - - def test_json_response(self): - """Test if a JSON response can be created.""" - message = "Test Message" - response = json_response(message=message) - self.assertEqual(response.status_code, 200) - content = json.loads(response.content.decode('utf-8')) - self.assertEqual(content['message'], message) - - def test_get_generic_context(self): - """Test if a generic context can be created.""" - request = self.factory.get('/') - request.user = self.user1 - context = get_generic_context(request) - self.assertEqual(context['user'], self.user1) - - def test_get_generic_context_with_extra(self): - """Test if a generic context with extra data can be created.""" - request = self.factory.get('/') - request.user = self.user1 - extra = {"key": "value"} - context = get_generic_context_with_extra(request, extra) - self.assertEqual(context['user'], self.user1) - self.assertEqual(context['key'], "value") - - def test_handle_unauthorized_response_json(self): - """Test if an unauthorized response can be created when the response type is JSON.""" - request = self.factory.get('/') - message = "Unauthorized" - response = handle_unauthorized_response(request=request, message=message, response_type='json') - self.assertEqual(response.status_code, 403) - content = json.loads(response.content.decode('utf-8')) - self.assertEqual(content['message'], message) - - def test_handle_unauthorized_response_html(self): - """Test if an unauthorized response can be created when the response type is HTML.""" - request = self.factory.get('/app-admin/user-events/') - message = "Unauthorized" - response = handle_unauthorized_response(request, message, 'html') - self.assertEqual(response.status_code, 403) diff --git a/appointment/tests/utils/test_permissions.py b/appointment/tests/utils/test_permissions.py deleted file mode 100644 index 7854891..0000000 --- a/appointment/tests/utils/test_permissions.py +++ /dev/null @@ -1,60 +0,0 @@ -# test_permissions.py -# Path: appointment/tests/utils/test_permissions.py - -import datetime - -from appointment.tests.base.base_test import BaseTest -from appointment.utils.db_helpers import WorkingHours -from appointment.utils.permissions import check_entity_ownership, check_extensive_permissions, check_permissions - - -class PermissionTests(BaseTest): - - def setUp(self): - super().setUp() - # Create users and entities for testing- - self.superuser = self.create_user_(username='superuser', email="superuser@gmail.com") - self.superuser.is_superuser = True - self.superuser.save() - self.entity_owned_by_user1 = WorkingHours.objects.create(staff_member=self.staff_member1, day_of_week=0, - start_time=datetime.time(8, 0), - end_time=datetime.time(12, 0)) - - def test_check_entity_ownership(self): - """Test if ownership of an entity can be checked.""" - # User is the owner - self.assertTrue(check_entity_ownership(self.user1, self.entity_owned_by_user1)) - - # Superuser but not owner - self.assertTrue(check_entity_ownership(self.superuser, self.entity_owned_by_user1)) - - # Neither owner nor superuser - self.assertFalse(check_entity_ownership(self.user2, self.entity_owned_by_user1)) - - def test_check_extensive_permissions(self): - """Test if extensive permissions can be checked.""" - # staff_user_id matches and user owns entity - self.assertTrue(check_extensive_permissions(self.user1.pk, self.user1, self.entity_owned_by_user1)) - - # staff_user_id matches but user doesn't own entity - self.assertFalse(check_extensive_permissions(self.user2.pk, self.user2, self.entity_owned_by_user1)) - - # staff_user_id doesn't match but user is superuser - self.assertTrue(check_extensive_permissions(None, self.superuser, self.entity_owned_by_user1)) - - # staff_user_id matches and no entity provided - self.assertTrue(check_extensive_permissions(self.user1.pk, self.user1, None)) - - # Neither staff_user_id matches nor superuser - self.assertFalse(check_extensive_permissions(None, self.user2, self.entity_owned_by_user1)) - - def test_check_permissions(self): - """Test if permissions can be checked.""" - # staff_user_id matches - self.assertTrue(check_permissions(self.user1.pk, self.user1)) - - # staff_user_id doesn't match but user is superuser - self.assertTrue(check_permissions(None, self.superuser)) - - # Neither staff_user_id matches nor superuser - self.assertFalse(check_permissions(None, self.user2)) diff --git a/appointment/tests/utils/test_session.py b/appointment/tests/utils/test_session.py deleted file mode 100644 index 01b5906..0000000 --- a/appointment/tests/utils/test_session.py +++ /dev/null @@ -1,94 +0,0 @@ -# test_session.py -# Path: appointment/tests/utils/test_session.py - -from django.contrib.messages.middleware import MessageMiddleware -from django.contrib.sessions.middleware import SessionMiddleware -from django.test import Client, override_settings -from django.test.client import RequestFactory - -from appointment.tests.base.base_test import BaseTest -from appointment.utils.session import get_appointment_data_from_session, handle_email_change, handle_existing_email - - -@override_settings(EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend') -class SessionTests(BaseTest): - - def setUp(self): - super().setUp() - self.ar = self.create_appt_request_for_sm1() - self.client = Client() - self.factory = RequestFactory() - - # Setup request object - self.request = self.factory.post('/') - self.request.user = self.user1 - - # Setup session for the request - middleware = SessionMiddleware(lambda req: None) - middleware.process_request(self.request) - self.request.session.save() - - # Setup messages for the request - middleware = MessageMiddleware(lambda req: None) - middleware.process_request(self.request) - self.request.session.save() - - def test_handle_existing_email(self): - """Test if an existing email can be handled.""" - client_data = { - 'email': self.client1.email, - 'name': 'John Doe' - } - appointment_data = { - 'phone': '+1234567890', - 'want_reminder': True, - 'address': '123 Main St, City, Country', - 'additional_info': 'Some additional info' - } - - response = handle_existing_email(self.request, client_data, appointment_data, self.ar.id, self.ar.id_request) - - # Assert session data - session = self.request.session - self.assertEqual(session['email'], client_data['email']) - self.assertEqual(session['phone'], appointment_data['phone']) - self.assertTrue(session['want_reminder']) - self.assertEqual(session['address'], appointment_data['address']) - self.assertEqual(session['additional_info'], appointment_data['additional_info']) - - # Assert redirect - self.assertEqual(response.status_code, 302) - - def test_handle_email_change(self): - """Test if an email change can be handled.""" - new_email = "new_email@example.com" - - response = handle_email_change(self.request, self.user1, new_email) - - # Assert session data - session = self.request.session - self.assertEqual(session['email'], new_email) - self.assertEqual(session['old_email'], self.user1.email) - - # Assert redirect - self.assertEqual(response.status_code, 302) - - def test_get_appointment_data_from_session(self): - """Test if appointment data can be retrieved from the session.""" - # Populate session with test data - session_data = { - 'phone': '+1234567890', - 'want_reminder': 'on', - 'address': '123 Main St, City, Country', - 'additional_info': 'Some additional info' - } - for key, value in session_data.items(): - self.request.session[key] = value - self.request.session.save() - - # Retrieve data using the function - appointment_data = get_appointment_data_from_session(self.request) - self.assertEqual(str(appointment_data['phone']), session_data['phone']) - self.assertTrue(appointment_data['want_reminder']) - self.assertEqual(appointment_data['address'], session_data['address']) - self.assertEqual(appointment_data['additional_info'], session_data['additional_info']) From 92e2ec0da02d0303f7a97c25aa783bd8d3736fcf Mon Sep 17 00:00:00 2001 From: Adams Pierre David <57180807+adamspd@users.noreply.github.com> Date: Sun, 21 Apr 2024 21:51:31 +0200 Subject: [PATCH 2/7] Rewrote the basetest class (stargate theme, I'm a great fan) --- appointment/tests/base/base_test.py | 99 ++++++++++++++++---------- appointment/tests/mixins/base_mixin.py | 18 ++--- 2 files changed, 71 insertions(+), 46 deletions(-) diff --git a/appointment/tests/base/base_test.py b/appointment/tests/base/base_test.py index ef36486..8c8e66c 100644 --- a/appointment/tests/base/base_test.py +++ b/appointment/tests/base/base_test.py @@ -1,29 +1,56 @@ from datetime import timedelta -from django.test import TestCase +from django.test import TransactionTestCase +from appointment.models import Appointment, AppointmentRequest, Service, StaffMember from appointment.tests.mixins.base_mixin import ( AppointmentMixin, AppointmentRequestMixin, AppointmentRescheduleHistoryMixin, ServiceMixin, StaffMemberMixin, UserMixin ) +from appointment.utils.db_helpers import get_user_model -class BaseTest(TestCase, UserMixin, StaffMemberMixin, ServiceMixin, AppointmentRequestMixin, AppointmentMixin, - AppointmentRescheduleHistoryMixin): - def setUp(self): - # Users - self.user1 = self.create_user_(email="tester1@gmail.com", username="tester1") - self.user2 = self.create_user_(first_name="Tester2", email="tester2@gmail.com", username="tester2") - self.client1 = self.create_user_(first_name="Client1", email="client1@gmail.com", username="client1") - self.client2 = self.create_user_(first_name="Client2", email="client2@gmail.com", username="client2") +class BaseTest(TransactionTestCase, UserMixin, StaffMemberMixin, ServiceMixin, AppointmentRequestMixin, + AppointmentMixin, AppointmentRescheduleHistoryMixin): + service1 = None + service2 = None + staff_member1 = None + staff_member2 = None + users = None - # Services - self.service1 = self.create_service_() - self.service2 = self.create_service_(name="Service 2") + USER_SPECS = { + 'staff1': {"first_name": "Daniel", "email": "daniel.jackson@django-appointment.com", + "username": "daniel.jackson"}, + 'staff2': {"first_name": "Samantha", "email": "samantha.carter@django-appointment.com", + "username": "samantha.carter"}, + 'client1': {"first_name": "Georges", "email": "georges.s.hammond@django-appointment.com", + "username": "georges.hammond"}, + 'client2': {"first_name": "Tealc", "email": "tealc.kree@django-appointment.com", "username": "tealc.kree"}, + 'superuser': {"first_name": "Jack", "email": "jack-oneill@django-appointment.com", "username": "jack.o.neill"}, + } - # Staff Members - self.staff_member1 = self.create_staff_member_(user=self.user1, service=self.service1) - self.staff_member2 = self.create_staff_member_(user=self.user2, service=self.service2) + @classmethod + def setUpTestData(cls): + cls.users = {key: cls.create_user_(**details) for key, details in cls.USER_SPECS.items()} + cls.service1 = cls.create_service_() + cls.service2 = cls.create_service_(name="Dial Home Device Repair", duration=timedelta(hours=2), price=200) + # Mapping services to staff members + cls.staff_member1 = cls.create_staff_member_(user=cls.users['staff1'], service=cls.service1) + cls.staff_member2 = cls.create_staff_member_(user=cls.users['staff2'], service=cls.service2) + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + # Clean up any class-level resources + cls.clean_all_data() + + @classmethod + def clean_all_data(cls): + Appointment.objects.all().delete() + AppointmentRequest.objects.all().delete() + StaffMember.objects.all().delete() + Service.objects.all().delete() + get_user_model().objects.all().delete() def create_appt_request_for_sm1(self, **kwargs): """Create an appointment request for staff_member1.""" @@ -33,20 +60,20 @@ def create_appt_request_for_sm2(self, **kwargs): """Create an appointment request for staff_member2.""" return self.create_appointment_request_(service=self.service2, staff_member=self.staff_member2, **kwargs) - def create_appointment_for_user1(self, appointment_request=None): + def create_appt_for_sm1(self, appointment_request=None): if not appointment_request: appointment_request = self.create_appt_request_for_sm1() - return self.create_appointment_(user=self.client1, appointment_request=appointment_request) + return self.create_appointment_(user=self.users['client1'], appointment_request=appointment_request) - def create_appointment_for_user2(self, appointment_request=None): + def create_appt_for_sm2(self, appointment_request=None): if not appointment_request: appointment_request = self.create_appt_request_for_sm2() - return self.create_appointment_(user=self.client2, appointment_request=appointment_request) + return self.create_appointment_(user=self.users['client2'], appointment_request=appointment_request) - def create_appointment_reschedule_for_user1(self, appointment_request=None, reason_for_rescheduling="Reason"): + def create_appt_reschedule_for_sm1(self, appointment_request=None, reason_for_rescheduling="Gate Malfunction"): if not appointment_request: appointment_request = self.create_appt_request_for_sm1() - date_ = appointment_request.date + timedelta(days=1) + date_ = appointment_request.date + timedelta(days=7) return self.create_reschedule_history_( appointment_request=appointment_request, date_=date_, @@ -59,23 +86,21 @@ def create_appointment_reschedule_for_user1(self, appointment_request=None, reas def need_normal_login(self): self.client.force_login(self.create_user_()) - def need_staff_login(self, user=None): - if user is not None: - user.is_staff = True - user.save() - self.client.force_login(user) - self.user1.is_staff = True - self.user1.save() - self.client.force_login(self.user1) + def need_staff_login(self): + self.staff = self.users['staff1'] + self.staff.is_staff = True + self.staff.save() + self.client.force_login(self.staff) def need_superuser_login(self): - self.user1.is_superuser = True - self.user1.save() - self.client.force_login(self.user1) + self.superuser = self.users['superuser'] + self.superuser.is_superuser = True + self.superuser.save() + self.client.force_login(self.superuser) - def clean_staff_member_objects(self, user=None): + def clean_staff_member_objects(self, staff=None): """Delete all AppointmentRequests and Appointments linked to the StaffMember instance of self.user1.""" - if user is None: - user = self.user1 - self.clean_appointment_for_user(user) - self.clean_appt_request_for_user(user) + if staff is None: + staff = self.users['staff1'] + self.clean_appointment_for_user_(staff) + self.clean_appt_request_for_user_(staff) diff --git a/appointment/tests/mixins/base_mixin.py b/appointment/tests/mixins/base_mixin.py index eb4926a..1650d81 100644 --- a/appointment/tests/mixins/base_mixin.py +++ b/appointment/tests/mixins/base_mixin.py @@ -11,10 +11,9 @@ def __init__(self): pass @classmethod - def create_user_(cls, first_name="Tester", email="testemail@gmail.com", username="test_user", - password="Kfdqi3!?n"): - user_model = get_user_model() - return user_model.objects.create_user( + def create_user_(cls, first_name="Janet", email="janet.fraiser@django-appointment.com", username="janet.fraiser", + password="G0a'uld$Emp1re"): + return get_user_model().objects.create_user( first_name=first_name, email=email, username=username, @@ -27,7 +26,7 @@ def __init__(self): pass @classmethod - def create_service_(cls, name="Test Service", duration=timedelta(hours=1), price=100): + def create_service_(cls, name="Quantum Mirror Assessment", duration=timedelta(hours=1), price=100): return Service.objects.create( name=name, duration=duration, @@ -62,7 +61,7 @@ def create_appointment_request_(cls, service, staff_member, date_=date.today(), ) @classmethod - def clean_appt_request_for_user(cls, user): + def clean_appt_request_for_user_(cls, user): AppointmentRequest.objects.filter(staff_member__user=user).delete() @@ -71,7 +70,8 @@ def __init__(self): pass @classmethod - def create_appointment_(cls, user, appointment_request, phone="1234567890", address="Some City, Some State"): + def create_appointment_(cls, user, appointment_request, phone="1234567890", + address="Stargate Command, Cheyenne Mountain Complex, Colorado Springs, CO"): return Appointment.objects.create( client=user, appointment_request=appointment_request, @@ -80,7 +80,7 @@ def create_appointment_(cls, user, appointment_request, phone="1234567890", ) @classmethod - def clean_appointment_for_user(cls, user): + def clean_appointment_for_user_(cls, user): Appointment.objects.filter(client=user).delete() @@ -90,7 +90,7 @@ def __init__(self): @classmethod def create_reschedule_history_(cls, appointment_request, date_, start_time, end_time, staff_member, - reason_for_rescheduling=""): + reason_for_rescheduling="Zat'nik'tel Discharge"): return AppointmentRescheduleHistory.objects.create( appointment_request=appointment_request, date=date_, From 38e3039c7893b7d687cb1a47875ea40741d5ff5b Mon Sep 17 00:00:00 2001 From: Adams Pierre David <57180807+adamspd@users.noreply.github.com> Date: Sun, 21 Apr 2024 23:21:12 +0200 Subject: [PATCH 3/7] Rewrote Service tests & added parallel command to GH action --- .github/workflows/tests.yml | 2 +- appointment/models.py | 2 +- appointment/tests/base/base_test.py | 10 +- appointment/tests/mixins/base_mixin.py | 6 +- appointment/tests/models/__init__.py | 0 appointment/tests/models/test_service.py | 215 +++++++++++++++++++++++ 6 files changed, 227 insertions(+), 8 deletions(-) create mode 100644 appointment/tests/models/__init__.py create mode 100644 appointment/tests/models/test_service.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e05a1b6..c89c1c3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -34,7 +34,7 @@ jobs: pip install coverage - name: Run Tests run: | - coverage run --source=appointment manage.py test appointment.tests --verbosity=1 + coverage run --source=appointment manage.py test appointment.tests --parallel=10 --shuffle --verbosity=1 coverage report coverage xml - name: Upload to Codecov diff --git a/appointment/models.py b/appointment/models.py index 161b33c..91d98c0 100644 --- a/appointment/models.py +++ b/appointment/models.py @@ -67,7 +67,7 @@ class Service(models.Model): name = models.CharField(max_length=100, blank=False) description = models.TextField(blank=True, null=True) duration = models.DurationField(validators=[MinValueValidator(datetime.timedelta(seconds=1))]) - price = models.DecimalField(max_digits=6, decimal_places=2, validators=[MinValueValidator(0)]) + price = models.DecimalField(max_digits=8, decimal_places=2, validators=[MinValueValidator(0)]) down_payment = models.DecimalField(max_digits=6, decimal_places=2, default=0, validators=[MinValueValidator(0)]) image = models.ImageField(upload_to='services/', blank=True, null=True) currency = models.CharField(max_length=3, default='USD', validators=[MaxLengthValidator(3), MinLengthValidator(3)]) diff --git a/appointment/tests/base/base_test.py b/appointment/tests/base/base_test.py index 8c8e66c..d146da5 100644 --- a/appointment/tests/base/base_test.py +++ b/appointment/tests/base/base_test.py @@ -1,6 +1,6 @@ from datetime import timedelta -from django.test import TransactionTestCase +from django.test import TestCase from appointment.models import Appointment, AppointmentRequest, Service, StaffMember from appointment.tests.mixins.base_mixin import ( @@ -10,7 +10,7 @@ from appointment.utils.db_helpers import get_user_model -class BaseTest(TransactionTestCase, UserMixin, StaffMemberMixin, ServiceMixin, AppointmentRequestMixin, +class BaseTest(TestCase, UserMixin, StaffMemberMixin, ServiceMixin, AppointmentRequestMixin, AppointmentMixin, AppointmentRescheduleHistoryMixin): service1 = None service2 = None @@ -32,8 +32,10 @@ class BaseTest(TransactionTestCase, UserMixin, StaffMemberMixin, ServiceMixin, A @classmethod def setUpTestData(cls): cls.users = {key: cls.create_user_(**details) for key, details in cls.USER_SPECS.items()} - cls.service1 = cls.create_service_() - cls.service2 = cls.create_service_(name="Dial Home Device Repair", duration=timedelta(hours=2), price=200) + cls.service1 = cls.create_service_( + name="Stargate Activation", duration=timedelta(hours=1), price=100000, description="Activate the Stargate") + cls.service2 = cls.create_service_( + name="Dial Home Device Repair", duration=timedelta(hours=2), price=200000, description="Repair the DHD") # Mapping services to staff members cls.staff_member1 = cls.create_staff_member_(user=cls.users['staff1'], service=cls.service1) cls.staff_member2 = cls.create_staff_member_(user=cls.users['staff2'], service=cls.service2) diff --git a/appointment/tests/mixins/base_mixin.py b/appointment/tests/mixins/base_mixin.py index 1650d81..9c9857f 100644 --- a/appointment/tests/mixins/base_mixin.py +++ b/appointment/tests/mixins/base_mixin.py @@ -26,11 +26,13 @@ def __init__(self): pass @classmethod - def create_service_(cls, name="Quantum Mirror Assessment", duration=timedelta(hours=1), price=100): + def create_service_(cls, name="Quantum Mirror Assessment", duration=timedelta(hours=1), price=50000, + description="Assess the Quantum Mirror"): return Service.objects.create( name=name, duration=duration, - price=price + price=price, + description=description ) diff --git a/appointment/tests/models/__init__.py b/appointment/tests/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/appointment/tests/models/test_service.py b/appointment/tests/models/test_service.py new file mode 100644 index 0000000..13b47f9 --- /dev/null +++ b/appointment/tests/models/test_service.py @@ -0,0 +1,215 @@ +from copy import deepcopy +from datetime import timedelta + +from django.core.exceptions import ValidationError +from django.core.files.uploadedfile import SimpleUploadedFile +from django.conf import settings + +from appointment.models import Service +from appointment.tests.base.base_test import BaseTest + + +class ServiceModelGettersTestCase(BaseTest): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + cls.service = cls.service1 + + def test_service_is_created(self): + self.assertIsNotNone(self.service) + + def test_is_a_paid_service(self): + self.assertTrue(self.service.is_a_paid_service()) + + def test_service_name(self): + self.assertEqual(self.service.name, "Stargate Activation") + + def test_service_description(self): + self.assertEqual(self.service.description, "Activate the Stargate") + + def test_service_duration(self): + """Test the duration of the service.""" + self.assertEqual(self.service.duration, self.service1.duration) + + def check_duration(self, duration, expected_string): + service = deepcopy(self.service) + service.duration = duration + self.assertEqual(service.get_duration(), expected_string) + + def test_various_durations_for_get_duration(self): + """Test that the get_duration method returns the correct string for a service with a duration of 30 seconds, + 30 minutes, etc.""" + test_cases = [ + (timedelta(seconds=30), '30 seconds'), + (timedelta(minutes=30), '30 minutes'), + (timedelta(hours=1), '1 hour'), + (timedelta(hours=2, minutes=30), '2 hours 30 minutes'), + (timedelta(days=1), '1 day'), + ] + for duration, expected in test_cases: + self.check_duration(duration, expected) + + def test_service_price(self): + self.assertEqual(self.service.price, 100000) + self.assertEqual(self.service.get_price(), 100000) + + def check_price(self, price, expected_string): + service = deepcopy(self.service) + service.price = price + self.assertEqual(service.get_price_text(), expected_string) + + def test_various_prices_for_get_price(self): + """Test that the get_price method returns the correct string for a service with a price of 100, 1000, etc.""" + test_cases = [ + (100, '100$'), + (100.50, '100.5$'), + (49.99, '49.99$'), + (0, 'Free') + ] + for price, expected in test_cases: + self.check_price(price, expected) + + def test_down_payment_value(self): + """By default, down payment value is 0""" + self.assertEqual(self.service.down_payment, 0) + self.assertEqual(self.service.get_down_payment(), 0) + + self.assertEqual(self.service.get_down_payment_text(), "Free") + + # Change the down payment value to 69.99 + s = deepcopy(self.service) + s.down_payment = 69.99 + self.assertEqual(s.get_down_payment(), 69.99) + + self.assertEqual(s.get_down_payment_text(), "69.99$") + + def test_price_and_down_payment_same(self): + """A service can be created with a price and down payment of the same value. + This is useful when the service requires full payment upfront. + """ + service = Service.objects.create( + name="Naquadah Generator Maintenance", duration=timedelta(hours=1), price=100, down_payment=100) + self.assertEqual(service.price, service.down_payment) + + def test_accepts_down_payment(self): + """By default, down payment is not accepted.""" + self.assertFalse(self.service.accepts_down_payment()) + + # Change the accepts_down_payment value to True + s = deepcopy(self.service) + s.down_payment = 69.99 + self.assertTrue(s.accepts_down_payment()) + + def test_default_currency(self): + """The Default currency is USD.""" + self.assertEqual(self.service.currency, 'USD') + + # Change the currency to EUR + s = deepcopy(self.service) + s.currency = 'EUR' + self.assertEqual(s.currency, 'EUR') + + def test_default_created_and_updated_at_not_none(self): + """Newly created services should have created_at and updated_at values.""" + self.assertIsNotNone(self.service.created_at) + self.assertIsNotNone(self.service.updated_at) + + def test_str_method(self): + """Test the string representation of the Service model.""" + service_name = "Test Service" + service = Service.objects.create(name=service_name, duration=timedelta(hours=1), price=100) + self.assertEqual(str(service), service_name) + + def test_get_service_image_url_with_image(self): + """Service should return the correct URL for the image if provided.""" + # Create an image and attach it to the service + image_path = settings.BASE_DIR / 'appointment/static/img/texture.webp' # Adjust the path as necessary + image = SimpleUploadedFile(name='test_image.png', content=open(image_path, 'rb').read(), + content_type='image/png') + service = Service.objects.create(name="Service with Image", duration=timedelta(hours=1), price=50, image=image) + + # Assuming you have MEDIA_URL set in your settings for development like '/media/' + expected_url = f"{settings.MEDIA_URL}{service.image}" + self.assertTrue(service.get_image_url().endswith(expected_url)) + + def test_get_service_image_url_no_image(self): + """Service should handle cases where no image is provided gracefully.""" + service = Service.objects.create(name="Gate Travel Coordination", duration=timedelta(hours=1), price=50) + self.assertEqual(service.get_image_url(), "") + + def test_service_auto_generate_background_color(self): + """Service should auto-generate a background color if none is provided.""" + service = Service.objects.create(name="Wormhole Stability Analysis", duration=timedelta(hours=1), price=50) + self.assertIsNotNone(service.background_color) + self.assertNotEqual(service.background_color, "") + + def test_reschedule_limit_and_allowance(self): + """Service should correctly handle reschedule limits and rescheduling allowance.""" + service = Service.objects.create(name="Goa'uld Artifact Decryption", duration=timedelta(hours=1), price=50, + reschedule_limit=3, allow_rescheduling=True) + self.assertEqual(service.reschedule_limit, 3) + self.assertTrue(service.allow_rescheduling) + + def test_to_dict_method(self): + """Test the to_dict method returns the correct dictionary representation of the Service instance.""" + service = Service.objects.create(name="Off-world Tactical Training", duration=timedelta(hours=1), price=150, + description="Train for off-world missions") + expected_dict = { + "id": service.id, + "name": "Off-world Tactical Training", + "description": "Train for off-world missions", + "price": "150" + } + self.assertEqual(service.to_dict(), expected_dict) + + +class ServiceModelNegativeTestCase(BaseTest): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + cls.service = cls.service1 + + def test_invalid_service_name(self): + """Test that the max_length of the name field is 100 characters.""" + s = deepcopy(self.service) + s.name = ("Gate Diagnostics and Calibration for Intergalactic Travel through the Quantum Bridge Device " + "Portal - Series SG-1") # Exceeding the max_length (112 characters) + with self.assertRaises(ValidationError): + s.full_clean() + + def test_invalid_price_and_down_payment(self): + """Test that the price and down_payment fields cannot be negative.""" + s = deepcopy(self.service) + s.price = -100 + with self.assertRaises(ValidationError): + s.full_clean() + + s = deepcopy(self.service) + s.down_payment = -100 + with self.assertRaises(ValidationError): + s.full_clean() + + def test_invalid_service_currency_length(self): + """A service cannot be created with a currency of less or more than three characters.""" + s = deepcopy(self.service) + s.currency = "US" + + with self.assertRaises(ValidationError): + s.full_clean() + + s.currency = "DOLLAR" + with self.assertRaises(ValidationError): + s.full_clean() + + def test_service_invalid_duration(self): + """A service cannot be created with a duration being zero or negative.""" + service = Service(name="Zat'nik'tel Tune-Up", duration=timedelta(0), price=100, description="Tune-up the Zat") + self.assertRaises(ValidationError, service.full_clean) + service = Service(name="Ancient's Archive Retrieval ", duration=timedelta(seconds=-1), price=50, + description="Retrieve the Ancient's Archive") + self.assertRaises(ValidationError, service.full_clean) + + def test_service_with_no_name(self): + """A service cannot be created with no name.""" + with self.assertRaises(ValidationError): + Service.objects.create(name="", duration=timedelta(hours=1), price=100).full_clean() From 68cf666c528b7b4bb8da558f7d026a5c45ce2e91 Mon Sep 17 00:00:00 2001 From: Adams Pierre David <57180807+adamspd@users.noreply.github.com> Date: Thu, 25 Apr 2024 01:29:17 +0200 Subject: [PATCH 4/7] Rewrote staff member tests and optimized service's tests --- appointment/tests/models/test_service.py | 177 +++++++----- appointment/tests/models/test_staff_member.py | 256 ++++++++++++++++++ 2 files changed, 370 insertions(+), 63 deletions(-) create mode 100644 appointment/tests/models/test_staff_member.py diff --git a/appointment/tests/models/test_service.py b/appointment/tests/models/test_service.py index 13b47f9..ac5a4b8 100644 --- a/appointment/tests/models/test_service.py +++ b/appointment/tests/models/test_service.py @@ -1,33 +1,83 @@ from copy import deepcopy from datetime import timedelta +from django.conf import settings from django.core.exceptions import ValidationError from django.core.files.uploadedfile import SimpleUploadedFile -from django.conf import settings from appointment.models import Service from appointment.tests.base.base_test import BaseTest -class ServiceModelGettersTestCase(BaseTest): +class ServiceCreationAndBasicAttributesTests(BaseTest): @classmethod def setUpTestData(cls): super().setUpTestData() cls.service = cls.service1 - def test_service_is_created(self): + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def test_service_creation(self): self.assertIsNotNone(self.service) - def test_is_a_paid_service(self): + def test_basic_attributes_verification(self): + self.assertEqual(self.service.name, "Stargate Activation") + self.assertEqual(self.service.description, "Activate the Stargate") + self.assertEqual(self.service.duration, self.service1.duration) + + def test_timestamps_on_creation(self): + """Newly created services should have created_at and updated_at values.""" + self.assertIsNotNone(self.service.created_at) + self.assertIsNotNone(self.service.updated_at) + + +class ServicePriceTests(BaseTest): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + cls.service = cls.service1 + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def test_service_price_verification(self): + self.assertEqual(self.service.price, 100000) + self.assertEqual(self.service.get_price(), 100000) + + def test_paid_service_verification(self): self.assertTrue(self.service.is_a_paid_service()) - def test_service_name(self): - self.assertEqual(self.service.name, "Stargate Activation") + def check_price(self, price, expected_string): + service = deepcopy(self.service) + service.price = price + self.assertEqual(service.get_price_text(), expected_string) - def test_service_description(self): - self.assertEqual(self.service.description, "Activate the Stargate") + def test_dynamic_price_representation(self): + """Test that the get_price method returns the correct string for a service with a price of 100, 1000, etc.""" + test_cases = [ + (100, '100$'), + (100.50, '100.5$'), + (49.99, '49.99$'), + (0, 'Free') + ] + for price, expected in test_cases: + self.check_price(price, expected) + + +class ServiceDurationTests(BaseTest): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + cls.service = cls.service1 - def test_service_duration(self): + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def test_service_duration_verification(self): """Test the duration of the service.""" self.assertEqual(self.service.duration, self.service1.duration) @@ -36,7 +86,7 @@ def check_duration(self, duration, expected_string): service.duration = duration self.assertEqual(service.get_duration(), expected_string) - def test_various_durations_for_get_duration(self): + def test_dynamic_duration_representation(self): """Test that the get_duration method returns the correct string for a service with a duration of 30 seconds, 30 minutes, etc.""" test_cases = [ @@ -49,25 +99,16 @@ def test_various_durations_for_get_duration(self): for duration, expected in test_cases: self.check_duration(duration, expected) - def test_service_price(self): - self.assertEqual(self.service.price, 100000) - self.assertEqual(self.service.get_price(), 100000) - def check_price(self, price, expected_string): - service = deepcopy(self.service) - service.price = price - self.assertEqual(service.get_price_text(), expected_string) +class ServiceDownPaymentTests(BaseTest): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + cls.service = cls.service1 - def test_various_prices_for_get_price(self): - """Test that the get_price method returns the correct string for a service with a price of 100, 1000, etc.""" - test_cases = [ - (100, '100$'), - (100.50, '100.5$'), - (49.99, '49.99$'), - (0, 'Free') - ] - for price, expected in test_cases: - self.check_price(price, expected) + @classmethod + def tearDownClass(cls): + super().tearDownClass() def test_down_payment_value(self): """By default, down payment value is 0""" @@ -83,14 +124,6 @@ def test_down_payment_value(self): self.assertEqual(s.get_down_payment_text(), "69.99$") - def test_price_and_down_payment_same(self): - """A service can be created with a price and down payment of the same value. - This is useful when the service requires full payment upfront. - """ - service = Service.objects.create( - name="Naquadah Generator Maintenance", duration=timedelta(hours=1), price=100, down_payment=100) - self.assertEqual(service.price, service.down_payment) - def test_accepts_down_payment(self): """By default, down payment is not accepted.""" self.assertFalse(self.service.accepts_down_payment()) @@ -100,27 +133,32 @@ def test_accepts_down_payment(self): s.down_payment = 69.99 self.assertTrue(s.accepts_down_payment()) - def test_default_currency(self): - """The Default currency is USD.""" - self.assertEqual(self.service.currency, 'USD') + def test_equal_price_and_down_payment_scenario(self): + """A service can be created with a price and down payment of the same value. + This is useful when the service requires full payment upfront. + """ + service = Service.objects.create( + name="Naquadah Generator Maintenance", duration=timedelta(hours=1), price=100, down_payment=100) + self.assertEqual(service.price, service.down_payment) - # Change the currency to EUR - s = deepcopy(self.service) - s.currency = 'EUR' - self.assertEqual(s.currency, 'EUR') - def test_default_created_and_updated_at_not_none(self): - """Newly created services should have created_at and updated_at values.""" - self.assertIsNotNone(self.service.created_at) - self.assertIsNotNone(self.service.updated_at) +class ServiceRepresentationAndMiscTests(BaseTest): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + cls.service = cls.service1 + + @classmethod + def tearDownClass(cls): + super().tearDownClass() - def test_str_method(self): + def test_string_representation_of_service(self): """Test the string representation of the Service model.""" service_name = "Test Service" service = Service.objects.create(name=service_name, duration=timedelta(hours=1), price=100) self.assertEqual(str(service), service_name) - def test_get_service_image_url_with_image(self): + def test_image_url_with_attached_image(self): """Service should return the correct URL for the image if provided.""" # Create an image and attach it to the service image_path = settings.BASE_DIR / 'appointment/static/img/texture.webp' # Adjust the path as necessary @@ -132,25 +170,18 @@ def test_get_service_image_url_with_image(self): expected_url = f"{settings.MEDIA_URL}{service.image}" self.assertTrue(service.get_image_url().endswith(expected_url)) - def test_get_service_image_url_no_image(self): + def test_image_url_without_attached_image(self): """Service should handle cases where no image is provided gracefully.""" service = Service.objects.create(name="Gate Travel Coordination", duration=timedelta(hours=1), price=50) self.assertEqual(service.get_image_url(), "") - def test_service_auto_generate_background_color(self): + def test_auto_generation_of_background_color(self): """Service should auto-generate a background color if none is provided.""" service = Service.objects.create(name="Wormhole Stability Analysis", duration=timedelta(hours=1), price=50) self.assertIsNotNone(service.background_color) self.assertNotEqual(service.background_color, "") - def test_reschedule_limit_and_allowance(self): - """Service should correctly handle reschedule limits and rescheduling allowance.""" - service = Service.objects.create(name="Goa'uld Artifact Decryption", duration=timedelta(hours=1), price=50, - reschedule_limit=3, allow_rescheduling=True) - self.assertEqual(service.reschedule_limit, 3) - self.assertTrue(service.allow_rescheduling) - - def test_to_dict_method(self): + def test_service_to_dict_representation(self): """Test the to_dict method returns the correct dictionary representation of the Service instance.""" service = Service.objects.create(name="Off-world Tactical Training", duration=timedelta(hours=1), price=150, description="Train for off-world missions") @@ -162,6 +193,22 @@ def test_to_dict_method(self): } self.assertEqual(service.to_dict(), expected_dict) + def test_reschedule_features(self): + """Service should correctly handle reschedule limits and rescheduling allowance.""" + service = Service.objects.create(name="Goa'uld Artifact Decryption", duration=timedelta(hours=1), price=50, + reschedule_limit=3, allow_rescheduling=True) + self.assertEqual(service.reschedule_limit, 3) + self.assertTrue(service.allow_rescheduling) + + def test_default_currency_setting(self): + """The Default currency is USD.""" + self.assertEqual(self.service.currency, 'USD') + + # Change the currency to EUR + s = deepcopy(self.service) + s.currency = 'EUR' + self.assertEqual(s.currency, 'EUR') + class ServiceModelNegativeTestCase(BaseTest): @classmethod @@ -169,7 +216,11 @@ def setUpTestData(cls): super().setUpTestData() cls.service = cls.service1 - def test_invalid_service_name(self): + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def test_exceeding_service_name_length(self): """Test that the max_length of the name field is 100 characters.""" s = deepcopy(self.service) s.name = ("Gate Diagnostics and Calibration for Intergalactic Travel through the Quantum Bridge Device " @@ -177,7 +228,7 @@ def test_invalid_service_name(self): with self.assertRaises(ValidationError): s.full_clean() - def test_invalid_price_and_down_payment(self): + def test_negative_price_and_down_payment_values(self): """Test that the price and down_payment fields cannot be negative.""" s = deepcopy(self.service) s.price = -100 @@ -189,7 +240,7 @@ def test_invalid_price_and_down_payment(self): with self.assertRaises(ValidationError): s.full_clean() - def test_invalid_service_currency_length(self): + def test_invalid_currency_code_length(self): """A service cannot be created with a currency of less or more than three characters.""" s = deepcopy(self.service) s.currency = "US" @@ -201,7 +252,7 @@ def test_invalid_service_currency_length(self): with self.assertRaises(ValidationError): s.full_clean() - def test_service_invalid_duration(self): + def test_zero_or_negative_duration_handling(self): """A service cannot be created with a duration being zero or negative.""" service = Service(name="Zat'nik'tel Tune-Up", duration=timedelta(0), price=100, description="Tune-up the Zat") self.assertRaises(ValidationError, service.full_clean) @@ -209,7 +260,7 @@ def test_service_invalid_duration(self): description="Retrieve the Ancient's Archive") self.assertRaises(ValidationError, service.full_clean) - def test_service_with_no_name(self): + def test_creation_without_service_name(self): """A service cannot be created with no name.""" with self.assertRaises(ValidationError): Service.objects.create(name="", duration=timedelta(hours=1), price=100).full_clean() diff --git a/appointment/tests/models/test_staff_member.py b/appointment/tests/models/test_staff_member.py new file mode 100644 index 0000000..e3e4d76 --- /dev/null +++ b/appointment/tests/models/test_staff_member.py @@ -0,0 +1,256 @@ +import datetime +from copy import deepcopy +from datetime import timedelta + +from django.db import IntegrityError +from django.utils.translation import gettext as _ + +from appointment.models import Config, DayOff, Service, StaffMember, WorkingHours +from appointment.tests.base.base_test import BaseTest + + +class StaffMemberCreationTests(BaseTest): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + cls.staff_member = cls.staff_member1 + cls.staff = cls.users['staff1'] + cls.service = cls.service1 + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def test_default_attributes_on_creation(self): + self.assertIsNotNone(self.staff_member) + self.assertEqual(self.staff_member.user, self.staff) + self.assertEqual(list(self.staff_member.get_services_offered()), [self.service]) + self.assertIsNone(self.staff_member.lead_time) + self.assertIsNone(self.staff_member.finish_time) + self.assertIsNone(self.staff_member.slot_duration) + self.assertIsNone(self.staff_member.appointment_buffer_time) + + def test_creation_without_service(self): + """A staff member can be created without a service.""" + new_staff = self.create_user_( + first_name="Jonas", email="jonas.quinn@django-appointment.com", username="jonas.quinn") + new_staff_member = StaffMember.objects.create(user=new_staff) + self.assertIsNotNone(new_staff_member) + self.assertEqual(new_staff_member.services_offered.count(), 0) + + def test_creation_fails_without_user(self): + """A staff member cannot be created without a user.""" + with self.assertRaises(IntegrityError): + StaffMember.objects.create() + + +class StaffMemberServiceTests(BaseTest): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + cls.staff_member = cls.staff_member1 + cls.staff = cls.users['staff1'] + cls.service = cls.service1 + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def get_fresh_staff_member(self): + return deepcopy(self.staff_member) + + def test_association_with_multiple_services(self): + """A staff member can offer multiple services.""" + ori = self.create_service_(name="Ori Shield Configuration", duration=timedelta(hours=1), price=100000, + description="Configure the Ori shield") + symbiote = self.create_service_(name="Symbiote Extraction", duration=timedelta(hours=1), price=100000, + description="Extract a symbiote") + sm = self.get_fresh_staff_member() + + sm.services_offered.add(ori) + sm.services_offered.add(symbiote) + + self.assertIn(ori, sm.services_offered.all()) + self.assertIn(symbiote, sm.services_offered.all()) + + def test_services_offered(self): + """Test get_services_offered & get_service_offered_text function.""" + self.assertIn(self.service, self.staff_member.get_services_offered()) + self.assertEqual(self.staff_member.get_service_offered_text(), self.service.name) + + def test_staff_member_with_non_existent_service(self): + """A staff member cannot offer a non-existent service.""" + new_staff = self.create_user_( + first_name="Vala", email="vala.mal-doran@django-appointment.com", username="vala.mal-doran") + new_staff_member = StaffMember.objects.create(user=new_staff) + + # Trying to add a non-existent service to the staff member's services_offered + with self.assertRaises(ValueError): + new_staff_member.services_offered.add( + Service(id=9999, name="Prometheus Acquisition", duration=timedelta(hours=2), price=200)) + + +class StaffMemberWorkingTimeTests(BaseTest): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + cls.staff_member = cls.staff_member1 + cls.staff = cls.users['staff1'] + cls.service = cls.service1 + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def get_fresh_staff_member(self): + return deepcopy(self.staff_member) + + def test_working_both_weekend_days(self): + """Test works_on_both_weekends_day method.""" + sm = self.get_fresh_staff_member() + sm.work_on_saturday = True + sm.work_on_sunday = True + self.assertTrue(sm.works_on_both_weekends_day()) + + def test_get_non_working_days(self): + """Test get_non_working_days method.""" + sm = self.get_fresh_staff_member() + sm.work_on_saturday = False + sm.work_on_sunday = False + self.assertEqual(sm.get_non_working_days(), [6, 0]) # [6, 0] represents Saturday and Sunday respectively + + def test_identification_of_all_non_working_days(self): + """Test various combinations of weekend work using a deepcopy of staff_member.""" + # Test with work on Saturday only + sm = self.get_fresh_staff_member() + + sm.work_on_saturday = True + sm.work_on_sunday = False + sm.save() + self.assertEqual(sm.get_weekend_days_worked_text(), _("Saturday")) + + # Test with work on both Saturday and Sunday + sm.work_on_saturday = True + sm.work_on_sunday = True + sm.save() + self.assertEqual(sm.get_weekend_days_worked_text(), _("Saturday and Sunday")) + + # Test with work on Sunday only + sm.work_on_saturday = False + sm.work_on_sunday = True + sm.save() + self.assertEqual(sm.get_weekend_days_worked_text(), _("Sunday")) + + # Test with work on neither Saturday nor Sunday + sm.work_on_saturday = False + sm.work_on_sunday = False + sm.save() + self.assertEqual(sm.get_weekend_days_worked_text(), _("None")) + + def test_get_days_off(self): + """Test retrieval of days off.""" + sm = self.get_fresh_staff_member() + DayOff.objects.create(staff_member=sm, start_date="2023-01-01", end_date="2023-01-02") + self.assertEqual(len(sm.get_days_off()), 1) + + def test_get_working_hours(self): + """Test retrieval of working hours.""" + sm = self.get_fresh_staff_member() + WorkingHours.objects.create(staff_member=sm, day_of_week=1, start_time=datetime.time(9, 0), + end_time=datetime.time(17, 0)) + self.assertEqual(len(sm.get_working_hours()), 1) + + # Precautionary cleanup (FIRST principle) + WorkingHours.objects.all().delete() + + def test_update_upon_working_hours_deletion(self): + """Test the update of work_on_saturday and work_on_sunday upon working-hours deletion.""" + sm = self.get_fresh_staff_member() + sm.work_on_saturday = True + sm.work_on_sunday = True + sm.save() + + sm.update_upon_working_hours_deletion(6) + self.assertFalse(sm.work_on_saturday) + sm.update_upon_working_hours_deletion(0) + self.assertFalse(sm.work_on_sunday) + + def test_is_working_day(self): + """Test whether a day is considered a working day.""" + sm = self.get_fresh_staff_member() + sm.work_on_saturday = False + sm.work_on_sunday = False + sm.save() + + self.assertFalse(self.staff_member.is_working_day(6)) # Saturday + self.assertTrue(self.staff_member.is_working_day(1)) # Monday + + +class StaffMemberGetterTestCase(BaseTest): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + cls.staff_member = cls.staff_member1 + cls.staff = cls.users['staff1'] + cls.service = cls.service1 + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def get_fresh_staff_member(self): + return deepcopy(self.staff_member) + + def test_staff_member_string_output(self): + """Test the string representation of a StaffMember.""" + expected_str = self.staff_member.get_staff_member_name() + self.assertEqual(str(self.staff_member), expected_str) + + def test_get_staff_member_first_name(self): + """Test that the staff member's first name is returned.""" + self.assertEqual(self.staff_member.get_staff_member_first_name(), self.staff.first_name) + + def test_date_joined_auto_creation(self): + """Test if the date_joined field is automatically set upon creation.""" + self.assertIsNotNone(self.staff_member.created_at) + + def test_config_values_takes_over_when_sm_values_null(self): + """When some values are null in the StaffMember, the Config values should be used.""" + config = Config.objects.create( + lead_time=datetime.time(9, 34), + finish_time=datetime.time(17, 11), + slot_duration=37, + appointment_buffer_time=16 + ) + # Checking that the StaffMember's values are None + self.assertIsNone(self.staff_member.slot_duration) + self.assertIsNone(self.staff_member.lead_time) + self.assertIsNone(self.staff_member.finish_time) + self.assertIsNone(self.staff_member.appointment_buffer_time) + + # Checking that the Config values are used + self.assertEqual(self.staff_member.get_slot_duration(), config.slot_duration) + self.assertEqual(self.staff_member.get_lead_time(), config.lead_time) + self.assertEqual(self.staff_member.get_finish_time(), config.finish_time) + self.assertEqual(self.staff_member.get_appointment_buffer_time(), config.appointment_buffer_time) + + # Setting the StaffMember values + sm = self.get_fresh_staff_member() + sm.slot_duration = 45 + sm.lead_time = datetime.time(9, 0) + sm.finish_time = datetime.time(17, 0) + sm.appointment_buffer_time = 15 + + # Checking that the StaffMember values are used and not the Config values + self.assertEqual(sm.get_slot_duration(), 45) + self.assertEqual(sm.get_lead_time(), datetime.time(9, 0)) + self.assertEqual(sm.get_finish_time(), datetime.time(17, 0)) + self.assertEqual(sm.get_appointment_buffer_time(), 15) + + def test_get_slot_duration_and_appt_buffer_time_text(self): + """Test get_slot_duration_text & get_appointment_buffer_time_text function.""" + sm = self.get_fresh_staff_member() + sm.slot_duration = 33 + sm.appointment_buffer_time = 24 + self.assertEqual(sm.get_slot_duration_text(), "33 minutes") + self.assertEqual(sm.get_appointment_buffer_time_text(), "24 minutes") From 0cf597cb3aa66bbd01ca84cd72954201aada37e4 Mon Sep 17 00:00:00 2001 From: Adams Pierre David <57180807+adamspd@users.noreply.github.com> Date: Tue, 14 May 2024 01:06:19 +0200 Subject: [PATCH 5/7] Reformat more tests in models --- appointment/models.py | 37 +++- appointment/tests/base/base_test.py | 12 +- appointment/tests/mixins/base_mixin.py | 2 +- .../tests/models/test_appointment_request.py | 182 ++++++++++++++++++ .../test_appointment_reschedule_history.py | 112 +++++++++++ appointment/tests/models/test_config.py | 128 ++++++++++++ appointment/tests/models/test_day_off.py | 69 +++++++ .../tests/models/test_email_verification.py | 51 +++++ appointment/tests/models/test_payment_info.py | 74 +++++++ appointment/tests/models/test_service.py | 2 +- appointment/tests/models/test_staff_member.py | 13 +- 11 files changed, 658 insertions(+), 24 deletions(-) create mode 100644 appointment/tests/models/test_appointment_request.py create mode 100644 appointment/tests/models/test_appointment_reschedule_history.py create mode 100644 appointment/tests/models/test_config.py create mode 100644 appointment/tests/models/test_day_off.py create mode 100644 appointment/tests/models/test_email_verification.py create mode 100644 appointment/tests/models/test_payment_info.py diff --git a/appointment/models.py b/appointment/models.py index 91d98c0..55e800e 100644 --- a/appointment/models.py +++ b/appointment/models.py @@ -10,6 +10,7 @@ import random import string import uuid +from decimal import Decimal, InvalidOperation from babel.numbers import get_currency_symbol from django.conf import settings @@ -435,7 +436,7 @@ class Appointment(models.Model): want_reminder = models.BooleanField(default=False) additional_info = models.TextField(blank=True, null=True) paid = models.BooleanField(default=False) - amount_to_pay = models.DecimalField(max_digits=6, decimal_places=2, blank=True, null=True) + amount_to_pay = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True) id_request = models.CharField(max_length=100, blank=True, null=True) # meta datas @@ -453,16 +454,30 @@ def save(self, *args, **kwargs): if self.id_request is None: self.id_request = f"{get_timestamp()}{self.appointment_request.id}{generate_random_id()}" - if self.amount_to_pay is None or self.amount_to_pay == 0: - payment_type = self.appointment_request.payment_type - if payment_type == 'full': - self.amount_to_pay = self.appointment_request.get_service_price() - elif payment_type == 'down': - self.amount_to_pay = self.appointment_request.get_service_down_payment() - else: - self.amount_to_pay = 0 + + try: + # Ensure `amount_to_pay` is a Decimal and handle both int and float inputs + if self.amount_to_pay is None: + self.amount_to_pay = self._calculate_amount_to_pay() + + self.amount_to_pay = self._to_decimal(self.amount_to_pay) + except InvalidOperation: + raise ValidationError("Invalid amount format for payment.") + return super().save(*args, **kwargs) + def _calculate_amount_to_pay(self): + payment_type = self.appointment_request.payment_type + if payment_type == 'full': + return self.appointment_request.get_service_price() + elif payment_type == 'down': + return self.appointment_request.get_service_down_payment() + else: + return Decimal('0.00') + + def _to_decimal(self, value): + return Decimal(f"{value}").quantize(Decimal('0.01')) + def get_client_name(self): if hasattr(self.client, 'get_full_name') and callable(getattr(self.client, 'get_full_name')): name = self.client.get_full_name() @@ -664,6 +679,10 @@ def clean(self): if self.lead_time is not None and self.finish_time is not None: if self.lead_time >= self.finish_time: raise ValidationError(_("Lead time must be before finish time")) + if self.appointment_buffer_time is not None and self.appointment_buffer_time < 0: + raise ValidationError(_("Appointment buffer time cannot be negative")) + if self.slot_duration is not None and self.slot_duration <= 0: + raise ValidationError(_("Slot duration must be greater than 0")) def save(self, *args, **kwargs): self.clean() diff --git a/appointment/tests/base/base_test.py b/appointment/tests/base/base_test.py index d146da5..5b9af17 100644 --- a/appointment/tests/base/base_test.py +++ b/appointment/tests/base/base_test.py @@ -54,13 +54,17 @@ def clean_all_data(cls): Service.objects.all().delete() get_user_model().objects.all().delete() - def create_appt_request_for_sm1(self, **kwargs): + def create_appt_request_for_sm1(self, service=None, staff_member=None, **kwargs): """Create an appointment request for staff_member1.""" - return self.create_appointment_request_(service=self.service1, staff_member=self.staff_member1, **kwargs) + service = service or self.service1 + staff_member = staff_member or self.staff_member1 + return self.create_appointment_request_(service=service, staff_member=staff_member, **kwargs) - def create_appt_request_for_sm2(self, **kwargs): + def create_appt_request_for_sm2(self, service=None, staff_member=None, **kwargs): """Create an appointment request for staff_member2.""" - return self.create_appointment_request_(service=self.service2, staff_member=self.staff_member2, **kwargs) + service = service or self.service2 + staff_member = staff_member or self.staff_member2 + return self.create_appointment_request_(service=service, staff_member=staff_member, **kwargs) def create_appt_for_sm1(self, appointment_request=None): if not appointment_request: diff --git a/appointment/tests/mixins/base_mixin.py b/appointment/tests/mixins/base_mixin.py index 9c9857f..11788ed 100644 --- a/appointment/tests/mixins/base_mixin.py +++ b/appointment/tests/mixins/base_mixin.py @@ -72,7 +72,7 @@ def __init__(self): pass @classmethod - def create_appointment_(cls, user, appointment_request, phone="1234567890", + def create_appointment_(cls, user, appointment_request, phone="+12392340543", address="Stargate Command, Cheyenne Mountain Complex, Colorado Springs, CO"): return Appointment.objects.create( client=user, diff --git a/appointment/tests/models/test_appointment_request.py b/appointment/tests/models/test_appointment_request.py new file mode 100644 index 0000000..6d1b5d7 --- /dev/null +++ b/appointment/tests/models/test_appointment_request.py @@ -0,0 +1,182 @@ +from copy import deepcopy +from datetime import date, datetime, time, timedelta + +from django.core.exceptions import ValidationError +from django.utils import timezone + +from appointment.tests.base.base_test import BaseTest + + +class AppointmentRequestCreationAndBasicAttributesTests(BaseTest): + @classmethod + def setUpTestData(cls): + return super().setUpTestData() + + @classmethod + def tearDownClass(cls): + return super().tearDownClass() + + def setUp(self) -> None: + self.ar = self.create_appt_request_for_sm1() + return super().setUp() + + def tearDown(self): + self.ar.delete() + super().tearDown() + + def test_appointment_request_is_properly_created(self): + self.assertIsNotNone(self.ar) + self.assertEqual(self.ar.service, self.service1) + self.assertEqual(self.ar.staff_member, self.staff_member1) + self.assertEqual(self.ar.start_time, time(9, 0)) + self.assertEqual(self.ar.end_time, time(10, 0)) + self.assertIsNotNone(self.ar.get_id_request()) + self.assertEqual(self.ar.date, timezone.now().date()) + self.assertTrue(isinstance(self.ar.get_id_request(), str)) + self.assertIsNotNone(self.ar.created_at) + self.assertIsNotNone(self.ar.updated_at) + + def test_appointment_request_initial_state(self): + """Check the initial state of "reschedule attempts" and string representation.""" + self.assertEqual(self.ar.reschedule_attempts, 0) + expected_representation = f"{self.ar.date} - {self.ar.start_time} to {self.ar.end_time} - {self.ar.service.name}" + self.assertEqual(str(self.ar), expected_representation) + + +class AppointmentRequestServiceAttributesTests(BaseTest): + @classmethod + def setUpTestData(cls): + return super().setUpTestData() + + @classmethod + def tearDownClass(cls): + return super().tearDownClass() + + def setUp(self) -> None: + self.ar = self.create_appt_request_for_sm1() + return super().setUp() + + def tearDown(self): + self.ar.delete() + super().tearDown() + + def test_service_related_attributes_are_correct(self): + """Validate attributes related to the service within an appointment request.""" + self.assertEqual(self.ar.get_service_name(), self.service1.name) + self.assertEqual(self.ar.get_service_price(), self.service1.get_price()) + self.assertEqual(self.ar.get_service_down_payment(), self.service1.get_down_payment()) + self.assertEqual(self.ar.get_service_image(), self.service1.image) + self.assertEqual(self.ar.get_service_image_url(), self.service1.get_image_url()) + self.assertEqual(self.ar.get_service_description(), self.service1.description) + self.assertTrue(self.ar.is_a_paid_service()) + self.assertEqual(self.ar.payment_type, 'full') + self.assertFalse(self.ar.accepts_down_payment()) + + +class AppointmentRequestAttributeValidation(BaseTest): + @classmethod + def setUpTestData(cls): + return super().setUpTestData() + + @classmethod + def tearDownClass(cls): + return super().tearDownClass() + + def setUp(self) -> None: + self.ar = self.create_appt_request_for_sm1() + return super().setUp() + + def tearDown(self): + self.ar.delete() + super().tearDown() + + def test_appointment_request_time_validations(self): + """Ensure start and end times are validated correctly.""" + ar = deepcopy(self.ar) + + # End time before start time + ar.start_time = time(11, 0) + ar.end_time = time(9, 0) + with self.assertRaises(ValidationError): + ar.full_clean() + + # End time equal to start time + ar.end_time = time(11, 0) + with self.assertRaises(ValidationError): + ar.full_clean() + + with self.assertRaises(ValidationError, msg="Start time and end time cannot be the same"): + self.create_appointment_request_( + self.service1, self.staff_member1, date_=date.today(), start_time=time(10, 0), end_time=time(10, 0) + ) + + def test_appointment_request_date_validations(self): + """Validate that appointment requests cannot be in the past or have invalid durations.""" + ar = deepcopy(self.ar) + + past_date = date.today() - timedelta(days=30) + ar.date = past_date + with self.assertRaises(ValidationError): + ar.full_clean() + + with self.assertRaises(ValidationError, msg="Date cannot be in the past"): + self.create_appointment_request_(self.service1, self.staff_member1, date_=past_date) + + with self.assertRaises(ValidationError, msg="The date is not valid"): + date_ = datetime.strptime("31-03-2021", "%d-%m-%Y").date() + self.create_appointment_request_( + self.service1, self.staff_member1, date_=date_) + + def test_appointment_duration_exceeds_service_time(self): + """Test that an appointment cannot be created with a duration greater than the service duration.""" + long_duration = timedelta(hours=3) + service = self.create_service_(name="Asgard Technology Retrofit", duration=long_duration) + service.duration = long_duration + service.save() + + # Create an appointment request with a 4-hour duration and the 3-hour service (should not work) + with self.assertRaises(ValidationError): + self.create_appointment_request_(service, self.staff_member1, start_time=time(9, 0), + end_time=time(13, 0)) + + def test_invalid_payment_type_raises_error(self): + """Payment type must be either 'full' or 'down'""" + ar = deepcopy(self.ar) + ar.payment_type = "Naquadah Instead of Credits" + with self.assertRaises(ValidationError): + ar.full_clean() + + +class AppointmentRequestRescheduleHistory(BaseTest): + @classmethod + def setUpTestData(cls): + return super().setUpTestData() + + @classmethod + def tearDownClass(cls): + return super().tearDownClass() + + def setUp(self) -> None: + service = deepcopy(self.service1) + service.reschedule_limit = 2 + service.allow_rescheduling = True + service.save() + self.ar_ = self.create_appt_request_for_sm1(service=service) + return super().setUp() + + def test_ar_can_be_reschedule(self): + self.assertTrue(self.ar_.can_be_rescheduled()) + + def test_reschedule_attempts_increment(self): + self.assertTrue(self.ar_.can_be_rescheduled()) + self.ar_.increment_reschedule_attempts() + self.assertEqual(self.ar_.reschedule_attempts, 1) + self.assertTrue(self.ar_.can_be_rescheduled()) + self.ar_.increment_reschedule_attempts() + self.assertEqual(self.ar_.reschedule_attempts, 2) + self.assertFalse(self.ar_.can_be_rescheduled()) + + def test_no_reschedule_history(self): + service = deepcopy(self.service1) + ar = self.create_appointment_request_(service, self.staff_member1) + self.assertFalse(ar.get_reschedule_history().exists()) diff --git a/appointment/tests/models/test_appointment_reschedule_history.py b/appointment/tests/models/test_appointment_reschedule_history.py new file mode 100644 index 0000000..a3079db --- /dev/null +++ b/appointment/tests/models/test_appointment_reschedule_history.py @@ -0,0 +1,112 @@ +from datetime import timedelta + +from django.core.exceptions import ValidationError +from django.utils import timezone + +from appointment.models import AppointmentRescheduleHistory +from appointment.tests.base.base_test import BaseTest + + +class AppointmentRescheduleHistoryCreationTests(BaseTest): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + + def setUp(self): + self.appointment_request = self.create_appt_request_for_sm1() + self.future_date = timezone.now().date() + timedelta(days=3) + return super().setUp() + + def test_reschedule_history_creation_with_valid_data(self): + reschedule_history = AppointmentRescheduleHistory.objects.create( + appointment_request=self.appointment_request, + date=self.future_date, + start_time=timezone.now().time(), + end_time=(timezone.now() + timedelta(hours=1)).time(), + staff_member=self.staff_member1, + reason_for_rescheduling="Client request", + reschedule_status='pending' + ) + self.assertIsNotNone(reschedule_history) + self.assertEqual(reschedule_history.reschedule_status, 'pending') + self.assertTrue(reschedule_history.still_valid()) + + def test_auto_generation_of_id_request_on_creation(self): + reschedule_history = AppointmentRescheduleHistory.objects.create( + appointment_request=self.appointment_request, + date=self.future_date, + start_time=timezone.now().time(), + end_time=(timezone.now() + timedelta(hours=1)).time(), + staff_member=self.staff_member1 + ) + self.assertIsNotNone(reschedule_history.id_request) + + +class AppointmentRescheduleHistoryValidationTests(BaseTest): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + cls.past_date = timezone.now().date() - timedelta(days=3) + cls.future_date = timezone.now().date() + timedelta(days=3) + + def setUp(self): + self.appointment_request = self.create_appt_request_for_sm1() + + def test_creation_with_past_date_raises_validation_error(self): + with self.assertRaises(ValidationError): + AppointmentRescheduleHistory.objects.create( + appointment_request=self.appointment_request, + date=self.past_date, + start_time=timezone.now().time(), + end_time=(timezone.now() + timedelta(hours=1)).time(), + staff_member=self.staff_member1 + ) + + def test_invalid_date_format_raises_type_error(self): + with self.assertRaises(TypeError): + AppointmentRescheduleHistory.objects.create( + appointment_request=self.appointment_request, + date="invalid-date", + start_time=timezone.now().time(), + end_time=(timezone.now() + timedelta(hours=1)).time(), + staff_member=self.staff_member1 + ) + + +class AppointmentRescheduleHistoryTimingTests(BaseTest): + @classmethod + def setUpTestData(cls): + return super().setUpTestData() + + def setUp(self): + self.appointment_request = self.create_appt_request_for_sm1() + self.future_date = timezone.now().date() + timedelta(days=3) + return super().setUp() + + def test_still_valid_within_time_frame(self): + reschedule_history = AppointmentRescheduleHistory.objects.create( + appointment_request=self.appointment_request, + date=self.future_date, + start_time=timezone.now().time(), + end_time=(timezone.now() + timedelta(hours=1)).time(), + staff_member=self.staff_member1, + reason_for_rescheduling="Client request", + reschedule_status='pending' + ) + self.assertTrue(reschedule_history.still_valid()) + + def test_still_valid_outside_time_frame(self): + reschedule_history = AppointmentRescheduleHistory.objects.create( + appointment_request=self.appointment_request, + date=self.future_date, + start_time=timezone.now().time(), + end_time=(timezone.now() + timedelta(hours=1)).time(), + staff_member=self.staff_member1, + reason_for_rescheduling="Client request", + reschedule_status='pending' + ) + self.assertTrue(reschedule_history.still_valid()) + # Simulate passage of time beyond the validity window + reschedule_history.created_at -= timedelta(minutes=6) + reschedule_history.save() + self.assertFalse(reschedule_history.still_valid()) diff --git a/appointment/tests/models/test_config.py b/appointment/tests/models/test_config.py new file mode 100644 index 0000000..e49a813 --- /dev/null +++ b/appointment/tests/models/test_config.py @@ -0,0 +1,128 @@ +from datetime import time + +from django.core.cache import cache +from django.core.exceptions import ValidationError +from django.test import TestCase, override_settings + +from appointment.models import Config + + +class ConfigCreationTestCase(TestCase): + def setUp(self): + self.config = Config.objects.create(slot_duration=30, lead_time=time(9, 0), + finish_time=time(17, 0), appointment_buffer_time=2.0, + website_name="Stargate Command") + + @override_settings(DEBUG=True) + def tearDown(self): + Config.objects.all().delete() + cache.clear() + super().tearDown() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def test_default_attributes_on_creation(self): + self.assertIsNotNone(self.config) + self.assertEqual(self.config.slot_duration, 30) + self.assertEqual(self.config.lead_time, time(9, 0)) + self.assertEqual(self.config.finish_time, time(17, 0)) + self.assertEqual(self.config.appointment_buffer_time, 2.0) + self.assertEqual(self.config.website_name, "Stargate Command") + + def test_multiple_config_creation(self): + """Test that only one configuration can be created.""" + with self.assertRaises(ValidationError): + Config.objects.create(slot_duration=20, lead_time=time(8, 0), finish_time=time(18, 0)) + + def test_config_str_method(self): + """Test that the string representation of a configuration is correct.""" + expected_str = f"Config {self.config.pk}: slot_duration=30, lead_time=09:00:00, finish_time=17:00:00" + self.assertEqual(str(self.config), expected_str) + + +class ConfigUpdateTestCase(TestCase): + def setUp(self): + self.config = Config.objects.create(slot_duration=30, lead_time=time(9, 0), + finish_time=time(17, 0), appointment_buffer_time=2.0, + website_name="Stargate Command") + + @override_settings(DEBUG=True) + def tearDown(self): + Config.objects.all().delete() + cache.clear() + super().tearDown() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def test_editing_existing_config(self): + """Test that an existing configuration can be edited.""" + self.config.slot_duration = 41 + self.config.website_name = "Cheyeene Mountain Complex" + self.config.save() + + updated_config = Config.objects.get(pk=self.config.pk) + self.assertEqual(updated_config.website_name, "Cheyeene Mountain Complex") + self.assertEqual(updated_config.slot_duration, 41) + + +class ConfigDeletionTestCase(TestCase): + def setUp(self): + self.config = Config.objects.create(slot_duration=30, lead_time=time(9, 0), + finish_time=time(17, 0), appointment_buffer_time=2.0, + website_name="Stargate Command") + + @override_settings(DEBUG=True) + def tearDown(self): + Config.objects.all().delete() + cache.clear() + super().tearDown() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def test_cant_delete_config(self): + """Test that a configuration cannot be deleted.""" + self.config.delete() + self.assertIsNotNone(Config.objects.first()) + + +class ConfigDurationValidationTestCase(TestCase): + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def test_invalid_slot_duration(self): + """Test that a configuration cannot be created with a negative slot duration.""" + with self.assertRaises(ValidationError): + Config.objects.create(slot_duration=-10, lead_time=time(9, 0), finish_time=time(17, 0)) + with self.assertRaises(ValidationError): + Config.objects.create(slot_duration=0, lead_time=time(9, 0), finish_time=time(17, 0)) + + +class ConfigTimeValidationTestCase(TestCase): + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def test_lead_time_greater_than_finish_time(self): + # TODO: Think about business with night shifts, start time will be greater than finish time, + # but again, not sure client will use this app for night shifts + """Test that lead time cannot be greater than finish time.""" + with self.assertRaises(ValidationError): + Config.objects.create(slot_duration=30, lead_time=time(18, 0), finish_time=time(9, 0)) + + def test_same_lead_and_finish_time(self): + """Test that a configuration cannot be created with the same lead time and finish time.""" + with self.assertRaises(ValidationError): + Config.objects.create(slot_duration=30, lead_time=time(9, 0), finish_time=time(9, 0)) + + def test_negative_appointment_buffer_time(self): + """Test that a configuration cannot be created with a negative appointment buffer time.""" + with self.assertRaises(ValidationError): + Config.objects.create(slot_duration=30, lead_time=time(9, 0), finish_time=time(17, 0), + appointment_buffer_time=-2.0) diff --git a/appointment/tests/models/test_day_off.py b/appointment/tests/models/test_day_off.py new file mode 100644 index 0000000..ac4fc1b --- /dev/null +++ b/appointment/tests/models/test_day_off.py @@ -0,0 +1,69 @@ +from copy import deepcopy +from datetime import date, timedelta + +from django.core.exceptions import ValidationError +from django.db import IntegrityError + +from appointment.models import DayOff +from appointment.tests.base.base_test import BaseTest + + +class DayOffCreationTestCase(BaseTest): + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + @classmethod + def setUpTestData(cls): + super().setUpTestData() + + def setUp(self): + self.day_off = DayOff.objects.create( + staff_member=self.staff_member1, + start_date=date.today(), + end_date=date.today() + timedelta(days=2) + ) + super().setUp() + + def tearDown(self): + super().tearDown() + DayOff.objects.all().delete() + + def test_default_attributes_on_creation(self): + """Test basic creation of DayOff.""" + self.assertIsNotNone(self.day_off) + self.assertEqual(self.day_off.staff_member, self.staff_member1) + self.assertTrue(self.day_off.is_owner(self.users['staff1'].id)) + self.assertFalse(self.day_off.is_owner(9999)) # Assuming 9999 is not a valid user ID + + def test_day_off_str_method(self): + """Test that the string representation of a day off is correct.""" + self.assertEqual(str(self.day_off), f"{date.today()} to {date.today() + timedelta(days=2)} - Day off") + day_off = deepcopy(self.day_off) + # Testing with a description + day_off.description = "Vacation" + day_off.save() + self.assertEqual(str(day_off), f"{date.today()} to {date.today() + timedelta(days=2)} - Vacation") + + +class DayOffModelTestCase(BaseTest): + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def test_day_off_start_date_before_end_date(self): + """Test that start date must be before end date upon day off creation.""" + with self.assertRaises(ValidationError): + DayOff.objects.create( + staff_member=self.staff_member1, + start_date=date.today() + timedelta(days=1), + end_date=date.today() + ).clean() + + def test_day_off_without_staff_member(self): + """Test that a day off cannot be created without a staff member.""" + with self.assertRaises(IntegrityError): + DayOff.objects.create( + start_date=date.today(), + end_date=date.today() + timedelta(days=1) + ) diff --git a/appointment/tests/models/test_email_verification.py b/appointment/tests/models/test_email_verification.py new file mode 100644 index 0000000..877f12c --- /dev/null +++ b/appointment/tests/models/test_email_verification.py @@ -0,0 +1,51 @@ +import string + +from django.test import TestCase + +from appointment.models import EmailVerificationCode +from appointment.tests.mixins.base_mixin import UserMixin + + +class EmailVerificationCodeBasicTestCase(TestCase, UserMixin): + def setUp(self): + self.user = self.create_user_() + self.code = EmailVerificationCode.generate_code(self.user) + + def tearDown(self): + super().tearDown() + EmailVerificationCode.objects.all().delete() + self.user.delete() + + def test_default_attributes_on_creation(self): + """Test if a verification code can be generated.""" + verification_code = EmailVerificationCode.objects.get(user=self.user) + self.assertIsNotNone(verification_code) + self.assertEqual(verification_code.code, self.code) + self.assertEqual(str(verification_code), self.code) + self.assertIsNotNone(verification_code.created_at) + self.assertIsNotNone(verification_code.updated_at) + self.assertEqual(len(self.code), 6) + + def test_code_content(self): + """Test that the code only contains uppercase letters and digits.""" + valid_characters = set(string.ascii_uppercase + string.digits) + self.assertTrue(all(char in valid_characters for char in self.code)) + + def test_multiple_codes_for_user(self): + """ + Test if multiple verification codes can be generated for a user. + This should ideally create a new code, but the old one will still exist. + """ + new_code = EmailVerificationCode.generate_code(self.user) + self.assertNotEqual(self.code, new_code) + + def test_code_verification_match(self): + """The check_code method returns True when the code matches.""" + code = EmailVerificationCode.objects.get(user=self.user) + self.assertTrue(code.check_code(self.code)) + + def test_code_verification_mismatch(self): + """The check_code method returns False when the code does not match.""" + mismatched_code = "ABCDEF" + code = EmailVerificationCode.objects.get(user=self.user) + self.assertFalse(code.check_code(mismatched_code)) diff --git a/appointment/tests/models/test_payment_info.py b/appointment/tests/models/test_payment_info.py new file mode 100644 index 0000000..e2a21d9 --- /dev/null +++ b/appointment/tests/models/test_payment_info.py @@ -0,0 +1,74 @@ +from appointment.models import PaymentInfo +from appointment.tests.base.base_test import BaseTest + + +class PaymentInfoBasicTestCase(BaseTest): + @classmethod + def setUpTestData(cls): + return super().setUpTestData() + + @classmethod + def tearDownClass(cls): + return super().tearDownClass() + + def setUp(self): + self.ar = self.create_appt_request_for_sm1() + self.appointment = self.create_appt_for_sm1(appointment_request=self.ar) + self.payment_info = PaymentInfo.objects.create(appointment=self.appointment) + return super().setUp() + + def tearDown(self): + self.ar.delete() + self.appointment.delete() + self.payment_info.delete() + return super().tearDown() + + def test_str_representation(self): + """Test if a payment info's string representation is correct.""" + self.assertEqual(str(self.payment_info), f"{self.service1.name} - {self.service1.price}") + + def test_payment_info_creation(self): + """Test if a payment info can be created.""" + payment_info = PaymentInfo.objects.get(appointment=self.appointment) + self.assertIsNotNone(payment_info) + self.assertEqual(payment_info.appointment, self.appointment) + self.assertEqual(self.payment_info.get_id_request(), self.appointment.get_appointment_id_request()) + self.assertEqual(self.payment_info.get_amount_to_pay(), self.appointment.get_appointment_amount_to_pay()) + self.assertEqual(self.payment_info.get_currency(), self.appointment.get_appointment_currency()) + self.assertEqual(self.payment_info.get_name(), self.appointment.get_service_name()) + self.assertIsNotNone(self.payment_info.created_at) + self.assertIsNotNone(self.payment_info.updated_at) + + def test_get_user_info(self): + """Test if payment info's username is correct.""" + self.assertEqual(self.payment_info.get_user_name(), self.users['client1'].first_name) + self.assertEqual(self.payment_info.get_user_email(), self.users['client1'].email) + + +class PaymentInfoStatusTestCase(BaseTest): + @classmethod + def setUpTestData(cls): + return super().setUpTestData() + + @classmethod + def tearDownClass(cls): + return super().tearDownClass() + + def setUp(self): + self.ar = self.create_appt_request_for_sm1() + self.appointment = self.create_appt_for_sm1(appointment_request=self.ar) + self.payment_info = PaymentInfo.objects.create(appointment=self.appointment) + return super().setUp() + + def tearDown(self): + self.ar.delete() + self.appointment.delete() + self.payment_info.delete() + return super().tearDown() + + def test_set_paid_status(self): + """Test if a payment info's paid status can be set correctly.""" + self.payment_info.set_paid_status(True) + self.assertTrue(self.appointment.is_paid()) + self.payment_info.set_paid_status(False) + self.assertFalse(self.appointment.is_paid()) diff --git a/appointment/tests/models/test_service.py b/appointment/tests/models/test_service.py index ac5a4b8..883f011 100644 --- a/appointment/tests/models/test_service.py +++ b/appointment/tests/models/test_service.py @@ -25,7 +25,7 @@ def test_service_creation(self): def test_basic_attributes_verification(self): self.assertEqual(self.service.name, "Stargate Activation") self.assertEqual(self.service.description, "Activate the Stargate") - self.assertEqual(self.service.duration, self.service1.duration) + self.assertEqual(self.service.duration, timedelta(hours=1)) def test_timestamps_on_creation(self): """Newly created services should have created_at and updated_at values.""" diff --git a/appointment/tests/models/test_staff_member.py b/appointment/tests/models/test_staff_member.py index e3e4d76..f3e47a7 100644 --- a/appointment/tests/models/test_staff_member.py +++ b/appointment/tests/models/test_staff_member.py @@ -29,6 +29,9 @@ def test_default_attributes_on_creation(self): self.assertIsNone(self.staff_member.finish_time) self.assertIsNone(self.staff_member.slot_duration) self.assertIsNone(self.staff_member.appointment_buffer_time) + self.assertIsNotNone(self.staff_member.created_at) + expected_str = self.staff_member.get_staff_member_name() + self.assertEqual(str(self.staff_member), expected_str) def test_creation_without_service(self): """A staff member can be created without a service.""" @@ -77,6 +80,7 @@ def test_services_offered(self): """Test get_services_offered & get_service_offered_text function.""" self.assertIn(self.service, self.staff_member.get_services_offered()) self.assertEqual(self.staff_member.get_service_offered_text(), self.service.name) + self.assertTrue(self.staff_member.get_service_is_offered(self.service.pk)) def test_staff_member_with_non_existent_service(self): """A staff member cannot offer a non-existent service.""" @@ -201,19 +205,10 @@ def tearDownClass(cls): def get_fresh_staff_member(self): return deepcopy(self.staff_member) - def test_staff_member_string_output(self): - """Test the string representation of a StaffMember.""" - expected_str = self.staff_member.get_staff_member_name() - self.assertEqual(str(self.staff_member), expected_str) - def test_get_staff_member_first_name(self): """Test that the staff member's first name is returned.""" self.assertEqual(self.staff_member.get_staff_member_first_name(), self.staff.first_name) - def test_date_joined_auto_creation(self): - """Test if the date_joined field is automatically set upon creation.""" - self.assertIsNotNone(self.staff_member.created_at) - def test_config_values_takes_over_when_sm_values_null(self): """When some values are null in the StaffMember, the Config values should be used.""" config = Config.objects.create( From 98fe67eb74f1dfa1345b80cca6d9711c34a61ad1 Mon Sep 17 00:00:00 2001 From: Adams Pierre David <57180807+adamspd@users.noreply.github.com> Date: Sat, 18 May 2024 06:33:48 +0200 Subject: [PATCH 6/7] Added the rest of the test --- appointment/models.py | 2 +- appointment/tests/models/test_appointment.py | 224 +++ .../tests/models/test_appointment_request.py | 2 +- appointment/tests/models/test_config.py | 1 + .../tests/models/test_password_reset_token.py | 221 +++ appointment/tests/models/test_payment_info.py | 8 +- .../tests/models/test_working_hours.py | 104 ++ appointment/tests/test_services.py | 904 ++++++++++++ appointment/tests/test_settings.py | 45 + appointment/tests/test_tasks.py | 52 + appointment/tests/test_views.py | 1258 +++++++++++++++++ appointment/tests/utils/__init__.py | 0 appointment/tests/utils/test_date_time.py | 389 +++++ appointment/tests/utils/test_db_helpers.py | 1095 ++++++++++++++ appointment/tests/utils/test_email_ops.py | 231 +++ appointment/tests/utils/test_json_context.py | 117 ++ appointment/tests/utils/test_permissions.py | 132 ++ appointment/tests/utils/test_session.py | 163 +++ .../tests/utils/test_staff_member_time.py | 118 ++ appointment/tests/utils/test_validators.py | 31 + appointment/tests/utils/test_view_helpers.py | 55 + appointment/utils/db_helpers.py | 36 +- 22 files changed, 5160 insertions(+), 28 deletions(-) create mode 100644 appointment/tests/models/test_appointment.py create mode 100644 appointment/tests/models/test_password_reset_token.py create mode 100644 appointment/tests/models/test_working_hours.py create mode 100644 appointment/tests/test_services.py create mode 100644 appointment/tests/test_settings.py create mode 100644 appointment/tests/test_tasks.py create mode 100644 appointment/tests/test_views.py create mode 100644 appointment/tests/utils/__init__.py create mode 100644 appointment/tests/utils/test_date_time.py create mode 100644 appointment/tests/utils/test_db_helpers.py create mode 100644 appointment/tests/utils/test_email_ops.py create mode 100644 appointment/tests/utils/test_json_context.py create mode 100644 appointment/tests/utils/test_permissions.py create mode 100644 appointment/tests/utils/test_session.py create mode 100644 appointment/tests/utils/test_staff_member_time.py create mode 100644 appointment/tests/utils/test_validators.py create mode 100644 appointment/tests/utils/test_view_helpers.py diff --git a/appointment/models.py b/appointment/models.py index 55e800e..0bec7ee 100644 --- a/appointment/models.py +++ b/appointment/models.py @@ -611,7 +611,7 @@ def is_owner(self, staff_user_id): def to_dict(self): return { "id": self.id, - "client_name": self.client.get_full_name(), + "client_name": self.get_client_name(), "client_email": self.client.email, "start_time": self.appointment_request.start_time.strftime('%Y-%m-%d %H:%M'), "end_time": self.appointment_request.end_time.strftime('%Y-%m-%d %H:%M'), diff --git a/appointment/tests/models/test_appointment.py b/appointment/tests/models/test_appointment.py new file mode 100644 index 0000000..4474abf --- /dev/null +++ b/appointment/tests/models/test_appointment.py @@ -0,0 +1,224 @@ +from copy import deepcopy +from datetime import datetime, time, timedelta + +from django.core.exceptions import ValidationError +from django.utils import timezone +from django.utils.translation import gettext as _ + +from appointment.models import Appointment, DayOff, WorkingHours +from appointment.tests.base.base_test import BaseTest +from appointment.utils.date_time import get_weekday_num + + +class AppointmentCreationTestCase(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + self.address = 'Stargate Command, Cheyenne Mountain Complex, Colorado Springs, CO' + self.ar = self.create_appt_request_for_sm1() + self.appointment = self.create_appt_for_sm1(appointment_request=self.ar) + self.client_ = self.users['client1'] + self.expected_end_time = datetime.combine(self.ar.date, self.ar.end_time) + self.expected_service_name = 'Stargate Activation' + self.expected_service_price = 100000 + self.expected_start_time = datetime.combine(self.ar.date, self.ar.start_time) + self.phone = '+12392340543' + return super().setUp() + + def tearDown(self): + self.appointment.delete() + return super().tearDown() + + def test_default_attributes_on_creation(self): + """Test default attributes when an appointment is created.""" + self.assertIsNotNone(self.appointment) + self.assertIsNotNone(self.appointment.created_at) + self.assertIsNotNone(self.appointment.updated_at) + self.assertIsNotNone(self.appointment.get_appointment_id_request()) + self.assertIsNone(self.appointment.additional_info) + self.assertEqual(self.appointment.client, self.users['client1']) + self.assertEqual(self.appointment.phone, self.phone) + self.assertEqual(self.appointment.address, self.address) + self.assertFalse(self.appointment.want_reminder) + + def test_str_representation(self): + """Test if an appointment's string representation is correct.""" + expected_str = f"{self.client_} - {self.ar.start_time.strftime('%Y-%m-%d %H:%M')} to " \ + f"{self.ar.end_time.strftime('%Y-%m-%d %H:%M')}" + self.assertEqual(str(self.appointment), expected_str) + + def test_appointment_getters(self): + """Test getter methods for appointment details.""" + self.assertEqual(self.appointment.get_start_time(), self.expected_start_time) + self.assertEqual(self.appointment.get_end_time(), self.expected_end_time) + self.assertEqual(self.appointment.get_service_name(), self.expected_service_name) + self.assertEqual(self.appointment.get_service_price(), self.expected_service_price) + self.assertEqual(self.appointment.is_paid_text(), _('No')) + self.assertEqual(self.appointment.get_appointment_amount_to_pay(), self.expected_service_price) + self.assertEqual(self.appointment.get_service_down_payment(), self.service1.get_down_payment()) + self.assertEqual(self.appointment.get_service_description(), self.service1.description) + self.assertEqual(self.appointment.get_appointment_date(), self.ar.date) + self.assertEqual(self.appointment.get_service_duration(), "1 hour") + self.assertEqual(self.appointment.get_appointment_currency(), "USD") + self.assertEqual(self.appointment.get_appointment_amount_to_pay(), self.ar.get_service_price()) + self.assertEqual(self.appointment.get_service_img_url(), "") + self.assertEqual(self.appointment.get_staff_member_name(), self.staff_member1.get_staff_member_name()) + self.assertTrue(self.appointment.service_is_paid()) + self.assertFalse(self.appointment.is_paid()) + + def test_conversion_to_dict(self): + response = { + 'id': 1, + 'client_name': self.client_.first_name, + 'client_email': self.client_.email, + 'start_time': '1900-01-01 09:00', + 'end_time': '1900-01-01 10:00', + 'service_name': self.expected_service_name, + 'address': self.address, + 'want_reminder': False, + 'additional_info': None, + 'paid': False, + 'amount_to_pay': self.expected_service_price, + } + actual_response = self.appointment.to_dict() + actual_response.pop('id_request', None) + self.assertEqual(actual_response, response) + + +class AppointmentValidDateTestCase(BaseTest): + def setUp(self): + super().setUp() + self.weekday = "Monday" # Example weekday + self.weekday_num = get_weekday_num(self.weekday) + self.wh = WorkingHours.objects.create(staff_member=self.staff_member1, day_of_week=self.weekday_num, + start_time=time(9, 0), end_time=time(17, 0)) + self.appt_date = timezone.now().date() + timedelta(days=(self.weekday_num - timezone.now().weekday()) % 7) + self.start_time = timezone.now().replace(hour=10, minute=0, second=0, microsecond=0) + self.current_appointment_id = None + + def tearDown(self): + self.wh.delete() + return super().tearDown() + + def test_staff_member_works_on_given_day(self): + is_valid, message = Appointment.is_valid_date(self.appt_date, self.start_time, self.staff_member1, + self.current_appointment_id, self.weekday) + self.assertTrue(is_valid) + + def test_staff_member_does_not_work_on_given_day(self): + non_working_day = "Sunday" + non_working_day_num = get_weekday_num(non_working_day) + appt_date = self.appt_date + timedelta(days=(non_working_day_num - self.weekday_num) % 7) + is_valid, message = Appointment.is_valid_date(appt_date, self.start_time, self.staff_member1, + self.current_appointment_id, non_working_day) + self.assertFalse(is_valid) + self.assertIn("does not work on this day", message) + + def test_start_time_outside_working_hours(self): + early_start_time = timezone.now().replace(hour=8, minute=0) # Before working hours + is_valid, message = Appointment.is_valid_date(self.appt_date, early_start_time, self.staff_member1, + self.current_appointment_id, self.weekday) + self.assertFalse(is_valid) + self.assertIn("outside of", message) + + def test_staff_member_has_day_off(self): + DayOff.objects.create(staff_member=self.staff_member1, start_date=self.appt_date, end_date=self.appt_date) + is_valid, message = Appointment.is_valid_date(self.appt_date, self.start_time, self.staff_member1, + self.current_appointment_id, self.weekday) + self.assertFalse(is_valid) + self.assertIn("has a day off on this date", message) + + +class AppointmentValidationTestCase(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + self.tomorrow = timezone.now().date() + timedelta(days=1) + self.ar = self.create_appointment_request_(service=self.service2, staff_member=self.staff_member2, + date_=self.tomorrow) + self.appointment = self.create_appt_for_sm2(appointment_request=self.ar) + self.client_ = self.users['client1'] + return super().setUp() + + def tearDown(self): + self.appointment.delete() + return super().tearDown() + + def test_invalid_phone_number(self): + """Test that an appointment cannot be created with an invalid phone number.""" + self.appointment.phone = "1234" # Invalid phone number + with self.assertRaises(ValidationError): + self.appointment.full_clean() + + def test_set_paid_status(self): + """Test if an appointment's paid status can be set.""" + appointment = deepcopy(self.appointment) + appointment.set_appointment_paid_status(True) + self.assertTrue(appointment.is_paid()) + appointment.set_appointment_paid_status(False) + self.assertFalse(appointment.is_paid()) + + def test_save_with_down_payment(self): + """Test if an appointment can be saved with a down payment.""" + ar = self.create_appointment_request_(service=self.service2, staff_member=self.staff_member2, + date_=self.tomorrow) + appointment = self.create_appt_for_sm2(appointment_request=ar) + ar.payment_type = 'down' + ar.save() + appointment.save() + self.assertEqual(appointment.get_service_down_payment(), self.service2.get_down_payment()) + + def test_creation_without_appointment_request(self): + """Test that an appointment cannot be created without an appointment request.""" + with self.assertRaises(ValidationError): # Assuming model validation prevents this + Appointment.objects.create(client=self.client_) + + def test_creation_without_client(self): + """Test that an appointment can be created without a client.""" + ar = self.create_appointment_request_(service=self.service2, staff_member=self.staff_member2, + date_=self.tomorrow) + appt = Appointment.objects.create(appointment_request=ar) + self.assertIsNone(appt.client) + + def test_creation_without_required_fields(self): + """Test that an appointment cannot be created without the required fields.""" + with self.assertRaises(ValidationError): + Appointment.objects.create() + + def test_get_staff_member_name_without_staff_member(self): + """Test if you get_staff_member_name method returns an empty string when no staff member is associated.""" + ar = self.create_appointment_request_(service=self.service2, staff_member=self.staff_member2, + date_=self.tomorrow) + appointment = self.create_appt_for_sm2(appointment_request=ar) + appointment.appointment_request.staff_member = None + appointment.appointment_request.save() + self.assertEqual(appointment.get_staff_member_name(), "") + + def test_rescheduling(self): + """Simulate appointment rescheduling by changing the appointment date and times.""" + ar = self.create_appointment_request_(service=self.service2, staff_member=self.staff_member2, + date_=self.tomorrow) + appointment = self.create_appt_for_sm2(appointment_request=ar) + new_date = ar.date + timedelta(days=1) + new_start_time = time(10, 0) + new_end_time = time(11, 0) + ar.date = new_date + ar.start_time = new_start_time + ar.end_time = new_end_time + ar.save() + + self.assertEqual(appointment.get_date(), new_date) + self.assertEqual(appointment.get_start_time().time(), new_start_time) + self.assertEqual(appointment.get_end_time().time(), new_end_time) diff --git a/appointment/tests/models/test_appointment_request.py b/appointment/tests/models/test_appointment_request.py index 6d1b5d7..f31ee7b 100644 --- a/appointment/tests/models/test_appointment_request.py +++ b/appointment/tests/models/test_appointment_request.py @@ -24,7 +24,7 @@ def tearDown(self): self.ar.delete() super().tearDown() - def test_appointment_request_is_properly_created(self): + def test_default_attributes_on_creation(self): self.assertIsNotNone(self.ar) self.assertEqual(self.ar.service, self.service1) self.assertEqual(self.ar.staff_member, self.staff_member1) diff --git a/appointment/tests/models/test_config.py b/appointment/tests/models/test_config.py index e49a813..0c28a5c 100644 --- a/appointment/tests/models/test_config.py +++ b/appointment/tests/models/test_config.py @@ -30,6 +30,7 @@ def test_default_attributes_on_creation(self): self.assertEqual(self.config.finish_time, time(17, 0)) self.assertEqual(self.config.appointment_buffer_time, 2.0) self.assertEqual(self.config.website_name, "Stargate Command") + self.assertIsNotNone(Config.get_instance()) def test_multiple_config_creation(self): """Test that only one configuration can be created.""" diff --git a/appointment/tests/models/test_password_reset_token.py b/appointment/tests/models/test_password_reset_token.py new file mode 100644 index 0000000..decc0fb --- /dev/null +++ b/appointment/tests/models/test_password_reset_token.py @@ -0,0 +1,221 @@ +import datetime +import time + +from django.utils import timezone + +from appointment.models import PasswordResetToken +from appointment.tests.base.base_test import BaseTest + + +class PasswordResetTokenCreationTests(BaseTest): + + def setUp(self): + super().setUp() + self.user = self.create_user_(username='janet.fraiser', email='janet.fraiser@django-appointment.com', + password='LovedCassandra', first_name='Janet') + self.expired_time = timezone.now() - datetime.timedelta(minutes=5) + self.token = PasswordResetToken.create_token(user=self.user) + + def tearDown(self): + super().tearDown() + self.user.delete() + self.token.delete() + + def test_default_attributes_on_creation(self): + self.assertIsNotNone(self.token) + self.assertFalse(self.token.is_expired) + self.assertFalse(self.token.is_verified) + + def test_str_representation(self): + """Test the string representation of the token.""" + expected_str = (f"Password reset token for {self.user} " + f"[{self.token.token} status: {self.token.status} expires at {self.token.expires_at}]") + self.assertEqual(str(self.token), expected_str) + + +class PasswordResetTokenPropertiesTest(BaseTest): + def setUp(self): + super().setUp() + self.user = self.create_user_(username='janet.fraiser', email='janet.fraiser@django-appointment.com', + password='LovedCassandra', first_name='Janet') + self.expired_time = timezone.now() - datetime.timedelta(minutes=5) + self.token = PasswordResetToken.create_token(user=self.user) + + def tearDown(self): + super().tearDown() + self.user.delete() + self.token.delete() + + def test_is_verified_property(self): + """Test the is_verified property to check if the token status is correctly identified as verified.""" + token = PasswordResetToken.create_token(self.user) + self.assertFalse(token.is_verified, "Newly created token should not be verified.") + token.mark_as_verified() + self.assertTrue(token.is_verified, "Token should be marked as verified after calling mark_as_verified.") + + def test_is_active_property(self): + """Test the is_active property to check if the token status is correctly identified as active.""" + token = PasswordResetToken.create_token(self.user) + self.assertTrue(token.is_active, "Newly created token should be active.") + token.mark_as_verified() + token.refresh_from_db() + self.assertFalse(token.is_active, "Token should not be active after being verified.") + + # Invalidate the token and check is_active property + token.status = PasswordResetToken.TokenStatus.INVALIDATED + token.save() + self.assertFalse(token.is_active, "Token should not be active after being invalidated.") + + def test_is_invalidated_property(self): + """Test the is_invalidated property to check if the token status is correctly identified as invalidated.""" + token = PasswordResetToken.create_token(self.user) + self.assertFalse(token.is_invalidated, "Newly created token should not be invalidated.") + + # Invalidate the token and check is_invalidated property + token.status = PasswordResetToken.TokenStatus.INVALIDATED + token.save() + self.assertTrue(token.is_invalidated, "Token should be marked as invalidated after status change.") + + def test_token_expiration(self): + """Test that a token is considered expired after the expiration time.""" + token = PasswordResetToken.create_token(user=self.user, expiration_minutes=-1) # Token already expired + self.assertTrue(token.is_expired) + + +class PasswordResetTokenVerificationTests(BaseTest): + def setUp(self): + super().setUp() + self.user = self.create_user_(username='janet.fraiser', email='janet.fraiser@django-appointment.com', + password='LovedCassandra', first_name='Janet') + self.expired_time = timezone.now() - datetime.timedelta(minutes=5) + + def test_verify_token_success(self): + """Test successful token verification.""" + token = PasswordResetToken.create_token(user=self.user) + verified_token = PasswordResetToken.verify_token(user=self.user, token=token.token) + self.assertIsNotNone(verified_token) + + def test_verify_token_failure_expired(self): + """Test token verification fails if the token has expired.""" + token = PasswordResetToken.create_token(user=self.user, expiration_minutes=-1) # Token already expired + verified_token = PasswordResetToken.verify_token(user=self.user, token=token.token) + + self.assertIsNone(verified_token, "Expired token should not verify") + + def test_verify_token_failure_wrong_user(self): + """Test token verification fails if the token does not belong to the given user.""" + another_user = self.create_user_(username='another_user', email='another@example.com', + password='test_pass456') + token = PasswordResetToken.create_token(user=self.user) + verified_token = PasswordResetToken.verify_token(user=another_user, token=token.token) + self.assertIsNone(verified_token) + + def test_verify_token_failure_already_verified(self): + """Test token verification fails if the token has already been verified.""" + token = PasswordResetToken.create_token(user=self.user) + token.mark_as_verified() + verified_token = PasswordResetToken.verify_token(user=self.user, token=token.token) + self.assertIsNone(verified_token) + + def test_verify_token_invalid_token(self): + """Test token verification fails if the token does not exist.""" + PasswordResetToken.create_token(user=self.user) + invalid_token_uuid = "12345678-1234-1234-1234-123456789012" # An invalid token UUID + verified_token = PasswordResetToken.verify_token(user=self.user, token=invalid_token_uuid) + self.assertIsNone(verified_token) + + def test_token_expiration_boundary(self): + """Test token verification at the exact moment of expiration.""" + token = PasswordResetToken.create_token(user=self.user, expiration_minutes=0) # Token expires now + # Assuming there might be a very slight delay before verification, we wait a second + time.sleep(1) + verified_token = PasswordResetToken.verify_token(user=self.user, token=token.token) + self.assertIsNone(verified_token) + + def test_create_multiple_tokens_for_user(self): + """Test that multiple tokens can be created for a single user and only the latest is valid.""" + old_token = PasswordResetToken.create_token(user=self.user) + new_token = PasswordResetToken.create_token(user=self.user) + + old_verified = PasswordResetToken.verify_token(user=self.user, token=old_token.token) + new_verified = PasswordResetToken.verify_token(user=self.user, token=new_token.token) + + self.assertIsNone(old_verified, "Old token should not be valid after creating a new one") + self.assertIsNotNone(new_verified, "New token should be valid") + + def test_token_verification_resets_after_expiration(self): + """Test that an expired token cannot be verified after its expiration, even if marked as verified.""" + token = PasswordResetToken.create_token(user=self.user, expiration_minutes=-1) # Already expired + token.mark_as_verified() + + verified_token = PasswordResetToken.verify_token(user=self.user, token=token.token) + self.assertIsNone(verified_token, "Expired token should not verify, even if marked as verified") + + def test_verify_token_invalidated(self): + """Test token verification fails if the token has been invalidated.""" + token = PasswordResetToken.create_token(self.user) + # Invalidate the token by creating a new one + PasswordResetToken.create_token(self.user) + verified_token = PasswordResetToken.verify_token(self.user, token.token) + self.assertIsNone(verified_token) + + def test_expired_token_verification(self): + """Test that an expired token cannot be verified.""" + token = PasswordResetToken.objects.create(user=self.user, expires_at=self.expired_time, + status=PasswordResetToken.TokenStatus.ACTIVE) + self.assertTrue(token.is_expired) + verified_token = PasswordResetToken.verify_token(self.user, token.token) + self.assertIsNone(verified_token, "Expired token should not verify") + + def test_token_verification_after_user_deletion(self): + """Test that a token cannot be verified after the associated user is deleted.""" + token = PasswordResetToken.create_token(self.user) + self.user.delete() + verified_token = PasswordResetToken.verify_token(None, token.token) + self.assertIsNone(verified_token, "Token should not verify after user deletion") + + +class PasswordResetTokenTests(BaseTest): + def setUp(self): + super().setUp() + self.user = self.create_user_(username='janet.fraiser', email='janet.fraiser@django-appointment.com', + password='LovedCassandra', first_name='Janet') + self.expired_time = timezone.now() - datetime.timedelta(minutes=5) + + def tearDown(self): + super().tearDown() + PasswordResetToken.objects.all().delete() + self.user.delete() + + def test_mark_as_verified(self): + """Test marking a token as verified.""" + token = PasswordResetToken.create_token(user=self.user) + self.assertFalse(token.is_verified) + token.mark_as_verified() + token.refresh_from_db() # Refresh the token object from the database + self.assertTrue(token.is_verified) + + def test_mark_as_verified_is_idempotent(self): + """Test that marking a token as verified multiple times has no adverse effect.""" + token = PasswordResetToken.create_token(user=self.user) + token.mark_as_verified() + first_verification_time = token.updated_at + + time.sleep(1) # Ensure time has passed + token.mark_as_verified() + token.refresh_from_db() + + self.assertTrue(token.is_verified) + self.assertEqual(first_verification_time, token.updated_at, + "Token verification time should not update on subsequent calls") + + def test_deleting_user_cascades_to_tokens(self): + """Test that deleting a user deletes associated password reset tokens.""" + + apophis = self.create_user_(username='apophis.false_god', email='apophis.false_god@django-appointment.com', + password='LovedSayingSholva', first_name='Apophis') + token = PasswordResetToken.create_token(user=apophis) + apophis.delete() + + with self.assertRaises(PasswordResetToken.DoesNotExist): + PasswordResetToken.objects.get(pk=token.pk) diff --git a/appointment/tests/models/test_payment_info.py b/appointment/tests/models/test_payment_info.py index e2a21d9..cd5a5d2 100644 --- a/appointment/tests/models/test_payment_info.py +++ b/appointment/tests/models/test_payment_info.py @@ -24,11 +24,11 @@ def tearDown(self): return super().tearDown() def test_str_representation(self): - """Test if a payment info's string representation is correct.""" + """Test if payment info's string representation is correct.""" self.assertEqual(str(self.payment_info), f"{self.service1.name} - {self.service1.price}") - def test_payment_info_creation(self): - """Test if a payment info can be created.""" + def test_default_attributes_on_creation(self): + """Test if payment info can be created.""" payment_info = PaymentInfo.objects.get(appointment=self.appointment) self.assertIsNotNone(payment_info) self.assertEqual(payment_info.appointment, self.appointment) @@ -67,7 +67,7 @@ def tearDown(self): return super().tearDown() def test_set_paid_status(self): - """Test if a payment info's paid status can be set correctly.""" + """Test if payment info's paid status can be set correctly.""" self.payment_info.set_paid_status(True) self.assertTrue(self.appointment.is_paid()) self.payment_info.set_paid_status(False) diff --git a/appointment/tests/models/test_working_hours.py b/appointment/tests/models/test_working_hours.py new file mode 100644 index 0000000..7afaf58 --- /dev/null +++ b/appointment/tests/models/test_working_hours.py @@ -0,0 +1,104 @@ +from datetime import time + +from django.core.exceptions import ValidationError +from django.db import IntegrityError +from django.test import TestCase +from django.utils.translation import gettext as _ + +from appointment.models import WorkingHours +from appointment.tests.mixins.base_mixin import ServiceMixin, StaffMemberMixin, UserMixin + + +class WorkingHoursModelTestCase(TestCase, UserMixin, ServiceMixin, StaffMemberMixin): + def setUp(self): + self.user = self.create_user_() + self.service = self.create_service_() + self.staff_member = self.create_staff_member_(self.user, self.service) + self.working_hours = WorkingHours.objects.create( + staff_member=self.staff_member, + day_of_week=1, + start_time=time(9, 0), + end_time=time(17, 0) + ) + + def test_default_attributes_on_creation(self): + """Test if a WorkingHours instance can be created.""" + self.assertIsNotNone(self.working_hours) + self.assertEqual(self.working_hours.staff_member, self.staff_member) + self.assertEqual(self.working_hours.get_start_time(), time(9, 0)) + self.assertEqual(self.working_hours.get_end_time(), time(17, 0)) + + def test_working_hours_str_method(self): + """Test that the string representation of a WorkingHours instance is correct.""" + self.assertEqual(str(self.working_hours), "Monday - 09:00:00 to 17:00:00") + + def test_get_day_of_week_str(self): + """Test that the get_day_of_week_str method in WorkingHours model works as expected.""" + self.assertEqual(self.working_hours.get_day_of_week_str(), _("Monday")) + + +class WorkingHoursValidationTestCase(TestCase, UserMixin, ServiceMixin, StaffMemberMixin): + def setUp(self): + self.user = self.create_user_() + self.service = self.create_service_() + self.staff_member = self.create_staff_member_(self.user, self.service) + self.working_hours = WorkingHours.objects.create( + staff_member=self.staff_member, + day_of_week=1, + start_time=time(9, 0), + end_time=time(17, 0) + ) + + def test_working_hours_start_time_before_end_time(self): + """A WorkingHours instance cannot be created if start_time is after end_time.""" + with self.assertRaises(ValidationError): + WorkingHours.objects.create( + staff_member=self.staff_member, + day_of_week=2, + start_time=time(17, 0), + end_time=time(9, 0) + ).clean() + + def test_working_hours_without_staff_member(self): + """A WorkingHours instance cannot be created without a staff member.""" + with self.assertRaises(IntegrityError): + WorkingHours.objects.create( + day_of_week=3, + start_time=time(9, 0), + end_time=time(17, 0) + ) + + def test_working_hours_is_owner(self): + """Test that is_owner method in WorkingHours model works as expected.""" + self.assertTrue(self.working_hours.is_owner(self.user.id)) + self.assertFalse(self.working_hours.is_owner(9999)) # Assuming 9999 is not a valid user ID in your tests + + def test_staff_member_weekend_status_update(self): + """Test that the staff member's weekend status is updated when a WorkingHours instance is created.""" + WorkingHours.objects.create( + staff_member=self.staff_member, + day_of_week=6, # Saturday + start_time=time(9, 0), + end_time=time(12, 0) + ) + self.staff_member.refresh_from_db() + self.assertTrue(self.staff_member.work_on_saturday) + + WorkingHours.objects.create( + staff_member=self.staff_member, + day_of_week=0, # Sunday + start_time=time(9, 0), + end_time=time(12, 0) + ) + self.staff_member.refresh_from_db() + self.assertTrue(self.staff_member.work_on_sunday) + + def test_working_hours_duplicate_day(self): + """A WorkingHours instance cannot be created if the staff member already has a working hours on that day.""" + with self.assertRaises(IntegrityError): + WorkingHours.objects.create( + staff_member=self.staff_member, + day_of_week=1, # Same day as the working_hours created in setUp + start_time=time(9, 0), + end_time=time(17, 0) + ) diff --git a/appointment/tests/test_services.py b/appointment/tests/test_services.py new file mode 100644 index 0000000..14904d3 --- /dev/null +++ b/appointment/tests/test_services.py @@ -0,0 +1,904 @@ +# test_services.py +# Path: appointment/tests/test_services.py + +import datetime +import json +from _decimal import Decimal +from datetime import date, time, timedelta +from unittest.mock import patch + +from django.core.cache import cache +from django.test import Client, override_settings +from django.test.client import RequestFactory +from django.utils import timezone +from django.utils.translation import gettext as _, gettext_lazy as _ + +from appointment.forms import StaffDaysOffForm +from appointment.services import ( + create_staff_member_service, email_change_verification_service, fetch_user_appointments, get_available_slots, + get_available_slots_for_staff, get_finish_button_text, handle_day_off_form, handle_entity_management_request, + handle_service_management_request, handle_working_hours_form, prepare_appointment_display_data, + prepare_user_profile_data, save_appointment, save_appt_date_time, update_personal_info_service +) +from appointment.tests.base.base_test import BaseTest +from appointment.tests.mixins.base_mixin import ( + ConfigMixin) +from appointment.utils.date_time import convert_str_to_time, get_ar_end_time +from appointment.utils.db_helpers import Config, DayOff, EmailVerificationCode, StaffMember, WorkingHours +from appointment.views import get_appointments_and_slots + + +class GetAvailableSlotsTests(BaseTest): + """Test cases for get_available_slots""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + self.tomorrow = timezone.now().date() + datetime.timedelta(days=1) + ar = self.create_appt_request_for_sm1(date_=self.tomorrow) + self.appointment = self.create_appt_for_sm1(appointment_request=ar) + + def test_get_available_slots(self): + slots = get_available_slots(self.tomorrow, [self.appointment]) + self.assertIsInstance(slots, list) + self.assertNotIn('09:00 AM', slots) + + def test_get_available_slots_with_config(self): + Config.objects.create( + lead_time=datetime.time(8, 0), + finish_time=datetime.time(17, 0), + slot_duration=30, + appointment_buffer_time=2.0 + ) + slots = get_available_slots(self.tomorrow, [self.appointment]) + self.assertIsInstance(slots, list) + self.assertNotIn('09:00 AM', slots) + + +class FetchUserAppointmentsTests(BaseTest): + """Test suite for the `fetch_user_appointments` service function.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + # Create some appointments for testing purposes + self.appointment_for_user1 = self.create_appt_for_sm1() + self.appointment_for_user2 = self.create_appt_for_sm2() + + def test_fetch_appointments_for_superuser(self): + """Test that a superuser can fetch all appointments.""" + # Make user1 a superuser + jack = self.users['superuser'] + jack.is_superuser = True + jack.save() + + # Fetch appointments for superuser + appointments = fetch_user_appointments(jack) + + # Assert that the superuser sees all appointments + self.assertIn(self.appointment_for_user1, appointments, + "Superuser should be able to see all appointments, including those created for user1.") + self.assertIn(self.appointment_for_user2, appointments, + "Superuser should be able to see all appointments, including those created for user2.") + + def test_fetch_appointments_for_staff_member(self): + """Test that a staff member can only fetch their own appointments.""" + # Fetch appointments for staff member (user1 in this case) + daniel = self.users['staff1'] + daniel.is_staff = True + daniel.save() + + appointments = fetch_user_appointments(daniel) + + # Assert that the staff member sees only their own appointments + self.assertIn(self.appointment_for_user1, appointments, + "Staff members should only see appointments linked to them. User1's appointment is missing.") + self.assertNotIn(self.appointment_for_user2, appointments, + "Staff members should not see appointments not linked to them. User2's appointment was found.") + + def test_fetch_appointments_for_regular_user(self): + """Test that a regular user (not a user with staff member instance or staff) cannot fetch appointments.""" + # Fetching appointments for a regular user (client1 in this case) should raise ValueError + georges = self.users['client1'] + with self.assertRaises(ValueError, + msg="Regular users without staff or superuser status should raise a ValueError."): + fetch_user_appointments(georges) + + def test_fetch_appointments_for_staff_user_without_staff_member_instance(self): + """Test that a staff user without a staff member instance gets an empty list of appointments.""" + janet = self.create_user_() + janet.is_staff = True + janet.save() + + appointments = fetch_user_appointments(janet) + # Check that the returned value is an empty list + self.assertEqual(appointments, [], "Expected an empty list for a staff user without a staff member instance.") + + +class PrepareAppointmentDisplayDataTests(BaseTest): + """Test suite for the `prepare_appointment_display_data` service function.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + + # Create an appointment for testing purposes + self.appointment = self.create_appt_for_sm1() + self.daniel = self.users['staff1'] + self.samantha = self.users['staff2'] + self.georges = self.users['client1'] + + def test_non_existent_appointment(self): + """Test that the function handles a non-existent appointment correctly.""" + # Fetch data for a non-existent appointment + x, y, error_message, status_code = prepare_appointment_display_data(self.samantha, 9999) + + self.assertEqual(status_code, 404, "Expected status code to be 404 for a non-existent appointment.") + self.assertEqual(error_message, _("Appointment does not exist.")) + + def test_unauthorized_user(self): + """A user who doesn't own the appointment cannot view it.""" + # Fetch data for an appointment that user2 doesn't own + x, y, error_message, status_code = prepare_appointment_display_data(self.georges, self.appointment.id) + + self.assertEqual(status_code, 403, "Expected status code to be 403 for an unauthorized user.") + self.assertEqual(error_message, _("You are not authorized to view this appointment.")) + + def test_authorized_user(self): + """An authorized user can view the appointment.""" + # Fetch data for the appointment owned by user1 + appointment, page_title, error_message, status_code = prepare_appointment_display_data(self.daniel, + self.appointment.id) + + self.assertEqual(status_code, 200, "Expected status code to be 200 for an authorized user.") + self.assertIsNone(error_message) + self.assertEqual(appointment, self.appointment) + self.assertTrue(self.georges.first_name in page_title) + + def test_superuser(self): + """A superuser can view any appointment and sees the staff member name in the title.""" + + jack = self.users['superuser'] + jack.is_superuser = True + jack.save() + + # Fetch data for the appointment as a superuser + appointment, page_title, error_message, status_code = prepare_appointment_display_data(jack, + self.appointment.id) + + self.assertEqual(status_code, 200, "Expected status code to be 200 for a superuser.") + self.assertIsNone(error_message) + self.assertEqual(appointment, self.appointment) + self.assertTrue(self.georges.first_name in page_title) + self.assertTrue(self.daniel.first_name in page_title) + + +class PrepareUserProfileDataTests(BaseTest): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.jack = self.users['superuser'] + self.jack.is_superuser = True + self.jack.save() + + def test_superuser_without_staff_user_id(self): + """A superuser without a staff_user_id should see the staff list page.""" + data = prepare_user_profile_data(self.jack, None) + self.assertFalse(data['error']) + self.assertEqual(data['template'], 'administration/staff_list.html') + self.assertIn('btn_staff_me', data['extra_context']) + + def test_regular_user_with_mismatched_staff_user_id(self): + """A regular user cannot view another user's profile.""" + data = prepare_user_profile_data(self.jack, self.users['client2'].pk) + self.assertTrue(data['error']) + self.assertEqual(data['status_code'], 403) + + def test_superuser_with_non_existent_staff_user_id(self): + """A superuser with a non-existent staff_user_id cannot view the staff's profile.""" + data = prepare_user_profile_data(self.jack, 9999) + self.assertTrue(data['error']) + self.assertEqual(data['status_code'], 403) + + def test_regular_user_with_matching_staff_user_id(self): + """A regular user can view their own profile.""" + data = prepare_user_profile_data(self.users['staff1'], self.staff_member1.pk) + self.assertFalse(data['error']) + self.assertEqual(data['template'], 'administration/user_profile.html') + self.assertIn('user', data['extra_context']) + self.assertEqual(data['extra_context']['user'], self.users['staff1']) + + def test_regular_user_with_non_existent_staff_user_id(self): + """A regular user with a non-existent staff_user_id cannot view their profile.""" + data = prepare_user_profile_data(self.jack, 9999) + self.assertTrue(data['error']) + self.assertEqual(data['status_code'], 403) + + +class HandleEntityManagementRequestTests(BaseTest): + + def setUp(self): + super().setUp() + self.client = Client() + self.factory = RequestFactory() + + # Setup request object + self.request = self.factory.post('/') + self.request.user = self.staff_member1.user + + def test_staff_member_none(self): + """A day off cannot be created for a staff member that doesn't exist.""" + response = handle_entity_management_request(self.request, None, 'day_off') + self.assertEqual(response.status_code, 403) + + def test_day_off_get(self): + """Test if a day off can be fetched.""" + self.request.method = 'GET' + response = handle_entity_management_request(self.request, self.staff_member1, 'day_off') + self.assertEqual(response.status_code, 200) + + def test_working_hours_get(self): + """Test if working hours can be fetched.""" + self.request.method = 'GET' + response = handle_entity_management_request(request=self.request, staff_member=self.staff_member1, + entity_type='working_hours', staff_user_id=self.staff_member1.id) + self.assertEqual(response.status_code, 200) + + def test_day_off_post_conflicting_dates(self): + """A day off cannot be created if the staff member already has a day off on the same dates.""" + DayOff.objects.create(staff_member=self.staff_member1, start_date='2022-01-01', end_date='2022-01-07') + self.request.method = 'POST' + self.request.POST = { + 'start_date': '2022-01-01', + 'end_date': '2022-01-07' + } + response = handle_entity_management_request(self.request, self.staff_member1, 'day_off') + self.assertEqual(response.status_code, 400) + + def test_day_off_post_non_conflicting_dates(self): + """A day off can be created if the staff member doesn't have a day off on the same dates.""" + self.request.method = 'POST' + self.request.POST = { + 'start_date': '2022-01-08', + 'end_date': '2022-01-14' + } + response = handle_entity_management_request(self.request, self.staff_member1, 'day_off') + content = json.loads(response.content) + self.assertEqual(content['success'], True) + + def test_working_hours_post(self): + """Test if working hours can be created with valid data.""" + # Assuming handle_working_hours_form always returns a JsonResponse + self.request.method = 'POST' + self.request.POST = { + 'day_of_week': '2', + 'start_time': '08:00 AM', + 'end_time': '12:00 PM' + } + # Create a WorkingHours instance for self.staff_member1 + working_hours_instance = WorkingHours.objects.create(staff_member=self.staff_member1, day_of_week=1, + start_time=datetime.time(8, 0), + end_time=datetime.time(12, 0)) + + # Now, pass this instance to your function + response = handle_entity_management_request(request=self.request, staff_member=self.staff_member1, + entity_type='working_hours', + staff_user_id=self.staff_member1.user.id, + instance=working_hours_instance) + content = json.loads(response.content) + self.assertTrue(content['success']) + + +class HandleWorkingHoursFormTest(BaseTest): + + def setUp(self): + super().setUp() + + def test_add_working_hours(self): + """Test if working hours can be added.""" + response = handle_working_hours_form(self.staff_member1, 1, '09:00 AM', '05:00 PM', True) + self.assertEqual(response.status_code, 200) + + def test_update_working_hours(self): + """Test if working hours can be updated.""" + wh = WorkingHours.objects.create(staff_member=self.staff_member1, day_of_week=2, start_time='09:00', + end_time='17:00') + response = handle_working_hours_form(self.staff_member1, 3, '10:00 AM', '06:00 PM', False, wh_id=wh.id) + self.assertEqual(response.status_code, 200) + + def test_invalid_data(self): + """If the form is invalid, the function should return a JsonResponse with the appropriate error message.""" + response = handle_working_hours_form(None, 1, '09:00 AM', '05:00 PM', True) # Missing staff_member + self.assertEqual(response.status_code, 400) + self.assertFalse(json.loads(response.getvalue())['success']) + + def test_invalid_time(self): + """If the start time is after the end time, the function should return a JsonResponse with the + appropriate error""" + response = handle_working_hours_form(self.staff_member1, 1, '05:00 PM', '09:00 AM', True) + self.assertEqual(response.status_code, 400) + content = json.loads(response.getvalue()) + self.assertEqual(content['errorCode'], 5) + self.assertFalse(content['success']) + + def test_working_hours_conflict(self): + """A staff member cannot have two working hours on the same day.""" + WorkingHours.objects.create(staff_member=self.staff_member1, day_of_week=4, start_time='09:00', + end_time='17:00') + response = handle_working_hours_form(self.staff_member1, 4, '10:00 AM', '06:00 PM', True) + self.assertEqual(response.status_code, 400) + content = json.loads(response.getvalue()) + self.assertEqual(content['errorCode'], 11) + self.assertFalse(content['success']) + + def test_invalid_working_hours_id(self): + """If the working hours ID is invalid, the function should return a JsonResponse with the appropriate error""" + response = handle_working_hours_form(self.staff_member1, 1, '10:00 AM', '06:00 PM', False, wh_id=9999) + self.assertEqual(response.status_code, 400) + content = json.loads(response.getvalue()) + self.assertEqual(content['success'], False) + self.assertEqual(content['errorCode'], 10) + + def test_no_working_hours_id(self): + """If the working hours ID is not provided, the function should return a JsonResponse with the + appropriate error""" + response = handle_working_hours_form(self.staff_member1, 1, '10:00 AM', '06:00 PM', False) + self.assertEqual(response.status_code, 400) + content = json.loads(response.getvalue()) + self.assertEqual(content['success'], False) + self.assertEqual(content['errorCode'], 5) + + +class HandleDayOffFormTest(BaseTest): + + def setUp(self): + super().setUp() + + def test_valid_day_off_form(self): + """Test if a valid day off form is handled correctly.""" + data = { + 'start_date': '2023-01-01', + 'end_date': '2023-01-05' + } + day_off_form = StaffDaysOffForm(data) + response = handle_day_off_form(day_off_form, self.staff_member1) + self.assertEqual(response.status_code, 200) + content = json.loads(response.content) + self.assertTrue(content['success']) + + def test_invalid_day_off_form(self): + """A day off form with invalid data should return a JsonResponse with the appropriate error message.""" + data = { + 'start_date': '2023-01-01', + 'end_date': '' # Missing end_date + } + day_off_form = StaffDaysOffForm(data) + response = handle_day_off_form(day_off_form, self.staff_member1) + self.assertEqual(response.status_code, 400) + content = json.loads(response.content) + self.assertFalse(content['success']) + + +class SaveAppointmentTests(BaseTest): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + + # Assuming self.create_default_appointment creates an appointment with default values + self.appt = self.create_appt_for_sm1() + self.factory = RequestFactory() + self.request = self.factory.get('/') + + def test_save_appointment(self): + """Test if an appointment can be saved with valid data.""" + client_name = "Teal'c of Chulak" + client_email = "tealc@chulak.com" + start_time_str = "10:00 AM" + phone_number = "+1234567890" + client_address = "123 Stargate Command, Cheyenne Mountain" + service_id = self.service2.id + staff_member_id = self.staff_member2.id + + # Call the function + updated_appt = save_appointment(self.appt, client_name, client_email, start_time_str, phone_number, + client_address, service_id, self.request, staff_member_id) + + # Check client details + self.assertEqual(updated_appt.client.get_full_name(), client_name) + self.assertEqual(updated_appt.client.email, client_email) + + # Check appointment request details + self.assertEqual(updated_appt.appointment_request.service.id, service_id) + self.assertEqual(updated_appt.appointment_request.start_time, convert_str_to_time(start_time_str)) + end_time = get_ar_end_time(convert_str_to_time(start_time_str), self.service2.duration) + self.assertEqual(updated_appt.appointment_request.end_time, end_time) + + # Check appointment details + self.assertEqual(updated_appt.phone, phone_number) + self.assertEqual(updated_appt.address, client_address) + + +class SaveApptDateTimeTests(BaseTest): + + def setUp(self): + super().setUp() + + # Assuming create_appt_for_sm1 creates an appointment for user1 with default values + self.appt = self.create_appt_for_sm1() + self.factory = RequestFactory() + self.request = self.factory.get('/') + + def test_save_appt_date_time(self): + """Test if an appointment's date and time can be updated.""" + # Given new appointment date and time details + appt_start_time_str = "10:00:00.000000Z" + appt_date_str = (datetime.datetime.today() + datetime.timedelta(days=7)).strftime("%Y-%m-%d") + appt_id = self.appt.id + + # Call the function + updated_appt = save_appt_date_time(appt_start_time_str, appt_date_str, appt_id, self.request) + + # Convert given date and time strings to appropriate formats + time_format = "%H:%M:%S.%fZ" + appt_start_time_obj = datetime.datetime.strptime(appt_start_time_str, time_format).time() + appt_date_obj = datetime.datetime.strptime(appt_date_str, "%Y-%m-%d").date() + + # Calculate the expected end time + service = updated_appt.get_service() + end_time_obj = get_ar_end_time(appt_start_time_obj, service.duration) + + # Validate the updated appointment details + self.assertEqual(updated_appt.appointment_request.date, appt_date_obj) + self.assertEqual(updated_appt.appointment_request.start_time, appt_start_time_obj) + self.assertEqual(updated_appt.appointment_request.end_time, end_time_obj) + + +def get_next_weekday(d, weekday): + """ + Get the date of the next weekday from the given date. + This function uses python's weekday format, where Monday is 0, and Sunday is 6. + Remember that in my implementation for work days, I had to use a custom one where Monday is 1, and Sunday is 0. + So in the setup, I will use my format to create day-offs, working hours, etc. But when calling this function, I will + use the python format. + """ + days_ahead = weekday - d.weekday() + if days_ahead <= 0: # Target day already happened this week + days_ahead += 7 + next_day = d + datetime.timedelta(days_ahead) + return next_day + + +class GetAvailableSlotsForStaffTests(BaseTest): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + cache.clear() + self.today = datetime.date.today() + # Staff member1 works only on Mondays and Wednesday (day_of_week: 1, 3) + self.wh1 = WorkingHours.objects.create(staff_member=self.staff_member1, day_of_week=1, + start_time=datetime.time(9, 0), end_time=datetime.time(17, 0)) + self.wh2 = WorkingHours.objects.create(staff_member=self.staff_member1, day_of_week=3, + start_time=datetime.time(9, 0), end_time=datetime.time(17, 0)) + # But decides to take a day off next Monday + self.next_monday = get_next_weekday(self.today, 0) + self.next_tuesday = get_next_weekday(self.today, 1) + self.next_wednesday = get_next_weekday(self.today, 2) + self.next_thursday = get_next_weekday(self.today, 3) + self.next_friday = get_next_weekday(self.today, 4) + self.next_saturday = get_next_weekday(self.today, 5) + self.next_sunday = get_next_weekday(self.today, 6) + DayOff.objects.create(staff_member=self.staff_member1, start_date=self.next_monday, end_date=self.next_monday) + Config.objects.create(slot_duration=60, lead_time=datetime.time(9, 0), finish_time=datetime.time(17, 0), + appointment_buffer_time=0) + + def test_day_off(self): + """Test if a day off is handled correctly when getting available slots.""" + # Ask for slots for it, and it should return an empty list since next Monday is a day off + slots = get_available_slots_for_staff(self.next_monday, self.staff_member1) + self.assertEqual(slots, []) + + def test_staff_does_not_work(self): + """Test if a staff member who doesn't work on a given day is handled correctly when getting available slots.""" + # For next week, the staff member works only on Monday and Wednesday, but puts a day off on Monday + # So the staff member should not have any available slots except for Wednesday, which is day #2 (python weekday) + slots = get_available_slots_for_staff(self.next_monday, self.staff_member1) + self.assertEqual(slots, []) + slots = get_available_slots_for_staff(self.next_tuesday, self.staff_member1) + self.assertEqual(slots, []) + slots = get_available_slots_for_staff(self.next_thursday, self.staff_member1) + self.assertEqual(slots, []) + slots = get_available_slots_for_staff(self.next_friday, self.staff_member1) + self.assertEqual(slots, []) + slots = get_available_slots_for_staff(self.next_saturday, self.staff_member1) + self.assertEqual(slots, []) + slots = get_available_slots_for_staff(self.next_sunday, self.staff_member1) + self.assertEqual(slots, []) + + def test_available_slots(self): + """Test if available slots are returned correctly.""" + # On a Wednesday, the staff member should have slots from 9 AM to 5 PM + slots = get_available_slots_for_staff(self.next_wednesday, self.staff_member1) + expected_slots = [ + datetime.datetime(self.next_wednesday.year, self.next_wednesday.month, self.next_wednesday.day, hour) for + hour in range(9, 17)] + self.assertEqual(slots, expected_slots) + + def test_booked_slots(self): + """On a given day, if a staff member has an appointment, that time slot should not be available.""" + # Let's book a slot for the staff member on next Wednesday + start_time = datetime.time(10, 0) + end_time = datetime.time(11, 0) + + # Create an appointment request for that time + appt_request = self.create_appointment_request_(service=self.service1, staff_member=self.staff_member1, + date_=self.next_wednesday, start_time=start_time, + end_time=end_time) + # Create an appointment using that request + self.create_appointment_(user=self.users['client1'], appointment_request=appt_request) + + # Now, the staff member should not have that slot available + slots = get_available_slots_for_staff(self.next_wednesday, self.staff_member1) + expected_slots = [ + datetime.datetime(self.next_wednesday.year, self.next_wednesday.month, self.next_wednesday.day, hour, 0) for + hour in range(9, 17) if hour != 10] + self.assertEqual(slots, expected_slots) + + def test_no_working_hours(self): + """If a staff member doesn't have working hours on a given day, no slots should be available.""" + # Let's ask for slots on a Thursday, which the staff member doesn't work + # Let's remove the config object also since it may contain default working days + Config.objects.all().delete() + # Now no slots should be available + slots = get_available_slots_for_staff(self.next_thursday, self.staff_member1) + self.assertEqual(slots, []) + + +class UpdatePersonalInfoServiceTest(BaseTest): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.daniel = self.users['staff1'] + self.post_data_valid = { + 'first_name': 'UpdatedName', + 'last_name': 'UpdatedLastName', + 'email': self.daniel.email + } + + def test_update_name(self): + """Test if the user's name can be updated.""" + user, is_valid, error_message = update_personal_info_service(self.staff_member1.user.id, self.post_data_valid, + self.daniel) + self.assertTrue(is_valid) + self.assertIsNone(error_message) + self.assertEqual(user.first_name, 'UpdatedName') + self.assertEqual(user.last_name, 'UpdatedLastName') + + def test_update_invalid_user_id(self): + """Updating a user that doesn't exist should return an error message.""" + user, is_valid, error_message = update_personal_info_service(9999, self.post_data_valid, + self.daniel) # Assuming 9999 is an invalid user ID + + self.assertFalse(is_valid) + self.assertEqual(error_message, _("User not found.")) + self.assertIsNone(user) + + def test_invalid_form(self): + """Updating a user with invalid form data should return an error message.""" + user, is_valid, error_message = update_personal_info_service(self.staff_member1.user.id, {}, self.daniel) + self.assertFalse(is_valid) + self.assertEqual(error_message, _("Empty fields are not allowed.")) + + def test_invalid_form_(self): + """Updating a user with invalid form data should return an error message.""" + # remove email in post_data + del self.post_data_valid['email'] + user, is_valid, error_message = update_personal_info_service(self.staff_member1.user.id, self.post_data_valid, + self.daniel) + self.assertFalse(is_valid) + self.assertEqual(error_message, "email: This field is required.") + + +class EmailChangeVerificationServiceTest(BaseTest): + + def setUp(self): + super().setUp() + self.georges = self.users['client1'] + self.valid_code = EmailVerificationCode.generate_code(self.georges) + self.invalid_code = "INVALID_CODE456" + + self.old_email = self.georges.email + self.new_email = "georges.hammond@django-appointment.com" + + def test_valid_code_and_email(self): + """Test if a valid code and email can be verified.""" + is_verified = email_change_verification_service(self.valid_code, self.new_email, self.old_email) + + self.assertTrue(is_verified) + self.georges.refresh_from_db() # Refresh the user object to get the updated email + self.assertEqual(self.georges.email, self.new_email) + + def test_invalid_code(self): + """If the code is invalid, the email should not be updated.""" + is_verified = email_change_verification_service(self.invalid_code, self.new_email, self.old_email) + + self.assertFalse(is_verified) + self.georges.refresh_from_db() + self.assertEqual(self.georges.email, self.old_email) # Email should not change + + def test_valid_code_no_user(self): + """If the code is valid but the user doesn't exist, the email should not be updated.""" + is_verified = email_change_verification_service(self.valid_code, self.new_email, "nonexistent@gmail.com") + + self.assertFalse(is_verified) + + def test_code_doesnt_match_users_code(self): + """If the code is valid but doesn't match the user's code, the email should not be updated.""" + # Using valid code but for another user + is_verified = email_change_verification_service(self.valid_code, self.new_email, + "g.hammond@django-appointment.com") + + self.assertFalse(is_verified) + + +class CreateStaffMemberServiceTest(BaseTest): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.factory = RequestFactory() + + # Setup request object + self.request = self.factory.post('/') + + def test_valid_data(self): + """Test if a staff member can be created with valid data.""" + post_data = { + 'first_name': 'Catherine', + 'last_name': 'Langford', + 'email': 'catherine.langford@django-appointment.com' + } + + user, success, error_message = create_staff_member_service(post_data, self.request) + + self.assertTrue(success) + self.assertIsNotNone(user) + self.assertEqual(user.first_name, 'Catherine') + self.assertEqual(user.last_name, 'Langford') + self.assertEqual(user.email, 'catherine.langford@django-appointment.com') + self.assertTrue(StaffMember.objects.filter(user=user).exists()) + + def test_invalid_data(self): + """Empty fields should not be allowed when creating a staff member.""" + post_data = { + 'first_name': '', # Missing first name + 'last_name': 'Langford', + 'email': 'catherine.langford@django-appointment.com' + } + user, success, error_message = create_staff_member_service(post_data, self.request) + + self.assertFalse(success) + self.assertIsNone(user) + self.assertIsNotNone(error_message) + + def test_email_already_exists(self): + """If the email already exists, the staff member should not be created.""" + self.create_user_() + post_data = { + 'first_name': 'Janet', + 'last_name': 'Fraiser', + 'email': 'janet.fraiser@django-appointment.com' # Using an email that already exists + } + user, success, error_message = create_staff_member_service(post_data, self.request) + + self.assertFalse(success) + self.assertIsNone(user) + self.assertEqual(error_message, "email: This email is already taken.") + + @patch('appointment.services.send_reset_link_to_staff_member') + def test_send_reset_link_to_new_staff_member(self, mock_send_reset_link): + """Test if a reset password link is sent to a new staff member.""" + post_data = { + 'first_name': 'Janet', + 'last_name': 'Fraiser', + 'email': 'janet.fraiser@django-appointment.com' + } + user, success, _ = create_staff_member_service(post_data, self.request) + self.assertTrue(success) + self.assertIsNotNone(user) + + # Check that the mock_send_reset_link function was called once + mock_send_reset_link.assert_called_once_with(user, self.request, user.email) + + +class HandleServiceManagementRequestTest(BaseTest): + + def setUp(self): + super().setUp() + + def test_create_new_service(self): + """Test if a new service can be created with valid data.""" + post_data = { + 'name': "Goa'uld extraction", + 'duration': '1:00:00', + 'price': '10000', + 'currency': 'USD', + 'down_payment': '5000', + } + service, success, message = handle_service_management_request(post_data) + self.assertTrue(success) + self.assertIsNotNone(service) + self.assertEqual(service.name, "Goa'uld extraction") + self.assertEqual(service.duration, datetime.timedelta(hours=1)) + self.assertEqual(service.price, Decimal('10000')) + self.assertEqual(service.down_payment, Decimal('5000')) + self.assertEqual(service.currency, 'USD') + + def test_update_existing_service(self): + """Test if an existing service can be updated with valid data.""" + existing_service = self.create_service_() + post_data = { + 'name': 'Quantum Mirror Repair', + 'duration': '2:00:00', + 'price': '15000', + 'down_payment': '7500', + 'currency': 'EUR' + } + service, success, message = handle_service_management_request(post_data, service_id=existing_service.id) + + self.assertTrue(success) + self.assertIsNotNone(service) + self.assertEqual(service.name, 'Quantum Mirror Repair') + self.assertEqual(service.duration, datetime.timedelta(hours=2)) + self.assertEqual(service.price, Decimal('15000')) + self.assertEqual(service.currency, 'EUR') + + def test_invalid_data(self): + """Empty fields should not be allowed when creating a service.""" + post_data = { + 'name': '', # Missing name + 'duration': '1:00:00', + 'price': '100', + 'currency': 'USD', + 'down_payment': '50', + } + service, success, message = handle_service_management_request(post_data, service_id=self.service1.id) + + self.assertFalse(success) + self.assertIsNone(service) + self.assertEqual(message, "name: This field is required.") + + def test_service_not_found(self): + """If the service ID is invalid, the service should not be updated.""" + post_data = { + 'name': 'DHD maintenance', + 'duration': '1:00:00', + 'price': '10000', + 'currency': 'USD' + } + service, success, message = handle_service_management_request(post_data, service_id=9999) # Invalid service_id + + self.assertFalse(success) + self.assertIsNone(service) + self.assertIn(str(_("Service matching query does not exist")), str(message)) + + +class GetFinishButtonTextTests(BaseTest): + """Test cases for get_finish_button_text""" + + def test_get_finish_button_text_free_service(self): + button_text = get_finish_button_text(self.service1) + self.assertEqual(button_text, _("Finish")) + + def test_get_finish_button_text_paid_service(self): + with patch('appointment.services.APPOINTMENT_PAYMENT_URL', 'https://payment.com'): + button_text = get_finish_button_text(self.service1) + self.assertEqual(button_text, _("Pay Now")) + + +class SlotAvailabilityTest(BaseTest, ConfigMixin): + + def setUp(self): + self.service = self.create_service_(duration=timedelta(hours=2)) + self.config = self.create_config_(lead_time=time(11, 0), finish_time=time(15, 0), slot_duration=120) + self.test_date = date.today() + timedelta(days=1) # Use tomorrow's date for the tests + + @override_settings(DEBUG=True) + def tearDown(self): + self.service.delete() + self.config.delete() + cache.clear() + + def test_slot_availability_without_appointments(self): + """Test if the available slots are correct when there are no appointments.""" + _, available_slots = get_appointments_and_slots(self.test_date, self.service) + expected_slots = ['11:00 AM', '01:00 PM'] + self.assertEqual(available_slots, expected_slots) + + def test_slot_availability_with_first_slot_booked(self): + """Available slots (total 2) should be one when the first slot is booked.""" + self.ar = self.create_appt_request_for_sm1(service=self.service, date_=self.test_date, start_time=time(11, 0), + end_time=time(13, 0)) + self.create_appt_for_sm1(appointment_request=self.ar) + _, available_slots = get_appointments_and_slots(self.test_date, self.service) + expected_slots = ['01:00 PM'] + self.assertEqual(available_slots, expected_slots) + + def test_slot_availability_with_second_slot_booked(self): + """Available slots (total 2) should be one when the second slot is booked.""" + self.ar = self.create_appt_request_for_sm1(service=self.service, date_=self.test_date, start_time=time(13, 0), + end_time=time(15, 0)) + self.create_appt_for_sm1(appointment_request=self.ar) + _, available_slots = get_appointments_and_slots(self.test_date, self.service) + expected_slots = ['11:00 AM'] + self.assertEqual(available_slots, expected_slots) + + def test_slot_availability_with_both_slots_booked(self): + """Available slots (total 2) should be zero when both slots are booked.""" + self.ar1 = self.create_appt_request_for_sm1(service=self.service, date_=self.test_date, start_time=time(11, 0), + end_time=time(13, 0)) + self.ar2 = self.create_appt_request_for_sm1(service=self.service, date_=self.test_date, start_time=time(13, 0), + end_time=time(15, 0)) + self.create_appt_for_sm1(appointment_request=self.ar1) + self.create_appt_for_sm1(appointment_request=self.ar2) + _, available_slots = get_appointments_and_slots(self.test_date, self.service) + expected_slots = [] + self.assertEqual(available_slots, expected_slots) diff --git a/appointment/tests/test_settings.py b/appointment/tests/test_settings.py new file mode 100644 index 0000000..5232c4f --- /dev/null +++ b/appointment/tests/test_settings.py @@ -0,0 +1,45 @@ +# test_settings.py +# Path: appointment/tests/test_settings.py + +from unittest.mock import patch + +from django.test import TestCase + +from appointment.settings import check_q_cluster + + +class CheckQClusterTest(TestCase): + @patch('appointment.settings.settings') + @patch('appointment.settings.logger') + def test_check_q_cluster_with_django_q_missing(self, mock_logger, mock_settings): + # Simulate 'django_q' not being in INSTALLED_APPS + mock_settings.INSTALLED_APPS = [] + + # Call the function under test + result = check_q_cluster() + + # Check the result + self.assertFalse(result) + # Verify logger was called with the expected warning about 'django_q' not being installed + mock_logger.warning.assert_called_with( + "Django Q is not in settings.INSTALLED_APPS. Please add it to the list.\n" + "Example: \n\n" + "INSTALLED_APPS = [\n" + " ...\n" + " 'appointment',\n" + " 'django_q',\n" + "]\n") + + @patch('appointment.settings.settings') + @patch('appointment.settings.logger') + def test_check_q_cluster_with_all_configurations_present(self, mock_logger, mock_settings): + # Simulate both 'django_q' being in INSTALLED_APPS and 'Q_CLUSTER' configuration present + mock_settings.INSTALLED_APPS = ['django_q'] + mock_settings.Q_CLUSTER = {'name': 'DjangORM'} + + # Call the function under test + result = check_q_cluster() + + # Check the result and ensure no warnings are logged + self.assertTrue(result) + mock_logger.warning.assert_not_called() diff --git a/appointment/tests/test_tasks.py b/appointment/tests/test_tasks.py new file mode 100644 index 0000000..f07cc2a --- /dev/null +++ b/appointment/tests/test_tasks.py @@ -0,0 +1,52 @@ +# test_tasks.py +# Path: appointment/tests/test_tasks.py + +from unittest.mock import patch + +from django.utils.translation import gettext as _ + +from appointment.tasks import send_email_reminder +from appointment.tests.base.base_test import BaseTest + + +class SendEmailReminderTest(BaseTest): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + @patch('appointment.tasks.send_email') + @patch('appointment.tasks.notify_admin') + def test_send_email_reminder(self, mock_notify_admin, mock_send_email): + # Use BaseTest setup to create an appointment + appointment_request = self.create_appt_request_for_sm1() + appointment = self.create_appt_for_sm1(appointment_request=appointment_request) + + # Extract necessary data for the test + to_email = appointment.client.email + first_name = appointment.client.first_name + appointment_id = appointment.id + + # Call the function under test + send_email_reminder(to_email, first_name, "", appointment_id) + + # Verify send_email was called with correct parameters + mock_send_email.assert_called_once_with( + recipient_list=[to_email], + subject=_("Reminder: Upcoming Appointment"), + template_url='email_sender/reminder_email.html', + context={'first_name': first_name, 'appointment': appointment, 'reschedule_link': "", + 'recipient_type': 'admin'} + ) + + # Verify notify_admin was called with correct parameters + mock_notify_admin.assert_called_once_with( + subject=_("Admin Reminder: Upcoming Appointment"), + template_url='email_sender/reminder_email.html', + context={'first_name': first_name, 'appointment': appointment, 'reschedule_link': "", + 'recipient_type': 'admin'} + ) diff --git a/appointment/tests/test_views.py b/appointment/tests/test_views.py new file mode 100644 index 0000000..6dc110a --- /dev/null +++ b/appointment/tests/test_views.py @@ -0,0 +1,1258 @@ +# test_views.py +# Path: appointment/tests/test_views.py + +import datetime +import json +import uuid +from datetime import date, time, timedelta +from unittest.mock import MagicMock, patch + +from django.contrib import messages +from django.contrib.messages import get_messages +from django.contrib.messages.middleware import MessageMiddleware +from django.contrib.sessions.middleware import SessionMiddleware +from django.http import HttpResponseRedirect +from django.test import Client +from django.test.client import RequestFactory +from django.urls import reverse +from django.utils import timezone +from django.utils.encoding import force_bytes +from django.utils.http import urlsafe_base64_encode +from django.utils.translation import gettext as _ + +from appointment.forms import StaffMemberForm +from appointment.messages_ import passwd_error +from appointment.models import ( + Appointment, AppointmentRequest, AppointmentRescheduleHistory, Config, DayOff, EmailVerificationCode, + PasswordResetToken, StaffMember +) +from appointment.tests.base.base_test import BaseTest +from appointment.utils.db_helpers import Service, WorkingHours, create_user_with_username +from appointment.utils.error_codes import ErrorCode +from appointment.views import ( + create_appointment, redirect_to_payment_or_thank_you_page, verify_user_and_login +) + + +class SlotTestCase(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.client = Client() + self.url = reverse('appointment:available_slots_ajax') + + def test_get_available_slots_ajax(self): + """get_available_slots_ajax view should return a JSON response with available slots for the selected date.""" + response = self.client.get(self.url, {'selected_date': date.today().isoformat(), 'staff_member': '1'}, + HTTP_X_REQUESTED_WITH='XMLHttpRequest') + print(f"response: {response.content}") + self.assertEqual(response.status_code, 200) + response_data = response.json() + self.assertIn('date_chosen', response_data) + self.assertIn('available_slots', response_data) + self.assertFalse(response_data.get('error')) + + def test_get_available_slots_ajax_past_date(self): + """get_available_slots_ajax view should return an error if the selected date is in the past.""" + past_date = (date.today() - timedelta(days=1)).isoformat() + response = self.client.get(self.url, {'selected_date': past_date}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()['error'], True) + self.assertEqual(response.json()['message'], 'Date is in the past') + + +class AppointmentRequestTestCase(BaseTest): + def setUp(self): + super().setUp() + self.url = reverse('appointment:appointment_request_submit') + + def test_appointment_request(self): + """Test if the appointment request form can be rendered.""" + url = reverse('appointment:appointment_request', args=[self.service1.id]) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertIn(self.service1.name, str(response.content)) + self.assertIn('all_staff_members', response.context) + self.assertIn('service', response.context) + + def test_appointment_request_submit_valid(self): + """Test if a valid appointment request can be submitted.""" + post_data = { + 'date': date.today().isoformat(), + 'start_time': time(9, 0), + 'end_time': time(10, 0), + 'service': self.service1.id, + 'staff_member': self.staff_member1.id, + } + response = self.client.post(self.url, post_data) + self.assertEqual(response.status_code, 302) # Redirect status + # Check if an AppointmentRequest object was created + self.assertTrue(AppointmentRequest.objects.filter(service=self.service1).exists()) + + def test_appointment_request_submit_invalid(self): + """Test if an invalid appointment request can be submitted.""" + post_data = {} # Missing required data + response = self.client.post(self.url, post_data) + self.assertEqual(response.status_code, 200) # Rendering the form with errors + self.assertIn('form', response.context) + self.assertTrue(response.context['form'].errors) # Ensure there are form errors + + +class VerificationCodeTestCase(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.daniel = self.users['staff1'] + self.factory = RequestFactory() + self.request = self.factory.get('/') + + # Simulate session middleware + middleware = SessionMiddleware(lambda req: None) + middleware.process_request(self.request) + self.request.session.save() + + # Attach message storage + middleware = MessageMiddleware(lambda req: None) + middleware.process_request(self.request) + self.request.session.save() + + self.ar = self.create_appt_request_for_sm1() + self.url = reverse('appointment:enter_verification_code', args=[self.ar.id, self.ar.id_request]) + + def test_verify_user_and_login_valid(self): + """Test if a user can be verified and logged in.""" + code = EmailVerificationCode.generate_code(user=self.daniel) + result = verify_user_and_login(self.request, self.daniel, code) + self.assertTrue(result) + + def test_verify_user_and_login_invalid(self): + """Test if a user cannot be verified and logged in with an invalid code.""" + invalid_code = '000000' # An invalid code + result = verify_user_and_login(self.request, self.daniel, invalid_code) + self.assertFalse(result) + + def test_enter_verification_code_valid(self): + """Test if a valid verification code can be entered.""" + code = EmailVerificationCode.generate_code(user=self.daniel) + post_data = {'code': code} # Assuming a valid code for the test setup + response = self.client.post(self.url, post_data) + self.assertEqual(response.status_code, 200) + + def test_enter_verification_code_invalid(self): + """Test if an invalid verification code can be entered.""" + post_data = {'code': '000000'} # Invalid code + response = self.client.post(self.url, post_data) + self.assertEqual(response.status_code, 200) # Stay on the same page + # Check for an error message + messages_list = list(messages.get_messages(response.wsgi_request)) + self.assertIn(_("Invalid verification code."), [str(msg) for msg in messages_list]) + + +class StaffMemberTestCase(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.user1 = self.users['staff1'] + self.staff_member = self.staff_member1 + self.appointment = self.create_appt_for_sm1() + + def remove_staff_member(self): + """Remove the StaffMember instance of self.user1.""" + self.clean_staff_member_objects() + StaffMember.objects.filter(user=self.user1).delete() + + def test_staff_user_without_staff_member_instance(self): + """Test that a staff user without a staff member instance receives an appropriate error message.""" + self.clean_staff_member_objects() + + # Now safely delete the StaffMember instance + StaffMember.objects.filter(user=self.user1).delete() + + self.user1.save() # Save the user to the database after updating + self.need_staff_login() + + url = reverse('appointment:get_user_appointments') + response = self.client.get(url) + + message_list = list(get_messages(response.wsgi_request)) + self.assertTrue(any( + message.message == "User doesn't have a staff member instance. Please contact the administrator." for + message in message_list), + "Expected error message not found in messages.") + + def test_remove_staff_member(self): + self.need_superuser_login() + self.clean_staff_member_objects() + + url = reverse('appointment:remove_staff_member', args=[self.staff_member.user_id]) + response = self.client.get(url) + + self.assertEqual(response.status_code, 302) # Redirect status code + self.assertRedirects(response, reverse('appointment:user_profile')) + + # Check for success messages + messages_list = list(get_messages(response.wsgi_request)) + self.assertTrue(any(_("Staff member deleted successfully!") in str(message) for message in messages_list)) + + # Check if staff member is deleted + staff_member_exists = StaffMember.objects.filter(pk=self.staff_member.id).exists() + self.assertFalse(staff_member_exists, "Appointment should be deleted but still exists.") + + def test_remove_staff_member_with_superuser(self): + self.need_superuser_login() + self.clean_staff_member_objects() + # Test removal of staff member by a superuser + self.jack = self.users['superuser'] + + self.client.get(reverse('appointment:make_superuser_staff_member')) + response = self.client.get(reverse('appointment:remove_superuser_staff_member')) + + # Check if the StaffMember instance was deleted + self.assertFalse(StaffMember.objects.filter(user=self.jack).exists()) + + # Check if it redirects to the user profile + self.assertRedirects(response, reverse('appointment:user_profile')) + + def test_remove_staff_member_without_superuser(self): + # Log out superuser and log in as a regular user + self.need_staff_login() + response = self.client.get(reverse('appointment:remove_superuser_staff_member')) + + # Check for a forbidden status code, as only superusers should be able to remove staff members + self.assertEqual(response.status_code, 403) + + def test_make_staff_member_with_superuser(self): + self.need_superuser_login() + self.remove_staff_member() + self.jack = self.users['superuser'] + # Test creating a staff member by a superuser + response = self.client.get(reverse('appointment:make_superuser_staff_member')) + + # Check if the StaffMember instance was created + self.assertTrue(StaffMember.objects.filter(user=self.jack).exists()) + + # Check if it redirects to the user profile + self.assertRedirects(response, reverse('appointment:user_profile')) + + def test_make_staff_member_without_superuser(self): + self.need_staff_login() + response = self.client.get(reverse('appointment:make_superuser_staff_member')) + + # Check for a forbidden status code, as only superusers should be able to create staff members + self.assertEqual(response.status_code, 403) + + def test_is_user_staff_admin_with_staff_member(self): + """Test that a user with a StaffMember instance is identified as a staff member.""" + self.need_staff_login() + + # Ensure the user has a StaffMember instance + if not StaffMember.objects.filter(user=self.user1).exists(): + StaffMember.objects.create(user=self.user1) + + url = reverse('appointment:is_user_staff_admin') + response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + + # Check the response status code and content + self.assertEqual(response.status_code, 200) + response_data = response.json() + self.assertIn('message', response_data) + self.assertEqual(response_data['message'], _("User is a staff member.")) + + def test_is_user_staff_admin_without_staff_member(self): + """Test that a user without a StaffMember instance is not identified as a staff member.""" + self.need_staff_login() + + # Ensure the user does not have a StaffMember instance + StaffMember.objects.filter(user=self.user1).delete() + + url = reverse('appointment:is_user_staff_admin') + response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + + # Check the response status code and content + self.assertEqual(response.status_code, 200) + response_data = response.json() + self.assertIn('message', response_data) + self.assertEqual(response_data['message'], _("User is not a staff member.")) + + +class AppointmentTestCase(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.appointment = self.create_appt_for_sm1() + + def test_delete_appointment(self): + self.need_staff_login() + + url = reverse('appointment:delete_appointment', args=[self.appointment.id]) + response = self.client.get(url) + + self.assertEqual(response.status_code, 302) # Redirect status code + self.assertRedirects(response, reverse('appointment:get_user_appointments')) + + # Check for success messages + messages_list = list(get_messages(response.wsgi_request)) + self.assertTrue(any(_("Appointment deleted successfully!") in str(message) for message in messages_list)) + + # Check if appointment is deleted + appointment_exists = Appointment.objects.filter(pk=self.appointment.id).exists() + self.assertFalse(appointment_exists, "Appointment should be deleted but still exists.") + + def test_delete_appointment_ajax(self): + self.need_staff_login() + + url = reverse('appointment:delete_appointment_ajax') + data = json.dumps({'appointment_id': self.appointment.id}) + response = self.client.post(url, data, content_type='application/json') + + self.assertEqual(response.status_code, 200) + # Expecting both 'message' and 'success' in the response + expected_response = {"message": "Appointment deleted successfully.", "success": True} + self.assertEqual(json.loads(response.content), expected_response) + + # Check if appointment is deleted + appointment_exists = Appointment.objects.filter(pk=self.appointment.id).exists() + self.assertFalse(appointment_exists, "Appointment should be deleted but still exists.") + + def test_delete_appointment_without_permission(self): + """Test that deleting an appointment without permission fails.""" + self.need_staff_login() # Login as a regular staff user + + # Try to delete an appointment belonging to a different staff member + different_appointment = self.create_appt_for_sm2() + url = reverse('appointment:delete_appointment', args=[different_appointment.id]) + + response = self.client.post(url) + + # Check that the user is redirected due to lack of permissions + self.assertEqual(response.status_code, 403) + + # Verify that the appointment still exists in the database + self.assertTrue(Appointment.objects.filter(id=different_appointment.id).exists()) + + def test_delete_appointment_ajax_without_permission(self): + """Test that deleting an appointment via AJAX without permission fails.""" + self.need_staff_login() # Login as a regular staff user + + # Try to delete an appointment belonging to a different staff member + different_appointment = self.create_appt_for_sm2() + url = reverse('appointment:delete_appointment_ajax') + + response = self.client.post(url, {'appointment_id': different_appointment.id}, content_type='application/json') + + # Check that the response indicates failure due to lack of permissions + self.assertEqual(response.status_code, 403) + response_data = response.json() + self.assertEqual(response_data['message'], _("You can only delete your own appointments.")) + self.assertFalse(response_data['success']) + + # Verify that the appointment still exists in the database + self.assertTrue(Appointment.objects.filter(id=different_appointment.id).exists()) + + +class UpdateAppointmentTestCase(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.appointment = self.create_appt_for_sm1() + self.tomorrow = date.today() + timedelta(days=1) + self.data = { + 'isCreating': False, 'service_id': self.service1.pk, 'appointment_id': self.appointment.id, + 'client_name': 'Vala Mal Doran', + 'client_email': 'vala.mal-doran@django-appointment.com', 'client_phone': '+12392350345', + 'client_address': '456 Outer Rim, Free Jaffa Nation', + 'want_reminder': 'false', 'additional_info': '', 'start_time': '15:00:26', + 'staff_id': self.staff_member1.id, + 'date': self.tomorrow.strftime('%Y-%m-%d') + } + + def test_update_appt_min_info_create(self): + self.need_staff_login() + + # Preparing data + self.data.update({'isCreating': True, 'appointment_id': None}) + url = reverse('appointment:update_appt_min_info') + + # Making the request + response = self.client.post(url, data=json.dumps(self.data), content_type='application/json', + **{'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}) + + # Check response status + self.assertEqual(response.status_code, 200) + + # Check response content + response_data = response.json() + self.assertIn('message', response_data) + self.assertEqual(response_data['message'], 'Appointment created successfully.') + self.assertIn('appt', response_data) + + # Verify appointment created in the database + appointment_id = response_data['appt'][0]['id'] + self.assertTrue(Appointment.objects.filter(id=appointment_id).exists()) + + def test_update_appt_min_info_update(self): + self.need_superuser_login() + + # Create an appointment to update + url = reverse('appointment:update_appt_min_info') + self.staff_member1.services_offered.add(self.service1) + + # Making the request + response = self.client.post(url, data=json.dumps(self.data), content_type='application/json', + **{'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}) + print(f"response: {response.content}") + + # Check response status + self.assertEqual(response.status_code, 200) + + # Check response content + response_data = response.json() + self.assertIn('message', response_data) + self.assertEqual(response_data['message'], 'Appointment updated successfully.') + self.assertIn('appt', response_data) + + # Verify appointment updated in the database + updated_appt = Appointment.objects.get(id=self.appointment.id) + self.assertEqual(updated_appt.client.email, self.data['client_email']) + + def test_update_nonexistent_appointment(self): + self.need_superuser_login() + + # Preparing data with a non-existent appointment ID + self.data['appointment_id'] = 999 + url = reverse('appointment:update_appt_min_info') + + # Making the request + response = self.client.post(url, data=json.dumps(self.data), content_type='application/json', + **{'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}) + + # Check response status and content + self.assertEqual(response.status_code, 404) + response_data = response.json() + self.assertIn('message', response_data) + self.assertEqual(response_data['message'], "Appointment does not exist.") + + def test_update_with_nonexistent_service(self): + self.need_superuser_login() + + # Preparing data with a non-existent service ID + self.data['service_id'] = 999 + url = reverse('appointment:update_appt_min_info') + + # Making the request + response = self.client.post(url, data=json.dumps(self.data), content_type='application/json', + **{'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}) + + # Check response status and content + self.assertEqual(response.status_code, 404) + response_data = response.json() + self.assertIn('message', response_data) + self.assertEqual(response_data['message'], "Service does not exist.") + + def test_update_with_invalid_data_causing_exception(self): + self.need_superuser_login() + + # Preparing invalid data to trigger an exception, for example here, no email address + data = { + 'isCreating': False, 'service_id': '1', 'appointment_id': self.appointment.id, + } + url = reverse('appointment:update_appt_min_info') + + # Making the request + response = self.client.post(url, data=json.dumps(data), content_type='application/json', + **{'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}) + + # Check response status and content + self.assertEqual(response.status_code, 400) + response_data = response.json() + self.assertIn('message', response_data) + + +class ServiceViewTestCase(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + + def test_fetch_service_list_for_staff(self): + self.need_staff_login() + + # Assuming self.service1 and self.service2 are services linked to self.staff_member1 + self.staff_member1.services_offered.add(self.service1, self.service2) + staff_member_services = [self.service1, self.service2] + + # Simulate a request without appointmentId + url = reverse('appointment:fetch_service_list_for_staff') + response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + response_data = response.json() + self.assertEqual(response_data["message"], "Successfully fetched services.") + self.assertCountEqual( + response_data["services_offered"], + [{"id": service.id, "name": service.name} for service in staff_member_services] + ) + + # Create a test appointment and link it to self.staff_member1 + test_appointment = self.create_appt_for_sm1() + + # Simulate a request with appointmentId + url_with_appointment = f"{url}?appointmentId={test_appointment.id}" + response_with_appointment = self.client.get(url_with_appointment, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response_with_appointment.status_code, 200) + response_data_with_appointment = response_with_appointment.json() + self.assertEqual(response_data_with_appointment["message"], "Successfully fetched services.") + # Assuming the staff member linked to the appointment offers the same services + self.assertCountEqual( + response_data_with_appointment["services_offered"], + [{"id": service.id, "name": service.name} for service in staff_member_services] + ) + + def test_fetch_service_list_for_staff_no_staff_member_instance(self): + """Test that a superuser without a StaffMember instance receives no inappropriate error message.""" + self.need_superuser_login() + jack = self.users['superuser'] + + # Ensure the superuser does not have a StaffMember instance + StaffMember.objects.filter(user=jack).delete() + + url = reverse('appointment:fetch_service_list_for_staff') + response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + + # Check the response status code and content + self.assertEqual(response.status_code, 200) + response_data = response.json() + self.assertIn('message', response_data) + + def test_fetch_service_list_for_staff_no_services_offered(self): + """Test fetching services for a staff member who offers no services.""" + self.need_staff_login() + + # Assuming self.staff_member1 offers no services + self.staff_member1.services_offered.clear() + + url = reverse('appointment:fetch_service_list_for_staff') + response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + + # Check response status code and content + self.assertEqual(response.status_code, 404) + response_data = response.json() + self.assertIn('message', response_data) + self.assertEqual(response_data['message'], _("No services offered by this staff member.")) + self.assertFalse(response_data['success']) + + def test_delete_service_with_superuser(self): + self.need_superuser_login() + # Test deletion with a superuser + response = self.client.get(reverse('appointment:delete_service', args=[self.service1.id])) + + # Check if the service was deleted + self.assertFalse(Service.objects.filter(id=self.service1.id).exists()) + + # Check if the success message is added + messages_ = list(get_messages(response.wsgi_request)) + self.assertIn(_("Service deleted successfully!"), [m.message for m in messages_]) + + # Check if it redirects to the user profile + self.assertRedirects(response, reverse('appointment:user_profile')) + + def test_delete_service_without_superuser(self): + # Log in as a regular/staff user + self.need_staff_login() + + response = self.client.get(reverse('appointment:delete_service', args=[self.service1.id])) + + # Check for a forbidden status code, as only superusers should be able to delete services + self.assertEqual(response.status_code, 403) + + def test_delete_nonexistent_service(self): + self.need_superuser_login() + # Try to delete a service that does not exist + response = self.client.get(reverse('appointment:delete_service', args=[99999])) + + # Check for a 404-status code + self.assertEqual(response.status_code, 404) + + +class AppointmentDisplayViewTestCase(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.appointment = self.create_appt_for_sm1() + self.url_display_appt = reverse('appointment:display_appointment', args=[self.appointment.id]) + + def test_display_appointment_authenticated_staff_user(self): + # Log in as staff user + self.need_staff_login() + response = self.client.get(self.url_display_appt) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'administration/display_appointment.html') + + def test_display_appointment_authenticated_superuser(self): + # Log in as superuser + self.need_superuser_login() + response = self.client.get(self.url_display_appt) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'administration/display_appointment.html') + + def test_display_appointment_unauthenticated_user(self): + # Attempt access without logging in + response = self.client.get(self.url_display_appt) + self.assertNotEqual(response.status_code, 200) # Expect redirection or error + + def test_display_appointment_authenticated_unauthorized_user(self): + # Log in as a regular user + self.need_normal_login() + response = self.client.get(self.url_display_appt) + self.assertNotEqual(response.status_code, 200) # Expect redirection or error + + def test_display_appointment_non_existent(self): + # Log in as staff user + self.need_superuser_login() + non_existent_url = reverse('appointment:display_appointment', args=[99999]) # Non-existent appointment ID + response = self.client.get(non_existent_url) + self.assertEqual(response.status_code, 404) # Expect 404 error + + +class DayOffViewsTestCase(BaseTest): + def setUp(self): + super().setUp() + self.url_add_day_off = reverse('appointment:add_day_off', args=[self.staff_member1.user_id]) + self.other_staff_member = self.staff_member2 + self.day_off = DayOff.objects.create(staff_member=self.staff_member1, + start_date=date.today() + timedelta(days=1), + end_date=date.today() + timedelta(days=2), description="Day off") + + def test_add_day_off_authenticated_staff_user(self): + # Log in as staff user + self.need_staff_login() + response = self.client.post(self.url_add_day_off, data={'start_date': '2050-01-01', 'end_date': '2050-01-01', + 'description': 'Test reason'}) + self.assertEqual(response.status_code, 200) # Assuming success redirects or shows a success message + + def test_add_day_off_authenticated_superuser_for_other(self): + # Log in as superuser + self.need_superuser_login() + other_staff_user_id = self.other_staff_member.user.pk + response = self.client.post(reverse('appointment:add_day_off', args=[other_staff_user_id]), + data={'start_date': '2023-01-02', 'end_date': '2050-01-01', + 'description': 'Admin adding for staff'}) + self.assertEqual(response.status_code, 200) # Assuming superuser can add for others + + def test_add_day_off_unauthenticated_user(self): + # Attempt access without logging in + response = self.client.post(self.url_add_day_off, data={'start_date': '2050-01-01', 'end_date': '2050-01-01', + 'description': 'Test reason'}) + self.assertNotEqual(response.status_code, 200) # Expect redirection or error + + def test_add_day_off_authenticated_unauthorized_user(self): + # Log in as a regular user + self.need_normal_login() + unauthorized_staff_user_id = self.other_staff_member.user.pk + response = self.client.post(reverse('appointment:add_day_off', args=[unauthorized_staff_user_id]), + data={'start_date': '2050-01-01', 'end_date': '2050-01-01', + 'description': 'Trying to add for others'}) + self.assertNotEqual(response.status_code, 200) # Expect redirection or error due to unauthorized action + + def test_update_day_off_authenticated_staff_user(self): + # Log in as staff user who owns the day off + self.need_staff_login() + url = reverse('appointment:update_day_off', args=[self.day_off.id]) + response = self.client.post(url, {'start_date': '2050-01-01', 'end_date': '2050-01-01', + 'description': 'Updated reason'}) + self.assertEqual(response.status_code, 200) + + def test_update_day_off_unauthorized_user(self): + # Log in as another staff user + self.need_normal_login() + url = reverse('appointment:update_day_off', args=[self.day_off.id]) + response = self.client.post(url, {'start_date': '2050-01-01', 'end_date': '2050-01-01', + 'description': 'Trying unauthorized update'}, 'json') + self.assertEqual(response.status_code, 403) # Expect forbidden error + + def test_update_nonexistent_day_off(self): + self.need_staff_login() + non_existent_day_off_id = 99999 + url = reverse('appointment:update_day_off', args=[non_existent_day_off_id]) + response = self.client.post(url, data={'start_date': '2050-01-01', 'end_date': '2050-01-01', + 'description': 'Non existent day off'}) + self.assertEqual(response.status_code, 404) # Expect 404 error + + def test_delete_day_off_authenticated_super_user(self): + # Log in as staff user + self.need_superuser_login() + url = reverse('appointment:delete_day_off', args=[self.day_off.id]) + response = self.client.get(url) + self.assertEqual(response.status_code, 302) # Assuming success redirects to the user profile + + def test_delete_day_off_unauthorized_user(self): + # Log in as another staff user + self.need_normal_login() + url = reverse('appointment:delete_day_off', args=[self.day_off.id]) + response = self.client.get(url) + self.assertEqual(response.status_code, 403) # Expect access denied + + def test_delete_nonexistent_day_off(self): + self.need_staff_login() + non_existent_day_off_id = 99999 + url = reverse('appointment:delete_day_off', args=[non_existent_day_off_id]) + response = self.client.get(url) + self.assertEqual(response.status_code, 404) + + +class ViewsTestCase(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.client = Client() + self.factory = RequestFactory() + self.request = self.factory.get('/') + self.staff_member = self.staff_member1 + WorkingHours.objects.create(staff_member=self.staff_member1, day_of_week=0, + start_time=datetime.time(8, 0), end_time=datetime.time(12, 0)) + WorkingHours.objects.create(staff_member=self.staff_member1, day_of_week=2, + start_time=datetime.time(8, 0), end_time=datetime.time(12, 0)) + self.ar = self.create_appt_request_for_sm1() + self.user1 = self.users['staff1'] + self.user1.is_staff = True + self.request.user = self.user1 + + def test_get_next_available_date_ajax(self): + """get_next_available_date_ajax view should return a JSON response with the next available date.""" + data = {'staff_id': self.staff_member.id} + url = reverse('appointment:request_next_available_slot', args=[self.service1.id]) + response = self.client.get(url, data=data, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + response_data = json.loads(response.content) + self.assertIsNotNone(response_data) + self.assertIsNotNone(response_data['next_available_date']) + + def test_default_thank_you(self): + """Test if the default thank you page can be rendered.""" + appointment = Appointment.objects.create(client=self.user1, appointment_request=self.ar) + url = reverse('appointment:default_thank_you', args=[appointment.id]) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + self.assertIn(appointment.get_service_name(), str(response.content)) + + +class AddStaffMemberInfoTestCase(ViewsTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.staff_member = self.staff_member1 + self.url = reverse('appointment:add_staff_member_info') + self.user_test = self.create_user_( + first_name="Great Tester", email="great.tester@django-appointment.com", username="great_tester" + ) + self.data = { + "user": self.user_test.id, + "services_offered": [self.service1.id, self.service2.id], + "working_hours": [ + {"day_of_week": 0, "start_time": "08:00", "end_time": "12:00"}, + {"day_of_week": 2, "start_time": "08:00", "end_time": "12:00"} + ] + } + + def test_add_staff_member_info_access_by_superuser(self): + """Test that the add staff member page is accessible by a superuser.""" + self.need_superuser_login() + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertIsInstance(response.context['form'], StaffMemberForm) + + def test_add_staff_member_info_access_by_non_superuser(self): + """Test that non-superusers cannot access the add staff member page.""" + self.need_staff_login() + response = self.client.get(self.url) + self.assertEqual(response.status_code, 403) + + def test_add_staff_member_info_successful_submission(self): + """Test successful submission of the "add staff member" form.""" + self.need_superuser_login() + response = self.client.post(self.url, data=self.data) + self.assertEqual(response.status_code, 302) # Expect a redirect + self.assertTrue(StaffMember.objects.filter(user=self.user_test).exists()) + + def test_add_staff_member_info_invalid_form_submission(self): + """Test submission of an invalid form.""" + self.need_superuser_login() + data = self.data.copy() + data.pop('user') + response = self.client.post(self.url, data=data) + self.assertEqual(response.status_code, 200) + self.assertIsInstance(response.context['form'], StaffMemberForm) + self.assertTrue(response.context['form'].errors) + + +class SetPasswordViewTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + user_data = { + 'username': 'bratac.of-chulak', + 'email': 'bratac.of-chulak@django-', + 'password': 'oldpassword', + 'first_name': "Bra'tac", + 'last_name': 'of Chulak' + } + + self.user = create_user_with_username(user_data) + self.token = PasswordResetToken.create_token(user=self.user, expiration_minutes=2880) # 2 days expiration + self.ui_db64 = urlsafe_base64_encode(force_bytes(self.user.pk)) + self.relative_set_passwd_link = reverse('appointment:set_passwd', args=[self.ui_db64, self.token.token]) + self.valid_link = reverse('appointment:set_passwd', args=[self.ui_db64, str(self.token.token)]) + + def test_get_request_with_valid_token(self): + assert PasswordResetToken.objects.filter(user=self.user, token=self.token.token).exists(), ("Token not found " + "in database") + response = self.client.get(self.valid_link) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "form") + self.assertNotContains(response, "The password reset link is invalid or has expired.") + + def test_post_request_with_valid_token_and_correct_password(self): + new_password_data = {'new_password1': 'newstrongpassword123', 'new_password2': 'newstrongpassword123'} + response = self.client.post(self.valid_link, new_password_data) + self.user.refresh_from_db() + self.assertTrue(self.user.check_password(new_password_data['new_password1'])) + messages_ = list(get_messages(response.wsgi_request)) + self.assertTrue(any(msg.message == _("Password reset successfully.") for msg in messages_)) + + def test_get_request_with_expired_token(self): + expired_token = PasswordResetToken.create_token(user=self.user, expiration_minutes=-60) + expired_token_link = reverse('appointment:set_passwd', args=[self.ui_db64, str(expired_token.token)]) + response = self.client.get(expired_token_link) + self.assertEqual(response.status_code, 200) + self.assertIn('messages', response.context) + self.assertEqual(response.context['page_message'], passwd_error) + + def test_get_request_with_invalid_token(self): + invalid_token = str(uuid.uuid4()) + invalid_token_link = reverse('appointment:set_passwd', args=[self.ui_db64, invalid_token]) + response = self.client.get(invalid_token_link, follow=True) + self.assertEqual(response.status_code, 200) + messages_ = list(get_messages(response.wsgi_request)) + self.assertTrue( + any(msg.message == _("The password reset link is invalid or has expired.") for msg in messages_)) + + def test_post_request_with_invalid_token(self): + invalid_token = str(uuid.uuid4()) + invalid_token_link = reverse('appointment:set_passwd', args=[self.ui_db64, invalid_token]) + new_password = 'newpassword123' + post_data = {'new_password1': new_password, 'new_password2': new_password} + response = self.client.post(invalid_token_link, post_data) + self.user.refresh_from_db() + self.assertFalse(self.user.check_password(new_password)) + messages_ = list(get_messages(response.wsgi_request)) + self.assertTrue(any(_("The password reset link is invalid or has expired.") in str(m) for m in messages_)) + + def test_post_request_with_expired_token(self): + expired_token = PasswordResetToken.create_token(user=self.user, expiration_minutes=-60) + expired_token_link = reverse('appointment:set_passwd', args=[self.ui_db64, str(expired_token.token)]) + new_password_data = {'new_password1': 'newpassword', 'new_password2': 'newpassword'} + response = self.client.post(expired_token_link, new_password_data) + self.assertEqual(response.status_code, 200) + messages_ = list(get_messages(response.wsgi_request)) + self.assertTrue( + any(msg.message == _("The password reset link is invalid or has expired.") for msg in messages_)) + + +class GetNonWorkingDaysAjaxTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.client = Client() + self.url = reverse('appointment:get_non_working_days_ajax') + + def test_no_staff_member_selected(self): + """Test the response when no staff member is selected.""" + response = self.client.get(self.url, {'staff_id': 'none'}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + response_data = response.json() + self.assertFalse(response_data['success']) + self.assertEqual(response_data['message'], _('No staff member selected')) + self.assertIn('errorCode', response_data) + self.assertEqual(response_data['errorCode'], ErrorCode.STAFF_ID_REQUIRED.value) + + def test_valid_staff_member_selected(self): + """Test the response for a valid staff member selection.""" + response = self.client.get(self.url, {'staff_id': self.staff_member1.id}, + HTTP_X_REQUESTED_WITH='XMLHttpRequest') + self.assertEqual(response.status_code, 200) + response_data = response.json() + self.assertTrue(response_data['success']) + self.assertEqual(response_data['message'], _('Successfully retrieved non-working days')) + self.assertIn('non_working_days', response_data) + self.assertTrue(isinstance(response_data['non_working_days'], list)) + + def test_ajax_required(self): + """Ensure the view only responds to AJAX requests.""" + non_ajax_response = self.client.get(self.url, {'staff_id': self.staff_member1.id}) + self.assertEqual(non_ajax_response.status_code, 200) + + +class AppointmentClientInformationTest(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.client = Client() + self.ar = self.create_appt_request_for_sm1() + self.url = reverse('appointment:appointment_client_information', args=[self.ar.pk, self.ar.id_request]) + self.factory = RequestFactory() + self.request = self.factory.get('/') + self.valid_form_data = { + 'name': 'Adria Origin', + 'service_id': '1', + 'payment_type': 'full', + 'email': 'adria@django-appointment.com', + 'phone': '+1234567890', + 'address': '123 Ori Temple, Celestis', + } + + def test_get_request(self): + """Test the view with a GET request.""" + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'appointment/appointment_client_information.html') + + def test_post_request_invalid_form(self): + """Test the view with an invalid POST request.""" + response = self.client.post(self.url, {}) # Empty data for invalid form + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'appointment/appointment_client_information.html') + + def test_already_submitted_session(self): + """Test the view when the appointment has already been submitted.""" + session = self.client.session + session[f'appointment_submitted_{self.ar.id_request}'] = True + session.save() + + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'error_pages/304_already_submitted.html') + + +class PrepareRescheduleAppointmentViewTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.client = Client() + self.ar = self.create_appt_request_for_sm1() + self.url = reverse('appointment:prepare_reschedule_appointment', args=[self.ar.id_request]) + + @patch('appointment.utils.db_helpers.can_appointment_be_rescheduled', return_value=True) + def test_reschedule_appointment_allowed(self, mock_can_appointment_be_rescheduled): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertIn('all_staff_members', response.context) + self.assertIn('available_slots', response.context) + self.assertIn('service', response.context) + self.assertIn('staff_member', response.context) + + def test_reschedule_appointment_not_allowed(self): + self.service1.reschedule_limit = 0 + self.service1.allow_rescheduling = True + self.service1.save() + response = self.client.get(self.url) + self.assertEqual(response.status_code, 403) + self.assertTemplateUsed(response, 'error_pages/403_forbidden_rescheduling.html') + + def test_reschedule_appointment_context_data(self): + Config.objects.create(app_offered_by_label="Test Label") + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['label'], "Test Label") + self.assertEqual(response.context['page_title'], f"Rescheduling appointment for {self.service1.name}") + self.assertTrue('date_chosen' in response.context) + self.assertTrue('page_description' in response.context) + self.assertTrue('timezoneTxt' in response.context) + + +class RescheduleAppointmentSubmitViewTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.client = Client() + self.ar = self.create_appt_request_for_sm1(date_=timezone.now().date() + datetime.timedelta(days=1)) + self.appointment = self.create_appt_for_sm1(appointment_request=self.ar) + self.url = reverse('appointment:reschedule_appointment_submit') + self.post_data = { + 'appointment_request_id': self.ar.id_request, + 'date': (timezone.now().date() + datetime.timedelta(days=2)).isoformat(), + 'start_time': '10:00', + 'end_time': '11:00', + 'staff_member': self.staff_member1.id, + 'reason_for_rescheduling': 'Need a different time', + } + + def test_post_request_with_valid_form(self): + with patch('appointment.views.AppointmentRequestForm.is_valid', return_value=True), \ + patch('appointment.views.send_reschedule_confirmation_email') as mock_send_email: + response = self.client.post(self.url, self.post_data) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'appointment/rescheduling_thank_you.html') + mock_send_email.assert_called_once() + self.assertTrue(AppointmentRescheduleHistory.objects.exists()) + + def test_post_request_with_invalid_form(self): + # Simulate an invalid form submission + response = self.client.post(self.url, {}) + self.assertEqual(response.status_code, 404) + + def test_get_request(self): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'appointment/appointments.html') + + def test_reschedule_not_allowed(self): + # Simulate the scenario where rescheduling is not allowed by setting the reschedule limit to 0 + self.service1.reschedule_limit = 0 + self.service1.allow_rescheduling = False + self.service1.save() + + response = self.client.post(self.url, self.post_data) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'appointment/appointments.html') + messages_list = list(get_messages(response.wsgi_request)) + self.assertTrue(any( + _("There was an error in your submission. Please check the form and try again.") in str(message) for + message + in messages_list)) + + +class ConfirmRescheduleViewTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.client = Client() + self.ar = self.create_appt_request_for_sm1() + self.create_appt_for_sm1(appointment_request=self.ar) + self.reschedule_history = AppointmentRescheduleHistory.objects.create( + appointment_request=self.ar, + date=timezone.now().date() + timezone.timedelta(days=2), + start_time='10:00', + end_time='11:00', + staff_member=self.staff_member1, + id_request='unique_id_request', + reschedule_status='pending' + ) + self.url = reverse('appointment:confirm_reschedule', args=[self.reschedule_history.id_request]) + + def test_confirm_reschedule_valid(self): + response = self.client.get(self.url) + self.reschedule_history.refresh_from_db() + self.ar.refresh_from_db() + self.assertEqual(self.reschedule_history.reschedule_status, 'confirmed') + self.assertEqual(response.status_code, 302) # Redirect to thank you page + self.assertRedirects(response, reverse('appointment:default_thank_you', args=[self.ar.appointment.id])) + + def test_confirm_reschedule_invalid_status(self): + self.reschedule_history.reschedule_status = 'confirmed' + self.reschedule_history.save() + response = self.client.get(self.url) + self.assertEqual(response.status_code, 404) # Render 404_not_found with error message + + def test_confirm_reschedule_no_longer_valid(self): + with patch('appointment.models.AppointmentRescheduleHistory.still_valid', return_value=False): + response = self.client.get(self.url) + self.assertEqual(response.status_code, 404) # Render 404_not_found with error message + + def test_confirm_reschedule_updates(self): + """Ensure that the appointment request and reschedule history are updated correctly.""" + self.client.get(self.url) + self.ar.refresh_from_db() + self.reschedule_history.refresh_from_db() + self.assertEqual(self.ar.staff_member, self.reschedule_history.staff_member) + self.assertEqual(self.reschedule_history.reschedule_status, 'confirmed') + + @patch('appointment.views.notify_admin_about_reschedule') + def test_notify_admin_about_reschedule_called(self, mock_notify_admin): + self.client.get(self.url) + mock_notify_admin.assert_called_once() + self.assertTrue(mock_notify_admin.called) + + +class RedirectToPaymentOrThankYouPageTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.appointment = self.create_appt_for_sm1() + + @patch('appointment.views.APPOINTMENT_PAYMENT_URL', 'http://example.com/payment/') + @patch('appointment.views.create_payment_info_and_get_url') + def test_redirect_to_payment_page(self, mock_create_payment_info_and_get_url): + """Test redirection to the payment page when APPOINTMENT_PAYMENT_URL is set.""" + mock_create_payment_info_and_get_url.return_value = 'http://example.com/payment/12345' + response = redirect_to_payment_or_thank_you_page(self.appointment) + + self.assertIsInstance(response, HttpResponseRedirect) + self.assertEqual(response.url, 'http://example.com/payment/12345') + + @patch('appointment.views.APPOINTMENT_PAYMENT_URL', '') + @patch('appointment.views.APPOINTMENT_THANK_YOU_URL', 'appointment:default_thank_you') + def test_redirect_to_custom_thank_you_page(self): + """Test redirection to a custom thank-you page when APPOINTMENT_THANK_YOU_URL is set.""" + response = redirect_to_payment_or_thank_you_page(self.appointment) + + self.assertIsInstance(response, HttpResponseRedirect) + self.assertTrue(response.url.startswith( + reverse('appointment:default_thank_you', kwargs={'appointment_id': self.appointment.id}))) + + @patch('appointment.views.APPOINTMENT_PAYMENT_URL', '') + @patch('appointment.views.APPOINTMENT_THANK_YOU_URL', '') + def test_redirect_to_default_thank_you_page(self): + """Test redirection to the default thank-you page when no specific URL is set.""" + response = redirect_to_payment_or_thank_you_page(self.appointment) + + self.assertIsInstance(response, HttpResponseRedirect) + self.assertTrue(response.url.startswith( + reverse('appointment:default_thank_you', kwargs={'appointment_id': self.appointment.id}))) + + +class CreateAppointmentTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.appointment_request = self.create_appt_request_for_sm1() + self.client_data = {'name': 'Orlin Ascended', 'email': 'orlin.ascended@django-appointment.com'} + self.appointment_data = {'phone': '1234567890', 'want_reminder': True, 'address': '123 Ancient St, Velona', + 'additional_info': 'Test info'} + self.request = RequestFactory().get('/') + + @patch('appointment.views.create_and_save_appointment') + @patch('appointment.views.redirect_to_payment_or_thank_you_page') + def test_create_appointment_success(self, mock_redirect, mock_create_and_save): + """Test successful creation of an appointment and redirection.""" + # Mock the appointment creation to return an Appointment instance + mock_appointment = MagicMock() + mock_create_and_save.return_value = mock_appointment + + # Mock the redirection function to simulate a successful redirection + mock_redirect.return_value = MagicMock() + + create_appointment(self.request, self.appointment_request, self.client_data, self.appointment_data) + + # Verify that create_and_save_appointment was called with the correct arguments + mock_create_and_save.assert_called_once_with(self.appointment_request, self.client_data, self.appointment_data, + self.request) + + # Verify that the redirect_to_payment_or_thank_you_page was called with the created appointment + mock_redirect.assert_called_once_with(mock_appointment) diff --git a/appointment/tests/utils/__init__.py b/appointment/tests/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/appointment/tests/utils/test_date_time.py b/appointment/tests/utils/test_date_time.py new file mode 100644 index 0000000..e2bb5af --- /dev/null +++ b/appointment/tests/utils/test_date_time.py @@ -0,0 +1,389 @@ +# test_date_time.py +# Path: appointment/tests/utils/test_date_time.py + +import datetime +from unittest.mock import Mock, patch + +from django.test import TestCase + +from appointment.utils.date_time import ( + combine_date_and_time, convert_12_hour_time_to_24_hour_time, convert_24_hour_time_to_12_hour_time, + convert_minutes_in_human_readable_format, convert_str_to_date, + convert_str_to_time, get_ar_end_time, get_current_year, get_timestamp, get_weekday_num, + time_difference +) + + +class Convert12HourTo24HourTimeTests(TestCase): + def test_basic_conversion(self): + """Test basic 12-hour to 24-hour conversions.""" + self.assertEqual(convert_12_hour_time_to_24_hour_time("01:10 AM"), "01:10:00") + self.assertEqual(convert_12_hour_time_to_24_hour_time("01:20 PM"), "13:20:00") + + def test_midnight_and_noon(self): + """Test conversion of midnight and noon times.""" + self.assertEqual(convert_12_hour_time_to_24_hour_time("12:00 AM"), "00:00:00") + self.assertEqual(convert_12_hour_time_to_24_hour_time("12:00 PM"), "12:00:00") + + def test_boundary_times(self): + """Test conversion of boundary times.""" + self.assertEqual(convert_12_hour_time_to_24_hour_time("12:00 AM"), "00:00:00") + self.assertEqual(convert_12_hour_time_to_24_hour_time("11:59 PM"), "23:59:00") + + def test_datetime_and_time_objects(self): + """Test conversion using datetime and time objects.""" + dt_obj = datetime.datetime(2023, 1, 1, 14, 30) + time_obj = datetime.time(14, 30) + self.assertEqual(convert_12_hour_time_to_24_hour_time(dt_obj), "14:30:00") + self.assertEqual(convert_12_hour_time_to_24_hour_time(time_obj), "14:30:00") + + def test_case_insensitivity_and_whitespace(self): + """Test conversion handling of different case formats and white-space.""" + self.assertEqual(convert_12_hour_time_to_24_hour_time(" 12:00 am "), "00:00:00") + self.assertEqual(convert_12_hour_time_to_24_hour_time("01:00 pM "), "13:00:00") + + def test_invalid_values(self): + """Test invalid values.""" + with self.assertRaises(ValueError): + convert_12_hour_time_to_24_hour_time("13:00 PM") + with self.assertRaises(ValueError): + convert_12_hour_time_to_24_hour_time("12:60 AM") + with self.assertRaises(ValueError): + convert_12_hour_time_to_24_hour_time(["12:00 AM"]) + with self.assertRaises(ValueError): + convert_12_hour_time_to_24_hour_time({"time": "12:00 AM"}) + with self.assertRaises(ValueError): + convert_12_hour_time_to_24_hour_time("25:00 AM") + with self.assertRaises(ValueError): + convert_12_hour_time_to_24_hour_time("01:00") + with self.assertRaises(ValueError): + convert_12_hour_time_to_24_hour_time("Random String") + with self.assertRaises(ValueError): + convert_12_hour_time_to_24_hour_time("01:60 AM") + + +class Convert24HourTimeTo12HourTimeTests(TestCase): + + def test_valid_24_hour_strings(self): + self.assertEqual(convert_24_hour_time_to_12_hour_time("13:00"), "01:00 PM") + self.assertEqual(convert_24_hour_time_to_12_hour_time("00:00"), "12:00 AM") + self.assertEqual(convert_24_hour_time_to_12_hour_time("23:59"), "11:59 PM") + self.assertEqual(convert_24_hour_time_to_12_hour_time("12:00"), "12:00 PM") + self.assertEqual(convert_24_hour_time_to_12_hour_time("01:00"), "01:00 AM") + + def test_valid_24_hour_with_seconds(self): + self.assertEqual(convert_24_hour_time_to_12_hour_time("13:00:01"), "01:00:01 PM") + self.assertEqual(convert_24_hour_time_to_12_hour_time("00:00:59"), "12:00:59 AM") + + def test_time_object_input(self): + time_input = datetime.time(13, 15) + self.assertEqual(convert_24_hour_time_to_12_hour_time(time_input), "01:15 PM") + time_input = datetime.time(0, 0) + self.assertEqual(convert_24_hour_time_to_12_hour_time(time_input), "12:00 AM") + + def test_invalid_time_strings(self): + with self.assertRaises(ValueError): + convert_24_hour_time_to_12_hour_time("25:00") + with self.assertRaises(ValueError): + convert_24_hour_time_to_12_hour_time("-01:00") + with self.assertRaises(ValueError): + convert_24_hour_time_to_12_hour_time("13:60") + with self.assertRaises(ValueError): + convert_24_hour_time_to_12_hour_time("invalid") + with self.assertRaises(ValueError): + convert_24_hour_time_to_12_hour_time("1 PM") + with self.assertRaises(ValueError): + convert_24_hour_time_to_12_hour_time("13 PM") + with self.assertRaises(ValueError): + convert_24_hour_time_to_12_hour_time("24:00") + + def test_edge_cases(self): + self.assertEqual(convert_24_hour_time_to_12_hour_time("12:00"), "12:00 PM") + self.assertEqual(convert_24_hour_time_to_12_hour_time("00:00"), "12:00 AM") + self.assertEqual(convert_24_hour_time_to_12_hour_time("11:59"), "11:59 AM") + self.assertEqual(convert_24_hour_time_to_12_hour_time("23:59"), "11:59 PM") + + +class ConvertMinutesInHumanReadableFormatTests(TestCase): + def test_basic_conversions(self): + """Test basic conversions""" + self.assertEqual(convert_minutes_in_human_readable_format(30), "30 minutes") + self.assertEqual(convert_minutes_in_human_readable_format(90), "1 hour and 30 minutes") + + def test_edge_cases(self): + """Test edgy cases""" + self.assertEqual(convert_minutes_in_human_readable_format(59), "59 minutes") + self.assertEqual(convert_minutes_in_human_readable_format(60), "1 hour") + self.assertEqual(convert_minutes_in_human_readable_format(1439), "23 hours and 59 minutes") + # '1440' minutes is the total number of minutes in a day, hence it should convert to "1 day". + self.assertEqual(convert_minutes_in_human_readable_format(1440), "1 day") + + def test_valid_combinations(self): + """Test various combinations""" + self.assertEqual(convert_minutes_in_human_readable_format(1441), "1 day and 1 minute") + self.assertEqual(convert_minutes_in_human_readable_format(1500), "1 day and 1 hour") + self.assertEqual(convert_minutes_in_human_readable_format(1560), "1 day and 2 hours") + self.assertEqual(convert_minutes_in_human_readable_format(1501), "1 day, 1 hour and 1 minute") + self.assertEqual(convert_minutes_in_human_readable_format(1562), "1 day, 2 hours and 2 minutes") + self.assertEqual(convert_minutes_in_human_readable_format(808), "13 hours and 28 minutes") + + def test_non_positive_values(self): + """Test that non-positive values are handled correctly""" + self.assertEqual(convert_minutes_in_human_readable_format(0), "Not set.") + # Expectation for negative values might depend on desired behavior, just an example below + with self.assertRaises(ValueError): + convert_minutes_in_human_readable_format(-5) + + def test_float_values(self): + """Test float values which should be correctly rounded down""" + self.assertEqual(convert_minutes_in_human_readable_format(2.5), "2 minutes") + self.assertEqual(convert_minutes_in_human_readable_format(2.9), "2 minutes") + + def test_invalid_inputs(self): + """Test invalid inputs which should raise an error""" + with self.assertRaises(TypeError): + convert_minutes_in_human_readable_format("30 minutes") + with self.assertRaises(TypeError): + convert_minutes_in_human_readable_format(["30"]) + with self.assertRaises(TypeError): + convert_minutes_in_human_readable_format({"minutes": 30}) + with self.assertRaises(TypeError): + convert_minutes_in_human_readable_format(None) + + +class ConvertStrToDateTests(TestCase): + + def test_valid_date_with_hyphen_separator(self): + """Test valid date with hyphen separator works correctly""" + self.assertEqual(convert_str_to_date("2023-12-31"), datetime.date(2023, 12, 31)) + self.assertEqual(convert_str_to_date("2020-02-29"), datetime.date(2020, 2, 29)) # Leap year + self.assertEqual(convert_str_to_date("2021-02-28"), datetime.date(2021, 2, 28)) + + def test_valid_date_with_slash_separator(self): + """Test valid date with slash separator works correctly""" + self.assertEqual(convert_str_to_date("2021/01/01"), datetime.date(2021, 1, 1)) + self.assertEqual(convert_str_to_date("2023/12/31"), datetime.date(2023, 12, 31)) + self.assertEqual(convert_str_to_date("2023.12.31"), datetime.date(2023, 12, 31)) + + def test_invalid_date_formats(self): + """The date format "MM-DD-YYY" & "DD/MM/YYYY" are not supported, hence it should raise an error.""" + with self.assertRaises(ValueError): + convert_str_to_date("12-31-2023") + with self.assertRaises(ValueError): + convert_str_to_date("31/12/2023") + + def test_nonexistent_dates(self): + """Test nonexistent dates""" + with self.assertRaises(ValueError): + convert_str_to_date("2023-02-30") + with self.assertRaises(ValueError): + convert_str_to_date("2021-02-29") # Not a leap year + + def test_other_invalid_inputs(self): + """Test other invalid inputs""" + with self.assertRaises(ValueError): + convert_str_to_date("") + with self.assertRaises(ValueError): + convert_str_to_date("RandomString") + with self.assertRaises(ValueError): + convert_str_to_date("0000-00-00") + + +class ConvertStrToTimeTests(TestCase): + def test_12h_format_str_to_time(self): + """Test valid time strings""" + # These tests check if a 12-hour time format string converts correctly. + self.assertEqual(convert_str_to_time("10:00 AM"), datetime.time(10, 0)) + self.assertEqual(convert_str_to_time("12:00 PM"), datetime.time(12, 0)) + self.assertEqual(convert_str_to_time("01:30 PM"), datetime.time(13, 30)) + + def test_24h_format_str_to_time(self): + """Test if a 24-hour time format string converts correctly.""" + # These tests check if a 24-hour time format string converts correctly. + self.assertEqual(convert_str_to_time("10:00:00"), datetime.time(10, 0)) + self.assertEqual(convert_str_to_time("12:00:00"), datetime.time(12, 0)) + self.assertEqual(convert_str_to_time("13:30:00"), datetime.time(13, 30)) + + def test_case_insensitivity_and_whitespace(self): + """Test conversion handling of different case formats and white-space.""" + self.assertEqual(convert_str_to_time(" 12:00 am "), datetime.time(0, 0)) + self.assertEqual(convert_str_to_time("01:00 pM "), datetime.time(13, 0)) + self.assertEqual(convert_str_to_time(" 13:00:00 "), datetime.time(13, 0)) + + def test_invalid_time_strings(self): + """Test invalid time strings""" + with self.assertRaises(ValueError): + convert_str_to_time("") + with self.assertRaises(ValueError): + convert_str_to_time("13:00 PM") + with self.assertRaises(ValueError): + convert_str_to_time("25:00 AM") + with self.assertRaises(ValueError): + convert_str_to_time("25:00:00") + with self.assertRaises(ValueError): + convert_str_to_time("10:60 AM") + with self.assertRaises(ValueError): + convert_str_to_time("10:60:00") + with self.assertRaises(ValueError): + convert_str_to_time("Random String") + + +class GetAppointmentRequestEndTimeTests(TestCase): + def test_get_ar_end_time_with_valid_inputs(self): + """Test positive cases""" + self.assertEqual(get_ar_end_time("10:00:00", 60), datetime.time(11, 0)) + self.assertEqual(get_ar_end_time(datetime.time(10, 0), 120), datetime.time(12, 0)) + self.assertEqual(get_ar_end_time(datetime.time(10, 0), datetime.timedelta(hours=2)), datetime.time(12, 0)) + + def test_negative_duration(self): + """Test negative duration""" + with self.assertRaises(ValueError): + get_ar_end_time("10:00:00", -60) + + def test_invalid_start_time_format(self): + """Test invalid start time format""" + with self.assertRaises(ValueError): + get_ar_end_time("25:00:00", 60) + + def test_invalid_duration_format(self): + """Test invalid duration format""" + with self.assertRaises(TypeError): + get_ar_end_time("10:00:00", "60") + + def test_end_time_past_midnight(self): + """Test end time past midnight""" + # If the end time goes past midnight, it should wrap around to the next day, + # hence "23:30:00" + 60 minutes = "00:30:00". + self.assertEqual(get_ar_end_time("23:30:00", 60), datetime.time(0, 30)) + + def test_invalid_start_time_type(self): + """Test that an invalid start_time type raises a TypeError.""" + with self.assertRaises(TypeError): + get_ar_end_time([], 60) # Passing a list instead of a datetime.time object or string + with self.assertRaises(TypeError): + get_ar_end_time(12345, 30) # Passing an integer + with self.assertRaises(TypeError): + get_ar_end_time(None, 30) # Passing None + + +class TimeDifferenceTests(TestCase): + + def test_difference_with_time_objects(self): + """Test difference between two time objects""" + time1 = datetime.time(10, 0) + time2 = datetime.time(11, 0) + difference = time_difference(time1, time2) + self.assertEqual(difference, datetime.timedelta(hours=1)) + + def test_difference_with_datetime_objects(self): + """Test difference between two datetime objects""" + datetime1 = datetime.datetime(2023, 1, 1, 10, 0) + datetime2 = datetime.datetime(2023, 1, 1, 11, 0) + difference = time_difference(datetime1, datetime2) + self.assertEqual(difference, datetime.timedelta(hours=1)) + + def test_negative_difference_with_time_objects(self): + """Two time objects cannot have a negative difference""" + time1 = datetime.time(10, 0) + time2 = datetime.time(11, 0) + with self.assertRaises(ValueError): + time_difference(time2, time1) + + def test_negative_difference_with_datetime_objects(self): + """Two datetime objects cannot have a negative difference""" + datetime1 = datetime.datetime(2023, 1, 1, 10, 0) + datetime2 = datetime.datetime(2023, 1, 1, 11, 0) + with self.assertRaises(ValueError): + time_difference(datetime2, datetime1) + + def test_mismatched_input_types(self): + """Test that providing one 'datetime.time' and one datetime.datetime raises a ValueError.""" + time_obj = datetime.time(10, 0) + datetime_obj = datetime.datetime(2023, 1, 1, 11, 0) + + with self.assertRaises(ValueError) as context: + time_difference(time_obj, datetime_obj) + + self.assertEqual(str(context.exception), + "Both inputs should be of the same type, either datetime.time or datetime.datetime") + + # Test the reverse case as well for completeness + with self.assertRaises(ValueError) as context: + time_difference(datetime_obj, time_obj) + + self.assertEqual(str(context.exception), + "Both inputs should be of the same type, either datetime.time or datetime.datetime") + + +class CombineDateAndTimeTests(TestCase): + def test_valid_date_and_time(self): + """Test combining a valid date and time.""" + date = datetime.date(2023, 1, 1) + time = datetime.time(12, 30) + expected_datetime = datetime.datetime(2023, 1, 1, 12, 30) + result = combine_date_and_time(date, time) + self.assertEqual(result, expected_datetime) + + def test_combine_with_midnight(self): + """Test combining a date with a midnight time.""" + date = datetime.date(2023, 1, 1) + time = datetime.time(0, 0) + expected_datetime = datetime.datetime(2023, 1, 1, 0, 0) + result = combine_date_and_time(date, time) + self.assertEqual(result, expected_datetime) + + def test_combine_with_leap_year_date(self): + """Test combining a leap year date and time.""" + date = datetime.date(2024, 2, 29) + time = datetime.time(23, 59) + expected_datetime = datetime.datetime(2024, 2, 29, 23, 59) + result = combine_date_and_time(date, time) + self.assertEqual(result, expected_datetime) + + def test_combine_with_end_of_day(self): + """Test combining a date with 'end of day time'.""" + date = datetime.date(2023, 1, 1) + time = datetime.time(23, 59, 59) + expected_datetime = datetime.datetime(2023, 1, 1, 23, 59, 59) + result = combine_date_and_time(date, time) + self.assertEqual(result, expected_datetime) + + def test_combine_with_microseconds(self): + """Test combining a date and time with microseconds.""" + date = datetime.date(2023, 1, 1) + time = datetime.time(12, 30, 15, 123456) + expected_datetime = datetime.datetime(2023, 1, 1, 12, 30, 15, 123456) + result = combine_date_and_time(date, time) + self.assertEqual(result, expected_datetime) + + +class TimestampTests(TestCase): + @patch('appointment.utils.date_time.timezone.now') + def test_get_timestamp(self, mock_now): + """Test get_timestamp function""" + mock_datetime = Mock() + mock_datetime.timestamp.return_value = 1612345678.1234 # Sample timestamp with decimal + mock_now.return_value = mock_datetime + + self.assertEqual(get_timestamp(), "16123456781234") + + +class GeneralDateTimeTests(TestCase): + def test_get_current_year(self): + """Test get_current_year function""" + self.assertEqual(get_current_year(), datetime.datetime.now().year) + + def test_get_current_year_mocked(self): + """Test get_current_year function with a mocked year.""" + with patch('appointment.utils.date_time.datetime.datetime') as mock_date: + mock_date.now.return_value.year = 1999 # Setting year attribute of the mock object + self.assertEqual(get_current_year(), 1999) + + def test_get_weekday_num(self): + """Test get_weekday_num function with valid input""" + self.assertEqual(get_weekday_num("Monday"), 1) + self.assertEqual(get_weekday_num("Sunday"), 0) + + def test_invalid_get_weekday_num(self): + """Test get_weekday_num function with invalid input which should return -1""" + self.assertEqual(get_weekday_num("InvalidDay"), -1) diff --git a/appointment/tests/utils/test_db_helpers.py b/appointment/tests/utils/test_db_helpers.py new file mode 100644 index 0000000..fbb7b1b --- /dev/null +++ b/appointment/tests/utils/test_db_helpers.py @@ -0,0 +1,1095 @@ +# test_db_helpers.py +# Path: appointment/tests/utils/test_db_helpers.py + +import datetime +from unittest.mock import MagicMock, PropertyMock, patch + +from django.apps import apps +from django.conf import settings +from django.core.cache import cache +from django.core.exceptions import FieldDoesNotExist +from django.test import TestCase, override_settings +from django.test.client import RequestFactory +from django.urls import reverse +from django.utils import timezone +from django_q.models import Schedule + +from appointment.models import Config, DayOff, PaymentInfo +from appointment.tests.base.base_test import BaseTest +from appointment.tests.mixins.base_mixin import ConfigMixin +from appointment.utils.db_helpers import ( + Appointment, AppointmentRequest, AppointmentRescheduleHistory, Config, WorkingHours, calculate_slots, + calculate_staff_slots, can_appointment_be_rescheduled, cancel_existing_reminder, check_day_off_for_staff, + create_and_save_appointment, create_new_user, create_payment_info_and_get_url, day_off_exists_for_date_range, + exclude_booked_slots, exclude_pending_reschedules, generate_unique_username_from_email, get_absolute_url_, + get_all_appointments, get_all_staff_members, get_appointment_buffer_time, get_appointment_by_id, + get_appointment_finish_time, get_appointment_lead_time, get_appointment_slot_duration, + get_appointments_for_date_and_time, get_config, get_day_off_by_id, get_non_working_days_for_staff, + get_staff_member_appointment_list, get_staff_member_by_user_id, get_staff_member_from_user_id_or_logged_in, + get_times_from_config, get_user_by_email, get_user_model, get_website_name, get_weekday_num_from_date, + get_working_hours_by_id, get_working_hours_for_staff_and_day, is_working_day, parse_name, schedule_email_reminder, + staff_change_allowed_on_reschedule, update_appointment_reminder, username_in_user_model, working_hours_exist +) + + +class TestCalculateSlots(TestCase): + def setUp(self): + self.start_time = datetime.datetime(2023, 10, 8, 8, 0) # 8:00 AM + self.end_time = datetime.datetime(2023, 10, 8, 12, 0) # 12:00 PM + self.slot_duration = datetime.timedelta(hours=1) # 1 hour + # Buffer time should've been datetime.datetime.now() but for the purpose of the tests, we'll use a fixed time. + self.buffer_time = datetime.datetime(2023, 10, 8, 8, 0) + self.slot_duration + + def test_multiple_slots(self): + """Buffer time goes 1 hour after the start time, it should only return three slots. + Start time: 08:00 AM\n + End time: 12:00 AM\n + Buffer time: 09:00 AM\n + Slot duration: 1 hour\n + """ + + expected = [ + datetime.datetime(2023, 10, 8, 9, 0), + datetime.datetime(2023, 10, 8, 10, 0), + datetime.datetime(2023, 10, 8, 11, 0) + ] + result = calculate_slots(self.start_time, self.end_time, self.buffer_time, self.slot_duration) + self.assertEqual(result, expected) + + def test_buffer_after_end_time(self): + """Buffer time goes beyond the end time, it should not then return any slots. + Start time: 08:00 AM\n + End time: 09:00 AM\n + Buffer time: 10:00 AM\n + """ + end_time = datetime.datetime(2023, 10, 8, 9, 0) + buffer_time = datetime.datetime(2023, 10, 8, 10, 0) + + expected = [] + result = calculate_slots(self.start_time, end_time, buffer_time, self.slot_duration) + self.assertEqual(result, expected) + + def test_one_slot_available(self): + """Buffer time goes beyond the end time, it should not then return any slots. + Start time: 08:00 AM\n + End time: 09:00 AM\n + Buffer time: 10:00 AM\n + """ + end_time = datetime.datetime(2023, 10, 8, 9, 0) + buffer_time = datetime.datetime(2023, 10, 8, 7, 30) + slot_duration = datetime.timedelta(minutes=30) + + expected = [datetime.datetime(2023, 10, 8, 8, 0), datetime.datetime(2023, 10, 8, 8, 30)] + result = calculate_slots(self.start_time, end_time, buffer_time, slot_duration) + self.assertEqual(result, expected) + + +class TestCalculateStaffSlots(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.slot_duration = datetime.timedelta(minutes=30) + # Not working today but tomorrow + self.date_not_working = datetime.date.today() + self.working_date = datetime.date.today() + datetime.timedelta(days=3) + weekday_num = get_weekday_num_from_date(self.working_date) + self.wh = WorkingHours.objects.create( + staff_member=self.staff_member1, + day_of_week=weekday_num, + start_time=datetime.time(9, 0), + end_time=datetime.time(17, 0) + ) + self.staff_member1.appointment_buffer_time = 25.0 + + @override_settings(DEBUG=True) + def tearDown(self): + self.wh.delete() + if Config.objects.exists(): + Config.objects.all().delete() + cache.clear() + super().tearDown() + + def test_calculate_slots_on_working_day_without_appointments(self): + slots = calculate_staff_slots(self.working_date, self.staff_member1) + # Slot duration is 30 minutes, so 8 working hours minus 25-minute buffer, divided by slot duration + expected_number_of_slots = int((8 * 60 - 25) / 30) + # 15 slots should be available instead of 16 because of the 25-minute buffer + self.assertEqual(len(slots), expected_number_of_slots) + + # Asserting the first slot starts at 9:30 AM because of the 25-minute buffer + self.assertEqual(slots[0].time(), datetime.time(9, 30)) + + # Asserting the last slot starts before the end time minus slot duration (16:30) + self.assertTrue((datetime.datetime.combine(self.working_date, slots[-1].time()) + + self.slot_duration).time() <= datetime.time(17, 0)) + + def test_calculate_slots_on_non_working_day(self): + """Test that no slots are returned on a day the staff member is not working.""" + slots = calculate_staff_slots(self.date_not_working, self.staff_member1) + self.assertEqual(slots, []) + + +class TestCheckDayOffForStaff(BaseTest): + def setUp(self): + super().setUp() # Call the parent class setup + # Specific setups for this test class + self.day_off1 = DayOff.objects.create(staff_member=self.staff_member1, start_date="2023-10-08", + end_date="2023-10-10") + self.day_off2 = DayOff.objects.create(staff_member=self.staff_member2, start_date="2023-10-05", + end_date="2023-10-05") + + def tearDown(self): + DayOff.objects.all().delete() + + def test_staff_member_has_day_off(self): + # Test for a date within the range of days off for staff_member1 + self.assertTrue(check_day_off_for_staff(self.staff_member1, "2023-10-09")) + + def test_staff_member_does_not_have_day_off(self): + # Test for a date outside the range of days off for staff_member1 + self.assertFalse(check_day_off_for_staff(self.staff_member1, "2023-10-11")) + + def test_another_staff_member_day_off(self): + # Test for a date within the range of days off for staff_member2 + self.assertTrue(check_day_off_for_staff(self.staff_member2, "2023-10-05")) + + def test_another_staff_member_no_day_off(self): + # Test for a date outside the range of days off for staff_member2 + self.assertFalse(check_day_off_for_staff(self.staff_member2, "2023-10-06")) + + +class TestCreateAndSaveAppointment(BaseTest): + + def setUp(self): + super().setUp() # Call the parent class setup + # Specific setups for this test class + self.ar = self.create_appt_request_for_sm1() + self.factory = RequestFactory() + self.request = self.factory.get('/') + + def tearDown(self): + Appointment.objects.all().delete() + AppointmentRequest.objects.all().delete() + + def test_create_and_save_appointment(self): + client_data = { + 'email': 'georges.s.hammond@django-appointment.com', + 'name': 'georges.hammond', + } + appointment_data = { + 'phone': '123456789', + 'want_reminder': True, + 'address': '123, Stargate Command, Cheyenne Mountain, Colorado, USA', + 'additional_info': 'Please bring a Zat gun.' + } + + appointment = create_and_save_appointment(self.ar, client_data, appointment_data, self.request) + + self.assertIsNotNone(appointment) + self.assertEqual(appointment.client.email, client_data['email']) + self.assertEqual(appointment.phone, appointment_data['phone']) + self.assertEqual(appointment.want_reminder, appointment_data['want_reminder']) + self.assertEqual(appointment.address, appointment_data['address']) + self.assertEqual(appointment.additional_info, appointment_data['additional_info']) + + +def get_mock_reverse(url_name, **kwargs): + """A mocked version of the reverse function.""" + if url_name == "app:view": + return f'/mocked-url/{kwargs["kwargs"]["object_id"]}/{kwargs["kwargs"]["id_request"]}/' + return reverse(url_name, **kwargs) + + +class ScheduleEmailReminderTest(BaseTest): + def setUp(self): + super().setUp() + self.factory = RequestFactory() + self.request = self.factory.get('/') + self.appointment = self.create_appt_for_sm1() + + def tearDown(self): + Appointment.objects.all().delete() + AppointmentRequest.objects.all().delete() + + def test_schedule_email_reminder_cluster_running(self): + with patch('appointment.settings.check_q_cluster', return_value=True), \ + patch('appointment.utils.db_helpers.schedule') as mock_schedule: + schedule_email_reminder(self.appointment, self.request) + mock_schedule.assert_called_once() + # Further assertions can be made here based on the arguments passed to schedule + + def test_schedule_email_reminder_cluster_not_running(self): + with patch('appointment.settings.check_q_cluster', return_value=False), \ + patch('appointment.utils.db_helpers.logger') as mock_logger: + schedule_email_reminder(self.appointment, self.request) + mock_logger.warning.assert_called_with( + "Django-Q cluster is not running. Email reminder will not be scheduled.") + + +class UpdateAppointmentReminderTest(BaseTest): + def setUp(self): + super().setUp() + self.factory = RequestFactory() + self.request = self.factory.get('/') + self.appointment = self.create_appt_for_sm1() + + def tearDown(self): + Appointment.objects.all().delete() + AppointmentRequest.objects.all().delete() + + def test_update_appointment_reminder_date_time_changed(self): + appointment = self.create_appt_for_sm1() + new_date = timezone.now().date() + timezone.timedelta(days=10) + new_start_time = timezone.now().time() + + with patch('appointment.utils.db_helpers.schedule_email_reminder') as mock_schedule_email_reminder, \ + patch('appointment.utils.db_helpers.cancel_existing_reminder') as mock_cancel_existing_reminder: + update_appointment_reminder(appointment, new_date, new_start_time, self.request, True) + mock_cancel_existing_reminder.assert_called_once_with(appointment.id_request) + mock_schedule_email_reminder.assert_called_once() + + def test_update_appointment_reminder_no_change(self): + appointment = self.create_appt_for_sm2() + # Use existing date and time + new_date = appointment.appointment_request.date + new_start_time = appointment.appointment_request.start_time + + with patch('appointment.utils.db_helpers.schedule_email_reminder') as mock_schedule_email_reminder, \ + patch('appointment.utils.db_helpers.cancel_existing_reminder') as mock_cancel_existing_reminder: + update_appointment_reminder(appointment, new_date, new_start_time, self.request, appointment.want_reminder) + mock_cancel_existing_reminder.assert_not_called() + mock_schedule_email_reminder.assert_not_called() + + @patch('appointment.utils.db_helpers.logger') # Adjust the import path as necessary + def test_reminder_not_scheduled_due_to_user_preference(self, mock_logger): + # Scenario where user does not want a reminder + want_reminder = False + new_date = timezone.now().date() + datetime.timedelta(days=1) + new_start_time = timezone.now().time() + + update_appointment_reminder(self.appointment, new_date, new_start_time, self.request, want_reminder) + + # Check that the logger.info was called with the expected message + mock_logger.info.assert_called_once_with( + f"Reminder for appointment {self.appointment.id} is not scheduled per user's preference or past datetime." + ) + + @patch('appointment.utils.db_helpers.logger') # Adjust the import path as necessary + def test_reminder_not_scheduled_due_to_past_datetime(self, mock_logger): + # Scenario where the new datetime is in the past + want_reminder = True + new_date = timezone.now().date() - datetime.timedelta(days=1) # Date in the past + new_start_time = timezone.now().time() + + update_appointment_reminder(self.appointment, new_date, new_start_time, self.request, want_reminder) + + # Check that the logger.info was called with the expected message + mock_logger.info.assert_called_once_with( + f"Reminder for appointment {self.appointment.id} is not scheduled per user's preference or past datetime." + ) + + +# Helper method for modifying service rescheduling settings +def modify_service_rescheduling(service, **kwargs): + for key, value in kwargs.items(): + setattr(service, key, value) + service.save() + + +class CanAppointmentBeRescheduledTests(BaseTest, ConfigMixin): + def setUp(self): + super().setUp() + self.appointment = self.create_appt_for_sm1() + + def tearDown(self): + Appointment.objects.all().delete() + AppointmentRescheduleHistory.objects.all().delete() + AppointmentRequest.objects.all().delete() + + @patch('appointment.models.Service.reschedule_limit', new_callable=PropertyMock) + @patch('appointment.models.Config.default_reschedule_limit', new=3) + def test_can_appointment_be_rescheduled(self, mock_reschedule_limit): + mock_reschedule_limit.return_value = 3 + self.assertTrue(can_appointment_be_rescheduled(self.appointment.appointment_request)) + + def test_appointment_cannot_be_rescheduled_due_to_service_limit(self): + modify_service_rescheduling(self.service1, allow_rescheduling=True, reschedule_limit=0) + self.assertFalse(can_appointment_be_rescheduled(self.appointment.appointment_request)) + + def test_rescheduling_allowed_exceeds_limit(self): + modify_service_rescheduling(self.service1, allow_rescheduling=True, reschedule_limit=3) + ar = self.create_appointment_request_with_histories(service=self.service1, count=4) + self.assertFalse(can_appointment_be_rescheduled(ar)) + + def test_rescheduling_with_default_limit(self): + ar = self.create_appointment_request_with_histories(service=self.service1, count=2, use_default_limit=True) + self.assertTrue(can_appointment_be_rescheduled(ar)) + self.create_appt_reschedule_for_sm1(appointment_request=ar) + self.assertFalse(can_appointment_be_rescheduled(ar)) + + # Helper method to create appointment request with rescheduled histories + def create_appointment_request_with_histories(self, service, count, use_default_limit=False): + ar = self.create_appointment_request_(service=service, staff_member=self.staff_member1) + for _ in range(count): + self.create_appt_reschedule_for_sm1(appointment_request=ar) + return ar + + +class StaffChangeAllowedOnRescheduleTests(TestCase): + def tearDown(self): + super().tearDown() + # Reset or delete the Config instance to ensure test isolation + Config.objects.all().delete() + + @patch('appointment.models.Config.objects.first') + def test_staff_change_allowed(self, mock_config_first): + # Mock the Config object to return True for allow_staff_change_on_reschedule + mock_config = MagicMock() + mock_config.allow_staff_change_on_reschedule = True + mock_config_first.return_value = mock_config + + # Call the function and assert that staff change is allowed + self.assertTrue(staff_change_allowed_on_reschedule()) + + @patch('appointment.models.Config.objects.first') + def test_staff_change_not_allowed(self, mock_config_first): + # Mock the Config object to return False for allow_staff_change_on_reschedule + mock_config = MagicMock() + mock_config.allow_staff_change_on_reschedule = False + mock_config_first.return_value = mock_config + + # Call the function and assert that staff change is not allowed + self.assertFalse(staff_change_allowed_on_reschedule()) + + +class CancelExistingReminderTest(BaseTest): + def test_cancel_existing_reminder(self): + appointment = self.create_appt_for_sm1() + Schedule.objects.create(func='appointment.tasks.send_email_reminder', name=f"reminder_{appointment.id_request}") + + self.assertEqual(Schedule.objects.count(), 1) + cancel_existing_reminder(appointment.id_request) + self.assertEqual(Schedule.objects.filter(name=f"reminder_{appointment.id_request}").count(), 0) + + +class TestCreatePaymentInfoAndGetUrl(BaseTest): + + def setUp(self): + super().setUp() # Call the parent class setup + # Specific setups for this test class + self.ar = self.create_appt_request_for_sm1() + self.appointment = self.create_appt_for_sm2(appointment_request=self.ar) + + def test_create_payment_info_and_get_url_string(self): + expected_url = "https://payment.com/1/1234567890" + with patch('appointment.utils.db_helpers.APPOINTMENT_PAYMENT_URL', expected_url): + payment_url = create_payment_info_and_get_url(self.appointment) + self.assertEqual(payment_url, expected_url) + + def test_create_payment_info_and_get_url_application(self): + expected_url = "app:view" + + with patch('appointment.utils.db_helpers.APPOINTMENT_PAYMENT_URL', expected_url): + with patch('appointment.utils.db_helpers.reverse', side_effect=get_mock_reverse): + self.assertEqual(PaymentInfo.objects.count(), 0) + + # Call the function to create PaymentInfo and get the URL + payment_url = create_payment_info_and_get_url(self.appointment) + + # Now, there should be one PaymentInfo object + self.assertEqual(PaymentInfo.objects.count(), 1) + + # Fetch the newly created PaymentInfo object + payment_info = PaymentInfo.objects.first() + + # Construct the expected mocked URL + expected_mocked_url = f'/mocked-url/{payment_info.id}/{payment_info.get_id_request()}/' + + # Assert that the appointment in the PaymentInfo object matches the appointment we provided + self.assertEqual(payment_info.appointment, self.appointment) + self.assertEqual(payment_url, expected_mocked_url) + + +class TestExcludeBookedSlots(BaseTest): + + def setUp(self): + super().setUp() + self.appointment = self.create_appt_for_sm1() + + # Sample slots for testing + self.today = datetime.date.today() + + self.slots = [ + datetime.datetime.combine(self.today, datetime.time(8, 0)), + datetime.datetime.combine(self.today, datetime.time(9, 0)), + datetime.datetime.combine(self.today, datetime.time(10, 0)), + datetime.datetime.combine(self.today, datetime.time(11, 0)), + datetime.datetime.combine(self.today, datetime.time(12, 0)) + ] + self.slot_duration = datetime.timedelta(hours=1) + + def test_no_appointments(self): + result = exclude_booked_slots([], self.slots, self.slot_duration) + self.assertEqual(result, self.slots) + + def test_appointment_not_intersecting_slots(self): + self.appointment.appointment_request.start_time = datetime.time(13, 30) + self.appointment.appointment_request.end_time = datetime.time(14, 30) + self.appointment.save() + + result = exclude_booked_slots([self.appointment], self.slots, self.slot_duration) + self.assertEqual(result, self.slots) + + def test_appointment_intersecting_single_slot(self): + self.appointment.appointment_request.start_time = datetime.time(8, 0) + self.appointment.appointment_request.end_time = datetime.time(9, 0) + self.appointment.save() + + result = exclude_booked_slots([self.appointment], self.slots, self.slot_duration) + expected = [ + datetime.datetime.combine(self.today, datetime.time(9, 0)), + datetime.datetime.combine(self.today, datetime.time(10, 0)), + datetime.datetime.combine(self.today, datetime.time(11, 0)), + datetime.datetime.combine(self.today, datetime.time(12, 0)) + ] + self.assertEqual(result, expected) + + def test_multiple_overlapping_appointments(self): + ar2 = self.create_appt_request_for_sm2(start_time=datetime.time(10, 30), + end_time=datetime.time(11, 30)) + appointment2 = self.create_appt_for_sm2(appointment_request=ar2) + appointment2.save() + result = exclude_booked_slots([self.appointment, appointment2], self.slots, self.slot_duration) + expected = [ + datetime.datetime.combine(self.today, datetime.time(8, 0)), + datetime.datetime.combine(self.today, datetime.time(12, 0)) + ] + self.assertEqual(result, expected) + + +class TestDayOffExistsForDateRange(BaseTest): + + def setUp(self): + super().setUp() + self.user = self.create_user_() + self.service = self.create_service_() + self.staff_member = self.create_staff_member_(user=self.user, service=self.service) + self.day_off1 = DayOff.objects.create(staff_member=self.staff_member, start_date="2023-10-08", + end_date="2023-10-10") + self.day_off2 = DayOff.objects.create(staff_member=self.staff_member, start_date="2023-10-15", + end_date="2023-10-17") + + def test_day_off_exists(self): + # Check for a date range that intersects with day_off1 + self.assertTrue(day_off_exists_for_date_range(self.staff_member, "2023-10-09", "2023-10-11")) + + def test_day_off_does_not_exist(self): + # Check for a date range that doesn't intersect with any day off + self.assertFalse(day_off_exists_for_date_range(self.staff_member, "2023-10-11", "2023-10-14")) + + def test_day_off_exists_but_excluded(self): + # Check for a date range that intersects with day_off1 but exclude day_off1 from the check using its ID + self.assertFalse( + day_off_exists_for_date_range(self.staff_member, "2023-10-09", "2023-10-11", + days_off_id=self.day_off1.id)) + + def test_day_off_exists_for_other_range(self): + # Check for a date range that intersects with day_off2 + self.assertTrue(day_off_exists_for_date_range(self.staff_member, "2023-10-16", "2023-10-18")) + + +class TestGetAllAppointments(BaseTest): + + def setUp(self): + super().setUp() + self.appointment1 = self.create_appt_for_sm1() + self.appointment2 = self.create_appt_for_sm2() + + def test_get_all_appointments(self): + appointments = get_all_appointments() + self.assertEqual(len(appointments), 2) + self.assertIn(self.appointment1, appointments) + self.assertIn(self.appointment2, appointments) + + +class TestGetAllStaffMembers(BaseTest): + + def setUp(self): + super().setUp() + + def test_get_all_staff_members(self): + staff_members = get_all_staff_members() + self.assertEqual(len(staff_members), 2) + self.assertIn(self.staff_member1, staff_members) + self.assertIn(self.staff_member2, staff_members) + + +class TestGetAppointmentByID(BaseTest): + + def setUp(self): + super().setUp() + self.appointment = self.create_appt_for_sm1() + + def test_existing_appointment(self): + """Test fetching an existing appointment.""" + fetched_appointment = get_appointment_by_id(self.appointment.id) + self.assertEqual(fetched_appointment, self.appointment) + + def test_non_existing_appointment(self): + """Test attempting to fetch a non-existing appointment.""" + non_existent_id = 9999 # Assume this ID doesn't exist in the test database + fetched_appointment = get_appointment_by_id(non_existent_id) + self.assertIsNone(fetched_appointment) + + +@patch('appointment.utils.db_helpers.APPOINTMENT_BUFFER_TIME', 60) +@patch('appointment.utils.db_helpers.APPOINTMENT_LEAD_TIME', '07:00:00') +@patch('appointment.utils.db_helpers.APPOINTMENT_FINISH_TIME', '15:00:00') +@patch('appointment.utils.db_helpers.APPOINTMENT_SLOT_DURATION', 30) +@patch('appointment.utils.db_helpers.APPOINTMENT_WEBSITE_NAME', "django-appointment-website") +class TestGetAppointmentConfigTimes(TestCase): + def tearDown(self): + super().tearDown() + # Reset or delete the Config instance to ensure test isolation + Config.objects.all().delete() + + def test_no_config_object(self): + """Test when there's no Config object in the database.""" + self.assertIsNone(Config.objects.first()) # Ensure no Config object exists + self.assertEqual(get_appointment_buffer_time(), 60) + self.assertEqual(get_appointment_lead_time(), '07:00:00') + self.assertEqual(get_appointment_finish_time(), '15:00:00') + self.assertEqual(get_appointment_slot_duration(), 30) + self.assertEqual(get_website_name(), "django-appointment-website") + + def test_config_object_no_time_set(self): + """Test with a Config object without 'slot_duration'; 'buffer', 'lead' and 'finish' time set.""" + Config.objects.create() + self.assertEqual(get_appointment_buffer_time(), 60) + self.assertEqual(get_appointment_lead_time(), '07:00:00') + self.assertEqual(get_appointment_finish_time(), '15:00:00') + self.assertEqual(get_appointment_slot_duration(), 30) + self.assertEqual(get_website_name(), "django-appointment-website") + + def test_config_object_with_finish_time(self): + """Test with a Config object with 'slot_duration'; 'buffer', 'lead' and 'finish' time set.""" + Config.objects.create(finish_time='17:00:00', lead_time='09:00:00', + appointment_buffer_time=60, slot_duration=30, website_name="config") + self.assertEqual(get_appointment_buffer_time(), 60) + self.assertEqual(get_appointment_lead_time().strftime('%H:%M:%S'), '09:00:00') + self.assertEqual(get_appointment_finish_time().strftime('%H:%M:%S'), '17:00:00') + self.assertEqual(get_appointment_slot_duration(), 30) + self.assertEqual(get_website_name(), "config") + + def test_config_not_set_but_constants_patched(self): + """Test with no Config object and patched constants.""" + self.assertEqual(get_appointment_buffer_time(), 60) + self.assertEqual(get_appointment_lead_time(), '07:00:00') + self.assertEqual(get_appointment_finish_time(), '15:00:00') + self.assertEqual(get_appointment_slot_duration(), 30) + self.assertEqual(get_website_name(), "django-appointment-website") + + +class TestGetAppointmentsForDateAndTime(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + + # Setting up some appointment requests and appointments for testing + self.date_sample = datetime.datetime.today() + + # Creating overlapping appointments for staff_member1 + self.client1 = self.users['client1'] + self.client2 = self.users['client2'] + ar1 = self.create_appt_request_for_sm1(start_time=datetime.time(9, 0), end_time=datetime.time(10, 0)) + self.appointment1 = self.create_appointment_(user=self.client1, appointment_request=ar1) + + ar2 = self.create_appt_request_for_sm1(start_time=datetime.time(10, 30), end_time=datetime.time(11, 30)) + self.appointment2 = self.create_appointment_(user=self.client2, appointment_request=ar2) + + # Creating a non-overlapping appointment for staff_member1 + ar3 = self.create_appt_request_for_sm1(start_time=datetime.time(13, 0), end_time=datetime.time(14, 0)) + self.appointment3 = self.create_appointment_(user=self.client1, appointment_request=ar3) + + def test_get_appointments_overlapping_time_range(self): + """Test retrieving appointments overlapping with a specific time range.""" + appointments = get_appointments_for_date_and_time(self.date_sample, datetime.time(10, 0), datetime.time(12, 0), + self.staff_member1) + self.assertEqual(appointments.count(), 2) + self.assertIn(self.appointment1, appointments) + self.assertIn(self.appointment2, appointments) + + def test_get_appointments_outside_time_range(self): + """Test retrieving appointments outside a specific time range.""" + appointments = get_appointments_for_date_and_time(self.date_sample, datetime.time(7, 0), datetime.time(8, 30), + self.staff_member1) + self.assertEqual(appointments.count(), 0) + + def test_get_appointments_for_different_date(self): + """Test retrieving appointments for a different date.""" + appointments = get_appointments_for_date_and_time(datetime.date(2023, 10, 11), datetime.time(9, 0), + datetime.time(12, 0), self.staff_member1) + self.assertEqual(appointments.count(), 0) + + def test_get_appointments_for_different_staff_member(self): + """Test retrieving appointments for a different staff member.""" + appointments = get_appointments_for_date_and_time(self.date_sample, datetime.time(9, 0), datetime.time(12, 0), + self.staff_member2) + self.assertEqual(appointments.count(), 0) + + +class TestGetConfig(TestCase): + + def setUp(self): + # Clear the cache at the start of each test to ensure a clean state + cache.clear() + + def tearDown(self): + super().tearDown() + # Reset or delete the Config instance to ensure test isolation + Config.objects.all().delete() + cache.clear() + + def test_no_config_in_cache_or_db(self): + """Test when there's no Config in cache or the database.""" + config = get_config() + self.assertIsNone(config) + + def test_config_in_db_not_in_cache(self): + """Test when there's a Config object in the database but not in the cache.""" + db_config = Config.objects.create(finish_time='17:00:00') + config = get_config() + self.assertEqual(config, db_config) + + def test_config_in_cache(self): + """Test when there's a Config object in the cache.""" + db_config = Config.objects.create(finish_time='17:00:00') + cache.set('config', db_config) + + # Clear the database to ensure it won't be accessed + Config.objects.all().delete() + + config = get_config() + self.assertEqual(config, db_config) + + +class TestGetDayOffById(BaseTest): # Assuming you have a BaseTest class with some initial setups + def setUp(self): + super().setUp() # Call the parent class setup + + # Assuming you have already set up some StaffMember objects in the BaseTest + self.day_off = DayOff.objects.create(staff_member=self.staff_member1, start_date="2023-10-08", + end_date="2023-10-10") + + def test_retrieve_existing_day_off(self): + """Test retrieving an existing DayOff object.""" + retrieved_day_off = get_day_off_by_id(self.day_off.id) + self.assertEqual(retrieved_day_off, self.day_off) + + def test_nonexistent_day_off_id(self): + """Test trying to retrieve a DayOff object using a non-existent ID.""" + nonexistent_id = self.day_off.id + 1 # Just to ensure a non-existent ID, you can use any logic that suits you + retrieved_day_off = get_day_off_by_id(nonexistent_id) + self.assertIsNone(retrieved_day_off) + + +class TestGetNonWorkingDaysForStaff(BaseTest): + def setUp(self): + super().setUp() # Call the parent class setup + + self.staff_member_with_working_days = self.staff_member1 + WorkingHours.objects.create(staff_member=self.staff_member_with_working_days, day_of_week=0, + start_time=datetime.time(8, 0), end_time=datetime.time(12, 0)) + WorkingHours.objects.create(staff_member=self.staff_member_with_working_days, day_of_week=2, + start_time=datetime.time(8, 0), end_time=datetime.time(12, 0)) + + self.staff_member_without_working_days = self.staff_member2 + + def test_retrieve_non_working_days_for_staff_with_some_working_days(self): + """Test retrieving non-working days for a StaffMember with some working days set.""" + non_working_days = get_non_working_days_for_staff(self.staff_member_with_working_days.id) + expected_days = [1, 3, 4, 5, 6] + self.assertListEqual(non_working_days, expected_days) + + def test_retrieve_non_working_days_for_staff_without_working_days(self): + """Test retrieving non-working days for a StaffMember with no working days set.""" + non_working_days = get_non_working_days_for_staff(self.staff_member_without_working_days.id) + expected_days = [0, 1, 2, 3, 4, 5, 6] + self.assertListEqual(non_working_days, expected_days) + + def test_nonexistent_staff_member_id(self): + """Test trying to retrieve non-working days using a non-existent StaffMember ID.""" + nonexistent_id = self.staff_member_with_working_days.id + 100 # Just to ensure a non-existent ID + non_working_days = get_non_working_days_for_staff(nonexistent_id) + self.assertListEqual(non_working_days, []) + + +class TestGetStaffMemberAppointmentList(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.client1 = self.users['client1'] + + # Creating appointments for each staff member. + self.appointment1_for_user1 = self.create_appt_for_sm1() + self.appointment2_for_user2 = self.create_appt_for_sm2() + + def test_retrieve_appointments_for_specific_staff_member(self): + """Test retrieving appointments for a specific StaffMember.""" + # Testing for staff_member1 + appointments_for_staff_member1 = get_staff_member_appointment_list(self.staff_member1) + self.assertIn(self.appointment1_for_user1, appointments_for_staff_member1) + self.assertNotIn(self.appointment2_for_user2, appointments_for_staff_member1) + + # Testing for staff_member2 + appointments_for_staff_member2 = get_staff_member_appointment_list(self.staff_member2) + self.assertIn(self.appointment2_for_user2, appointments_for_staff_member2) + self.assertNotIn(self.appointment1_for_user1, appointments_for_staff_member2) + + def test_retrieve_appointments_for_staff_member_with_no_appointments(self): + """Test retrieving appointments for a StaffMember with no appointments.""" + # Creating a new staff member with no appointments + staff_member_with_no_appointments = self.create_staff_member_(user=self.client1, service=self.service1) + appointments = get_staff_member_appointment_list(staff_member_with_no_appointments) + self.assertListEqual(list(appointments), []) + + +class TestGetWeekdayNumFromDate(TestCase): + def test_get_weekday_num_from_date(self): + """Test getting the weekday number from a date.""" + sample_dates = { + datetime.date(2023, 10, 9): 1, # Monday + datetime.date(2023, 10, 10): 2, # Tuesday + datetime.date(2023, 10, 11): 3, # Wednesday + datetime.date(2023, 10, 12): 4, # Thursday + datetime.date(2023, 10, 13): 5, # Friday + datetime.date(2023, 10, 14): 6, # Saturday + datetime.date(2023, 10, 15): 0, # Sunday + } + + for date, expected_weekday_num in sample_dates.items(): + self.assertEqual(get_weekday_num_from_date(date), expected_weekday_num) + + +class TestDBHelpers(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.user1 = self.users['staff1'] + self.user2 = self.users['staff2'] + + def test_get_staff_member_by_user_id(self): + """Test retrieving a StaffMember object using a user id works as expected.""" + staff = get_staff_member_by_user_id(self.user1.id) + self.assertIsNotNone(staff) + self.assertEqual(staff, self.staff_member1) + + # Test for a non-existent user id + staff = get_staff_member_by_user_id(99999) + self.assertIsNone(staff) + + def test_get_staff_member_from_user_id_or_logged_in(self): + """Test retrieving a StaffMember object using a user id or the logged-in user works as expected.""" + staff = get_staff_member_from_user_id_or_logged_in(self.user1) + self.assertIsNotNone(staff) + self.assertEqual(staff, self.staff_member1) + + staff = get_staff_member_from_user_id_or_logged_in(self.user1, self.user2.id) + self.assertIsNotNone(staff) + self.assertEqual(staff, self.staff_member2) + + # Test for a non-existent user id + staff = get_staff_member_from_user_id_or_logged_in(self.user1, 99999) + self.assertIsNone(staff) + + def test_get_user_model(self): + """Test retrieving the user model works as expected.""" + user_model = get_user_model() + user_model_in_settings = apps.get_model(settings.AUTH_USER_MODEL) + self.assertEqual(user_model, user_model_in_settings) + + def test_get_user_by_email(self): + """Retrieve a user by email.""" + user = get_user_by_email("daniel.jackson@django-appointment.com") + self.assertIsNotNone(user) + self.assertEqual(user, self.user1) + + # Test for a non-existent email + user = get_user_by_email("nonexistent@django-appointment.com") + self.assertIsNone(user) + + +class TestWorkingHoursFunctions(BaseTest): + def setUp(self): + super().setUp() + self.working_hours = WorkingHours.objects.create( + staff_member=self.staff_member1, + day_of_week=1, # Monday + start_time=datetime.time(9, 0), + end_time=datetime.time(17, 0) + ) + + def test_get_working_hours_by_id(self): + """Test retrieving a WorkingHours object by ID.""" + working_hours = get_working_hours_by_id(self.working_hours.id) + self.assertEqual(working_hours, self.working_hours) + + # Non-existent ID + working_hours = get_working_hours_by_id(99999) + self.assertIsNone(working_hours) + + def test_get_working_hours_for_staff_and_day(self): + """Test retrieving WorkingHours for a specific staff member and day.""" + # With set WorkingHours + result = get_working_hours_for_staff_and_day(self.staff_member1, 1) + self.assertEqual(result['start_time'], datetime.time(9, 0)) + self.assertEqual(result['end_time'], datetime.time(17, 0)) + + # Without set WorkingHours but with staff member's default times + self.staff_member1.lead_time = datetime.time(8, 0) + self.staff_member1.finish_time = datetime.time(18, 0) + self.staff_member1.save() + result = get_working_hours_for_staff_and_day(self.staff_member1, 2) # Tuesday + self.assertEqual(result['start_time'], datetime.time(8, 0)) + self.assertEqual(result['end_time'], datetime.time(18, 0)) + + def test_is_working_day(self): + """is_working_day() should return True if there are WorkingHours for the staff member and day, + False otherwise.""" + self.assertTrue(is_working_day(self.staff_member1, 1)) # Monday + self.assertFalse(is_working_day(self.staff_member1, 2)) # Tuesday + + def test_working_hours_exist(self): + """working_hours_exist() should return True if there are WorkingHours for the staff member and day, + False otherwise.""" + self.assertTrue(working_hours_exist(1, self.staff_member1)) # Monday + self.assertFalse(working_hours_exist(2, self.staff_member1)) # Tuesday + + +@patch('appointment.utils.db_helpers.APPOINTMENT_LEAD_TIME', (7, 0)) +@patch('appointment.utils.db_helpers.APPOINTMENT_FINISH_TIME', (15, 0)) +@patch('appointment.utils.db_helpers.APPOINTMENT_SLOT_DURATION', 30) +@patch('appointment.utils.db_helpers.APPOINTMENT_BUFFER_TIME', 60) +class TestGetTimesFromConfig(TestCase): + def setUp(self): + self.sample_date = datetime.date(2023, 10, 9) + cache.clear() + + def tearDown(self): + super().tearDown() + # Reset or delete the Config instance to ensure test isolation + Config.objects.all().delete() + + def test_times_from_config_object(self): + """Test retrieving times from a Config object.""" + # Create a Config object with custom values + Config.objects.create( + lead_time=datetime.time(9, 0), + finish_time=datetime.time(17, 0), + slot_duration=45, + appointment_buffer_time=90 + ) + + start_time, end_time, slot_duration, buff_time = get_times_from_config(self.sample_date) + + # Assert times from 'Config' object + self.assertEqual(start_time, datetime.datetime(2023, 10, 9, 9, 0)) + self.assertEqual(end_time, datetime.datetime(2023, 10, 9, 17, 0)) + self.assertEqual(slot_duration, datetime.timedelta(minutes=45)) + self.assertEqual(buff_time, datetime.timedelta(minutes=90)) + + def test_times_from_default_settings(self): + """Test retrieving times from default settings.""" + # Ensure no Config object exists + Config.objects.all().delete() + + start_time, end_time, slot_duration, buff_time = get_times_from_config(self.sample_date) + + # Assert times from default settings + self.assertEqual(start_time, datetime.datetime(2023, 10, 9, 7, 0)) + self.assertEqual(end_time, datetime.datetime(2023, 10, 9, 15, 0)) + self.assertEqual(slot_duration, datetime.timedelta(minutes=30)) + self.assertEqual(buff_time, datetime.timedelta(minutes=60)) + + +class CreateNewUserTest(TestCase): + def test_create_new_user_unique_username(self): + """Test creating a new user with a unique username.""" + client_data = {'name': 'Cameron Mitchell', 'email': 'cameron.mitchell@django-appointment.com'} + user = create_new_user(client_data) + self.assertEqual(user.username, 'cameron.mitchell') + self.assertEqual(user.first_name, 'Cameron') + self.assertEqual(user.email, 'cameron.mitchell@django-appointment.com') + + def test_create_new_user_duplicate_username(self): + """Test creating a new user with a duplicate username.""" + client_data1 = {'name': 'Martouf of Malkshur', 'email': 'the.malkshur@django-appointment.com'} + user1 = create_new_user(client_data1) + self.assertEqual(user1.username, 'the.malkshur') + + client_data2 = {'name': 'Jolinar of Malkshur', 'email': 'the.malkshur@django-appointment.com'} + user2 = create_new_user(client_data2) + self.assertEqual(user2.username, 'the.malkshur01') # Suffix added + + client_data3 = {'name': 'Lantash of Malkshur', 'email': 'the.malkshur@django-appointment.com'} + user3 = create_new_user(client_data3) + self.assertEqual(user3.username, 'the.malkshur02') # Next suffix + + def test_generate_unique_username(self): + """Test if generate_unique_username_from_email function generates unique usernames.""" + email = 'jacob.carter@django-appointment.com' + username = generate_unique_username_from_email(email) + self.assertEqual(username, 'jacob.carter') + + # Assuming we have a user with the same username + CLIENT_MODEL = get_user_model() + CLIENT_MODEL.objects.create_user(username='jacob.carter', email=email) + new_username = generate_unique_username_from_email(email) + self.assertEqual(new_username, 'jacob.carter01') + + def test_parse_name(self): + """Test if parse_name function splits names correctly.""" + name = "Garshaw of Belote" + first_name, last_name = parse_name(name) + self.assertEqual(first_name, 'Garshaw') + self.assertEqual(last_name, 'of Belote') + + def test_create_new_user_check_password(self): + """Test creating a new user with a password.""" + client_data = {'name': 'Harry Maybourne', 'email': 'harry.maybourne@django-appointment.com'} + user = create_new_user(client_data) + # Check that no password has been set + self.assertFalse(user.has_usable_password()) + + +class UsernameInUserModelTests(TestCase): + + @patch('django.contrib.auth.models.User._meta.get_field') + def test_username_field_exists(self, mock_get_field): + """ + Test that `username_in_user_model` returns True when the 'username' field exists. + """ + mock_get_field.return_value = True # Mocking as if 'username' field exists + self.assertTrue(username_in_user_model()) + + @patch('django.contrib.auth.models.User._meta.get_field') + def test_username_field_does_not_exist(self, mock_get_field): + """ + Test that `username_in_user_model` returns False when the 'username' field does not exist. + """ + mock_get_field.side_effect = FieldDoesNotExist # Simulating 'username' field does not exist + self.assertFalse(username_in_user_model()) + + +class ExcludePendingReschedulesTests(BaseTest): + + def setUp(self): + super().setUp() + self.date = timezone.now().date() + datetime.timedelta(minutes=5) + self.start_time = (timezone.now() - datetime.timedelta(minutes=4)).time() + self.end_time = (timezone.now() + datetime.timedelta(minutes=1)).time() + + self.slots = [ + datetime.datetime.combine(self.date, self.start_time), + datetime.datetime.combine(self.date, self.end_time) + ] + + def test_exclude_no_pending_reschedules(self): + """Slots should remain unchanged if there are no pending rescheduling.""" + filtered_slots = exclude_pending_reschedules(self.slots, self.staff_member1, self.date) + self.assertEqual(len(filtered_slots), len(self.slots)) + + def test_exclude_with_pending_reschedules_outside_last_5_minutes(self): + """Slots should remain unchanged if pending reschedules are outside the last 5 minutes.""" + appointment_request = self.create_appointment_request_(self.service1, self.staff_member1) + self.create_reschedule_history_( + appointment_request, + date_=self.date, + start_time=(timezone.now() - datetime.timedelta(minutes=10)).time(), + end_time=(timezone.now() - datetime.timedelta(minutes=5)).time(), + staff_member=self.staff_member1, + reason_for_rescheduling="Scheduling conflict" + ) + filtered_slots = exclude_pending_reschedules(self.slots, self.staff_member1, self.date) + self.assertEqual(len(filtered_slots), len(self.slots)) + + def test_exclude_with_pending_reschedules_within_last_5_minutes(self): + """Slots overlapping with pending rescheduling within the last 5 minutes should be excluded.""" + appointment_request = self.create_appointment_request_(self.service1, self.staff_member1) + reschedule_start_time = (timezone.now() - datetime.timedelta(minutes=4)).time() + reschedule_end_time = (timezone.now() + datetime.timedelta(minutes=1)).time() + self.create_reschedule_history_( + appointment_request, + date_=self.date, + start_time=reschedule_start_time, + end_time=reschedule_end_time, + staff_member=self.staff_member1, + reason_for_rescheduling="Client request" + ) + filtered_slots = exclude_pending_reschedules(self.slots, self.staff_member1, self.date) + self.assertEqual(len(filtered_slots), len(self.slots) - 1) # Assuming only one slot overlaps + + def test_exclude_with_non_pending_reschedules_within_last_5_minutes(self): + """Slots should remain unchanged if reschedules within the last 5 minutes are not pending.""" + appointment_request = self.create_appointment_request_(self.service1, self.staff_member1) + reschedule_start_time = (timezone.now() - datetime.timedelta(minutes=4)).time() + reschedule_end_time = (timezone.now() + datetime.timedelta(minutes=1)).time() + reschedule = self.create_reschedule_history_( + appointment_request, + date_=self.date, + start_time=reschedule_start_time, + end_time=reschedule_end_time, + staff_member=self.staff_member1, + reason_for_rescheduling="Urgent issue" + ) + reschedule.reschedule_status = 'confirmed' + reschedule.save() + filtered_slots = exclude_pending_reschedules(self.slots, self.staff_member1, self.date) + self.assertEqual(len(filtered_slots), len(self.slots)) + + +class GetAbsoluteUrlTests(TestCase): + + def setUp(self): + # Create a RequestFactory instance + self.factory = RequestFactory() + + def test_get_absolute_url_with_request(self): + # Create a request object using RequestFactory + request = self.factory.get('/some-path/') + relative_url = '/test-url/' + expected_url = request.build_absolute_uri(relative_url) + + # Call the function with the request object + result_url = get_absolute_url_(relative_url, request) + + # Assert the result matches the expected URL + self.assertEqual(result_url, expected_url) diff --git a/appointment/tests/utils/test_email_ops.py b/appointment/tests/utils/test_email_ops.py new file mode 100644 index 0000000..ce9b66b --- /dev/null +++ b/appointment/tests/utils/test_email_ops.py @@ -0,0 +1,231 @@ +from copy import deepcopy +from datetime import datetime +from unittest import mock +from unittest.mock import MagicMock, patch + +from django.test.client import RequestFactory +from django.utils import timezone +from django.utils.translation import gettext as _ + +from appointment.messages_ import thank_you_no_payment, thank_you_payment, thank_you_payment_plus_down +from appointment.models import AppointmentRescheduleHistory +from appointment.tests.base.base_test import BaseTest +from appointment.utils.email_ops import ( + get_thank_you_message, notify_admin_about_appointment, notify_admin_about_reschedule, + send_reschedule_confirmation_email, + send_reset_link_to_staff_member, send_thank_you_email, + send_verification_email +) + + +class SendResetLinkToStaffMemberTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.user = deepcopy(self.users['staff1']) + self.user.is_staff = True + self.user.save() + self.factory = RequestFactory() + self.request = self.factory.get('/') + self.email = 'daniel.jackson@django-appointment.com' + + @mock.patch('appointment.utils.email_ops.send_email') + @mock.patch('appointment.models.PasswordResetToken.create_token') + @mock.patch('appointment.utils.email_ops.get_absolute_url_') + @mock.patch('appointment.utils.email_ops.get_website_name') + def test_send_reset_link(self, mock_get_website_name, mock_get_absolute_url, mock_create_token, mock_send_email): + # Set up the token + mock_token = mock.Mock() + mock_token.token = "Colonel_Samantha_Carter_a_Tau_ri_Scientist" + mock_create_token.return_value = mock_token + + mock_get_absolute_url.return_value = f"http://gateroomserver/reset_password/{mock_token.token}" + mock_get_website_name.return_value = 'Gate Room Server' + + send_reset_link_to_staff_member(self.user, self.request, self.email) + + # Check send_email was called with correct parameters + mock_send_email.assert_called_once() + args, kwargs = mock_send_email.call_args + self.assertEqual(kwargs['recipient_list'], [self.email]) + self.assertIn('Gate Room Server', kwargs['message']) + self.assertIn('http://gateroomserver/reset_password', kwargs['message']) + self.assertIn('Colonel_Samantha_Carter_a_Tau_ri_Scientist', kwargs['message']) + + # Additional assertions to verify more parts of the message content + self.assertIn('Hello', kwargs['message']) + self.assertIn(self.user.first_name, kwargs['message']) + self.assertIn(str(datetime.now().year), kwargs['message']) + self.assertIn('No additional details provided.', kwargs['message']) + self.assertIn(self.user.username, kwargs['message']) + + +class GetThankYouMessageTests(BaseTest): + + def setUp(self): + super().setUp() + self.ar = MagicMock() + + def test_thank_you_no_payment(self): + with patch('appointment.utils.email_ops.APPOINTMENT_PAYMENT_URL', None): + message = get_thank_you_message(self.ar) + self.assertIn(thank_you_no_payment, message) + + def test_thank_you_payment_plus_down(self): + with patch('appointment.utils.email_ops.APPOINTMENT_PAYMENT_URL', "http://payment.url"): + self.ar.accepts_down_payment.return_value = True + message = get_thank_you_message(self.ar) + self.assertIn(thank_you_payment_plus_down, message) + + def test_thank_you_payment(self): + with patch('appointment.utils.email_ops.APPOINTMENT_PAYMENT_URL', "http://payment.url"): + self.ar.accepts_down_payment.return_value = False + message = get_thank_you_message(self.ar) + self.assertIn(thank_you_payment, message) + + +class SendThankYouEmailTests(BaseTest): + def setUp(self): + super().setUp() + self.factory = RequestFactory() + self.request = self.factory.get('/') + self.ar = self.create_appt_request_for_sm1() + self.email = "georges.s.hammond@django-appointment.com" + self.appointment_details = "Details about the appointment" + self.account_details = "Details about the account" + + @patch('appointment.utils.email_ops.send_email') + @patch('appointment.utils.email_ops.get_thank_you_message') + def test_send_thank_you_email(self, mock_get_thank_you_message, mock_send_email): + mock_get_thank_you_message.return_value = "Thank you message" + + send_thank_you_email(self.ar, self.users['client1'], self.request, self.email, self.appointment_details, + self.account_details) + + mock_send_email.assert_called_once() + args, kwargs = mock_send_email.call_args + self.assertIn(self.email, kwargs['recipient_list']) + self.assertIn("Thank you message", kwargs['context']['message_1']) + + +class NotifyAdminAboutAppointmentTests(BaseTest): + def setUp(self): + super().setUp() + self.appointment = self.create_appt_for_sm1() + self.client_name = "Oma Desala" + + @patch('appointment.utils.email_ops.notify_admin') + @patch('appointment.utils.email_ops.send_email') + def test_notify_admin_about_appointment(self, mock_send_email, mock_notify_admin): + notify_admin_about_appointment(self.appointment, self.client_name) + mock_notify_admin.assert_called_once() + mock_send_email.assert_called_once() + + +class SendVerificationEmailTests(BaseTest): + def setUp(self): + super().setUp() + self.appointment = self.create_appt_for_sm1() + self.email = "richard.woolsey@django-appointment.com" + + @patch('appointment.utils.email_ops.send_email') + @patch('appointment.models.EmailVerificationCode.generate_code', return_value="123456") + def test_send_verification_email(self, mock_generate_code, mock_send_email): + user = MagicMock() + + send_verification_email(user, self.email) + + mock_send_email.assert_called_once_with( + recipient_list=[self.email], + subject=_("Email Verification"), + message=mock.ANY + ) + self.assertIn("123456", mock_send_email.call_args[1]['message']) + + +class SendRescheduleConfirmationEmailTests(BaseTest): + def setUp(self): + super().setUp() + self.appointment_request = self.create_appt_request_for_sm1() + self.reschedule_history = AppointmentRescheduleHistory.objects.create( + appointment_request=self.appointment_request, + date=self.appointment_request.date + timezone.timedelta(days=1), + start_time=self.appointment_request.start_time, + end_time=self.appointment_request.end_time, + staff_member=self.staff_member1, + reason_for_rescheduling="Had to reschedule because I got stuck in a time loop. Again" + ) + self.first_name = "Jack" + self.email = "jack.oneill@django-appointment.com" + + @mock.patch('appointment.utils.email_ops.get_absolute_url_') + @mock.patch('appointment.utils.email_ops.send_email') + def test_send_reschedule_confirmation_email(self, mock_send_email, mock_get_absolute_url): + request = mock.MagicMock() + mock_get_absolute_url.return_value = "http://gateroomserver/confirmation_link" + + send_reschedule_confirmation_email(request, self.reschedule_history, self.appointment_request, self.first_name, + self.email) + + # Check if `send_email` was called correctly + mock_send_email.assert_called_once() + call_args, call_kwargs = mock_send_email.call_args + + self.assertEqual(call_kwargs['recipient_list'], [self.email]) + self.assertEqual(call_kwargs['subject'], _("Confirm Your Appointment Rescheduling")) + self.assertIn('reschedule_date', call_kwargs['context']) + self.assertIn('confirmation_link', call_kwargs['context']) + self.assertEqual(call_kwargs['context']['confirmation_link'], "http://gateroomserver/confirmation_link") + + +class NotifyAdminAboutRescheduleTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.appointment_request = self.create_appt_request_for_sm1() + self.reschedule_history = AppointmentRescheduleHistory.objects.create( + appointment_request=self.appointment_request, + date=self.appointment_request.date + timezone.timedelta(days=1), + start_time=self.appointment_request.start_time, + end_time=self.appointment_request.end_time, + staff_member=self.staff_member1, + reason_for_rescheduling="Captured by Anubis" + ) + self.client_name = "Jonas Quinn" + + @patch('appointment.utils.email_ops.notify_admin') + @patch('appointment.utils.email_ops.send_email') + @patch('appointment.utils.email_ops.get_website_name', return_value="Stargate Command") + @patch('appointment.utils.email_ops.convert_24_hour_time_to_12_hour_time', + side_effect=lambda x: x.strftime("%I:%M %p")) + def test_notify_admin_about_reschedule(self, mock_convert_time, mock_get_website_name, mock_send_email, + mock_notify_admin): + notify_admin_about_reschedule(self.reschedule_history, self.appointment_request, self.client_name) + + # Check if notify_admin was called correctly + mock_notify_admin.assert_called_once() + notify_admin_args, notify_admin_kwargs = mock_notify_admin.call_args + self.assertIn(self.client_name, notify_admin_kwargs['subject']) + self.assertEqual(notify_admin_kwargs['context']['client_name'], self.client_name) + self.assertEqual(notify_admin_kwargs['context']['service_name'], self.appointment_request.service.name) + self.assertEqual(notify_admin_kwargs['context']['reason_for_rescheduling'], + self.reschedule_history.reason_for_rescheduling) + self.assertEqual(notify_admin_kwargs['context']['old_date'], + self.appointment_request.date.strftime("%A, %d %B %Y")) + self.assertEqual(notify_admin_kwargs['context']['reschedule_date'], + self.reschedule_history.date.strftime("%A, %d %B %Y")) + self.assertEqual(notify_admin_kwargs['context']['company'], "Stargate Command") diff --git a/appointment/tests/utils/test_json_context.py b/appointment/tests/utils/test_json_context.py new file mode 100644 index 0000000..5d522cc --- /dev/null +++ b/appointment/tests/utils/test_json_context.py @@ -0,0 +1,117 @@ +# test_json_context.py +# Path: appointment/tests/utils/test_json_context.py + +import json + +from django.test import RequestFactory + +from appointment.tests.base.base_test import BaseTest +from appointment.utils.json_context import ( + convert_appointment_to_json, get_generic_context, get_generic_context_with_extra, handle_unauthorized_response, + json_response +) + + +class ConvertAppointmentToJsonTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.factory = RequestFactory() + self.appointments = [self.create_appt_for_sm1()] + self.request = self.factory.get('/') + self.request.user = self.users['client1'] + + def test_convert_appointment_to_json(self): + """Test if an appointment can be converted to JSON.""" + data = convert_appointment_to_json(self.request, self.appointments) + self.assertIsInstance(data, list, "Data should be a list") + self.assertEqual(len(data), 1, "Data list should have one appointment") + self.assertIn("id", data[0], "Data should contain 'id' field") + + +class JsonResponseTests(BaseTest): + def test_json_response(self): + """Test if a JSON response can be created.""" + message = "Gate Room Under Attack" + response = json_response(message=message) + self.assertEqual(response.status_code, 200, "Response status should be 200") + content = json.loads(response.content.decode('utf-8')) + self.assertEqual(content['message'], message, "Response content should match the message") + + +class GetGenericContextTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.factory = RequestFactory() + self.user1 = self.users['client1'] + self.request = self.factory.get('/') + self.request.user = self.user1 + + def test_get_generic_context(self): + """Test if a generic context can be created.""" + context = get_generic_context(self.request) + self.assertEqual(context['user'], self.user1, "Context user should match the request user") + self.assertIn('BASE_TEMPLATE', context, "Context should contain 'BASE_TEMPLATE'") + self.assertIn('is_superuser', context, "Context should contain 'is_superuser'") + + +class GetGenericContextWithExtraTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.factory = RequestFactory() + self.user1 = self.users['client1'] + self.request = self.factory.get('/') + self.request.user = self.user1 + self.extra = {"key": "value"} + + def test_get_generic_context_with_extra(self): + """Test if a generic context with extra data can be created.""" + context = get_generic_context_with_extra(self.request, self.extra) + self.assertEqual(context['user'], self.user1, "Context user should match the request user") + self.assertEqual(context['key'], "value", "Context should include extra data") + self.assertIn('BASE_TEMPLATE', context, "Context should contain 'BASE_TEMPLATE'") + self.assertIn('is_superuser', context, "Context should contain 'is_superuser'") + + +class HandleUnauthorizedResponseTests(BaseTest): + def setUp(self): + super().setUp() + self.factory = RequestFactory() + self.message = "Unauthorized" + + def test_handle_unauthorized_response_json(self): + """Test if an unauthorized response can be created when the response type is JSON.""" + request = self.factory.get('/') + response = handle_unauthorized_response(request=request, message=self.message, response_type='json') + self.assertEqual(response.status_code, 403, "Response status should be 403") + content = json.loads(response.content.decode('utf-8')) + self.assertEqual(content['message'], self.message, "Response content should match the message") + + def test_handle_unauthorized_response_html(self): + """Test if an unauthorized response can be created when the response type is HTML.""" + request = self.factory.get('/app-admin/user-events/') + response = handle_unauthorized_response(request, self.message, 'html') + self.assertEqual(response.status_code, 403, "Response status should be 403") diff --git a/appointment/tests/utils/test_permissions.py b/appointment/tests/utils/test_permissions.py new file mode 100644 index 0000000..e6dbef6 --- /dev/null +++ b/appointment/tests/utils/test_permissions.py @@ -0,0 +1,132 @@ +# test_permissions.py +# Path: appointment/tests/utils/test_permissions.py + +import datetime +from unittest import mock + +from appointment.tests.base.base_test import BaseTest +from appointment.utils.db_helpers import WorkingHours +from appointment.utils.permissions import ( + check_entity_ownership, check_extensive_permissions, check_permissions, has_permission_to_delete_appointment +) + + +class CheckEntityOwnershipTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.superuser = self.users['superuser'] + self.superuser.is_superuser = True + self.superuser.save() + self.user1 = self.users['staff1'] + self.user2 = self.users['staff2'] + self.entity_owned_by_user1 = WorkingHours.objects.create( + staff_member=self.staff_member1, day_of_week=0, start_time=datetime.time(8, 0), + end_time=datetime.time(12, 0)) + + def test_check_entity_ownership(self): + """Test if ownership of an entity can be checked.""" + # User is the owner + self.assertTrue(check_entity_ownership(self.user1, self.entity_owned_by_user1)) + # Superuser but not owner + self.assertTrue(check_entity_ownership(self.superuser, self.entity_owned_by_user1)) + # Neither owner nor superuser + self.assertFalse(check_entity_ownership(self.user2, self.entity_owned_by_user1)) + + +class CheckExtensivePermissionsTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.superuser = self.users['superuser'] + self.superuser.is_superuser = True + self.superuser.save() + self.user1 = self.users['staff1'] + self.user2 = self.users['staff2'] + self.entity_owned_by_user1 = WorkingHours.objects.create( + staff_member=self.staff_member1, day_of_week=0, start_time=datetime.time(8, 0), + end_time=datetime.time(12, 0)) + + def test_check_extensive_permissions(self): + """Test if extensive permissions can be checked.""" + # staff_user_id matches and user owns entity + self.assertTrue(check_extensive_permissions(self.user1.pk, self.user1, self.entity_owned_by_user1)) + # staff_user_id matches but user doesn't own entity + self.assertFalse(check_extensive_permissions(self.user2.pk, self.user2, self.entity_owned_by_user1)) + # staff_user_id doesn't match but user is superuser + self.assertTrue(check_extensive_permissions(None, self.superuser, self.entity_owned_by_user1)) + # staff_user_id matches and no entity provided + self.assertTrue(check_extensive_permissions(self.user1.pk, self.user1, None)) + # Neither staff_user_id matches nor superuser + self.assertFalse(check_extensive_permissions(None, self.user2, self.entity_owned_by_user1)) + + +class CheckPermissionsTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.superuser = self.users['superuser'] + self.superuser.is_superuser = True + self.superuser.save() + self.user1 = self.users['staff1'] + self.user2 = self.users['staff2'] + + def test_check_permissions(self): + """Test if permissions can be checked.""" + # staff_user_id matches + self.assertTrue(check_permissions(self.user1.pk, self.user1)) + # staff_user_id doesn't match but user is superuser + self.assertTrue(check_permissions(None, self.superuser)) + # Neither staff_user_id matches nor superuser + self.assertFalse(check_permissions(None, self.user2)) + + +class HasPermissionToDeleteAppointmentTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.superuser = self.users['superuser'] + self.superuser.is_superuser = True + self.superuser.save() + self.user1 = self.users['staff1'] + self.user2 = self.users['staff2'] + self.appointment = self.create_appt_for_sm1() + + def test_has_permission_to_delete_appointment(self): + """Test if the user has permission to delete the given appointment.""" + # Mock get_staff_member to return the staff member associated with the appointment + with mock.patch('appointment.models.Appointment.get_staff_member', return_value=self.staff_member1): + # User is the staff member associated with the appointment + self.assertTrue(has_permission_to_delete_appointment(self.user1, self.appointment)) + # User is a superuser + self.assertTrue(has_permission_to_delete_appointment(self.superuser, self.appointment)) + # User is neither a staff member nor a superuser + self.assertFalse(has_permission_to_delete_appointment(self.user2, self.appointment)) diff --git a/appointment/tests/utils/test_session.py b/appointment/tests/utils/test_session.py new file mode 100644 index 0000000..7bdb201 --- /dev/null +++ b/appointment/tests/utils/test_session.py @@ -0,0 +1,163 @@ +# test_session.py +# Path: appointment/tests/utils/test_session.py + +from unittest import mock + +from django.contrib.messages.middleware import MessageMiddleware +from django.contrib.sessions.middleware import SessionMiddleware +from django.test import Client, override_settings +from django.test.client import RequestFactory + +from appointment.tests.base.base_test import BaseTest +from appointment.utils.session import ( + get_appointment_data_from_session, handle_email_change, handle_existing_email +) + + +@override_settings(EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend') +class HandleExistingEmailTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.ar = self.create_appt_request_for_sm1() + self.client = Client() + self.factory = RequestFactory() + + # Setup request object + self.request = self.factory.post('/') + self.hammond = self.users['client1'] + self.request.user = self.hammond + + # Setup session for the request + session_middleware = SessionMiddleware(lambda req: None) + session_middleware.process_request(self.request) + self.request.session.save() + + # Setup messages for the request + messages_middleware = MessageMiddleware(lambda req: None) + messages_middleware.process_request(self.request) + self.request.session.save() + + @mock.patch('appointment.utils.session.get_user_by_email') + @mock.patch('appointment.utils.session.send_verification_email') + def test_handle_existing_email(self, mock_send_verification_email, mock_get_user_by_email): + """Test if an existing email can be handled.""" + client_data = { + 'email': 'georges.s.hammond@django-appointment.com', + 'name': 'georges.hammond', + } + appointment_data = { + 'phone': '123456789', + 'want_reminder': True, + 'address': '123, Stargate Command, Cheyenne Mountain, Colorado, USA', + 'additional_info': 'Please bring a Zat gun.' + } + + response = handle_existing_email(self.request, client_data, appointment_data, self.ar.id, self.ar.id_request) + + # Assert session data + session = self.request.session + self.assertEqual(session['email'], client_data['email']) + self.assertEqual(session['phone'], appointment_data['phone']) + self.assertTrue(session['want_reminder']) + self.assertEqual(session['address'], appointment_data['address']) + self.assertEqual(session['additional_info'], appointment_data['additional_info']) + + # Assert redirect + self.assertEqual(response.status_code, 302) + mock_send_verification_email.assert_called_once_with(user=mock_get_user_by_email.return_value, + email=client_data['email']) + mock_get_user_by_email.assert_called_once_with(client_data['email']) + + +class HandleEmailChangeTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.client = Client() + self.factory = RequestFactory() + + # Setup request object + self.request = self.factory.post('/') + self.hammond = self.users['client1'] + self.request.user = self.hammond + + # Setup session for the request + session_middleware = SessionMiddleware(lambda req: None) + session_middleware.process_request(self.request) + self.request.session.save() + + @mock.patch('appointment.utils.session.send_verification_email') + def test_handle_email_change(self, mock_send_verification_email): + """Test if an email change can be handled.""" + new_email = 'georges.hammond@django-appointment.com' + + response = handle_email_change(self.request, self.hammond, new_email) + + # Assert session data + session = self.request.session + self.assertEqual(session['email'], new_email) + self.assertEqual(session['old_email'], self.hammond.email) + + # Assert redirect + self.assertEqual(response.status_code, 302) + mock_send_verification_email.assert_called_once_with(user=self.hammond, email=new_email) + + +class GetAppointmentDataFromSessionTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + self.client = Client() + self.factory = RequestFactory() + + # Setup request object + self.request = self.factory.post('/') + self.hammond = self.users['client1'] + self.request.user = self.hammond + + # Setup session for the request + session_middleware = SessionMiddleware(lambda req: None) + session_middleware.process_request(self.request) + self.request.session.save() + + def test_get_appointment_data_from_session(self): + """Test if appointment data can be retrieved from the session.""" + # Populate session with test data + session_data = { + 'phone': '+1234567890', + 'want_reminder': 'on', + 'address': '123 Main St, City, Country', + 'additional_info': 'Some additional info' + } + for key, value in session_data.items(): + self.request.session[key] = value + self.request.session.save() + + # Retrieve data using the function + appointment_data = get_appointment_data_from_session(self.request) + self.assertEqual(str(appointment_data['phone']), session_data['phone']) + self.assertTrue(appointment_data['want_reminder']) + self.assertEqual(appointment_data['address'], session_data['address']) + self.assertEqual(appointment_data['additional_info'], session_data['additional_info']) diff --git a/appointment/tests/utils/test_staff_member_time.py b/appointment/tests/utils/test_staff_member_time.py new file mode 100644 index 0000000..3028fbb --- /dev/null +++ b/appointment/tests/utils/test_staff_member_time.py @@ -0,0 +1,118 @@ +import datetime +from unittest.mock import patch + +from django.core.cache import cache +from django.test import override_settings + +from appointment.models import StaffMember +from appointment.tests.base.base_test import BaseTest +from appointment.utils.db_helpers import Config, WorkingHours, get_staff_member_buffer_time, \ + get_staff_member_end_time, get_staff_member_slot_duration, get_staff_member_start_time + + +class BaseStaffMemberTimeTestSetup(BaseTest): + """Base setup class for staff member time function tests.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def setUp(self): + super().setUp() + + # Set staff member-specific settings + self.staff_member1.slot_duration = 15 + self.staff_member1.lead_time = datetime.time(8, 30) + self.staff_member1.finish_time = datetime.time(18, 0) + self.staff_member1.appointment_buffer_time = 45 + self.staff_member1.save() + + # Setting WorkingHours for staff_member1 for Monday + self.wh = WorkingHours.objects.create( + staff_member=self.staff_member1, + day_of_week=1, + start_time=datetime.time(9, 0), + end_time=datetime.time(17, 0) + ) + + @override_settings(DEBUG=True) + def tearDown(self): + super().tearDown() + StaffMember.objects.all().delete() + if Config.objects.exists(): + Config.objects.all().delete() + WorkingHours.objects.all().delete() + cache.clear() + + +@patch('appointment.utils.db_helpers.APPOINTMENT_BUFFER_TIME', 59) +class TestGetStaffMemberBufferTime(BaseStaffMemberTimeTestSetup): + """Test suite for get_staff_member_buffer_time function.""" + + def test_staff_member_buffer_time_with_global_setting(self): + """Test buffer time when staff member-specific setting is None.""" + self.staff_member1.appointment_buffer_time = None + self.staff_member1.save() + buffer_time = get_staff_member_buffer_time(self.staff_member1, datetime.date(2023, 10, 9)) + self.assertEqual(buffer_time, 59) # Global setting + + def test_staff_member_buffer_time_with_staff_member_setting(self): + """Test buffer time using staff member-specific setting.""" + buffer_time = get_staff_member_buffer_time(self.staff_member1, datetime.date(2023, 10, 9)) + self.assertEqual(buffer_time, 45) # Staff member specific setting + + def test_staff_member_buffer_time_with_working_hours_conflict(self): + """Test buffer time when it conflicts with WorkingHours.""" + self.staff_member1.appointment_buffer_time = 120 # Set a buffer time greater than WorkingHours start time + self.staff_member1.save() + buffer_time = get_staff_member_buffer_time(self.staff_member1, datetime.date(2023, 10, 9)) + self.assertEqual(buffer_time, 120) # Should still use staff member-specific setting even if it causes conflict + + +@patch('appointment.utils.db_helpers.APPOINTMENT_SLOT_DURATION', 31) +class TestGetStaffMemberSlotDuration(BaseStaffMemberTimeTestSetup): + """Test suite for get_staff_member_slot_duration function.""" + + def test_staff_member_slot_duration_with_global_setting(self): + """Test slot duration when staff member-specific setting is None.""" + self.staff_member1.slot_duration = None + self.staff_member1.save() + slot_duration = get_staff_member_slot_duration(self.staff_member1, datetime.date(2023, 10, 9)) + self.assertEqual(slot_duration, 31) # Global setting + + def test_staff_member_slot_duration_with_staff_member_setting(self): + """Test slot duration using staff member-specific setting.""" + slot_duration = get_staff_member_slot_duration(self.staff_member1, datetime.date(2023, 10, 9)) + self.assertEqual(slot_duration, 15) # Staff member specific setting + + +class TestGetStaffMemberStartTime(BaseStaffMemberTimeTestSetup): + """Test suite for get_staff_member_start_time function.""" + + def test_staff_member_start_time(self): + """Test start time based on WorkingHours.""" + start_time = get_staff_member_start_time(self.staff_member1, datetime.date(2023, 10, 9)) + self.assertEqual(start_time, datetime.time(9, 0)) + + def test_staff_member_start_time_with_lead_time(self): + """Test start time when both lead_time and WorkingHours are available.""" + start_time = get_staff_member_start_time(self.staff_member1, datetime.date(2023, 10, 9)) + self.assertEqual(start_time, self.wh.start_time) # lead_time should prevail + + +class TestGetStaffMemberEndTime(BaseStaffMemberTimeTestSetup): + """Test suite for get_staff_member_end_time function.""" + + def test_staff_member_end_time(self): + """Test end time based on WorkingHours.""" + end_time = get_staff_member_end_time(self.staff_member1, datetime.date(2023, 10, 9)) + self.assertEqual(end_time, datetime.time(17, 0)) + + def test_staff_member_end_time_with_finish_time(self): + """Test end time when both finish_time and WorkingHours are available.""" + end_time = get_staff_member_end_time(self.staff_member1, datetime.date(2023, 10, 9)) + self.assertEqual(end_time, self.wh.end_time) # finish_time should prevail diff --git a/appointment/tests/utils/test_validators.py b/appointment/tests/utils/test_validators.py new file mode 100644 index 0000000..8faea70 --- /dev/null +++ b/appointment/tests/utils/test_validators.py @@ -0,0 +1,31 @@ +import datetime + +from appointment.utils.validators import not_in_the_past +from django.core.exceptions import ValidationError +from django.test import TestCase +from django.utils.translation import gettext as _ + + +class NotInThePastTests(TestCase): + def test_date_in_the_past_raises_validation_error(self): + """Test that a date in the past raises a ValidationError.""" + past_date = datetime.date.today() - datetime.timedelta(days=1) + with self.assertRaises(ValidationError) as context: + not_in_the_past(past_date) + self.assertEqual(str(context.exception.message), _('Date is in the past')) + + def test_date_today_does_not_raise_error(self): + """Test that today's date does not raise an error.""" + today = datetime.date.today() + try: + not_in_the_past(today) + except ValidationError: + self.fail("not_in_the_past() raised ValidationError unexpectedly for today's date!") + + def test_date_in_the_future_does_not_raise_error(self): + """Test that a date in the future does not raise an error.""" + future_date = datetime.date.today() + datetime.timedelta(days=1) + try: + not_in_the_past(future_date) + except ValidationError: + self.fail("not_in_the_past() raised ValidationError unexpectedly for a future date!") diff --git a/appointment/tests/utils/test_view_helpers.py b/appointment/tests/utils/test_view_helpers.py new file mode 100644 index 0000000..57b9c04 --- /dev/null +++ b/appointment/tests/utils/test_view_helpers.py @@ -0,0 +1,55 @@ +# test_view_helpers.py +# Path: appointment/tests/test_view_helpers.py + +from django.http import HttpRequest +from django.test import TestCase + +from appointment.utils.view_helpers import generate_random_id, get_locale, is_ajax + + +class GetLocaleTests(TestCase): + """Test cases for get_locale""" + + def test_get_locale_en(self): + with self.settings(LANGUAGE_CODE='en'): + self.assertEqual(get_locale(), 'en') + + def test_get_locale_en_us(self): + with self.settings(LANGUAGE_CODE='en_US'): + self.assertEqual(get_locale(), 'en') + + def test_get_locale_fr(self): + # Set the local to French + with self.settings(LANGUAGE_CODE='fr'): + self.assertEqual(get_locale(), 'fr') + + def test_get_locale_fr_France(self): + # Set the local to French + with self.settings(LANGUAGE_CODE='fr_FR'): + self.assertEqual(get_locale(), 'fr') + + def test_get_locale_others(self): + with self.settings(LANGUAGE_CODE='de'): + self.assertEqual(get_locale(), 'de') + + +class IsAjaxTests(TestCase): + """Test cases for is_ajax""" + + def test_is_ajax_true(self): + request = HttpRequest() + request.META['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' + self.assertTrue(is_ajax(request)) + + def test_is_ajax_false(self): + request = HttpRequest() + self.assertFalse(is_ajax(request)) + + +class GenerateRandomIdTests(TestCase): + """Test cases for generate_random_id""" + + def test_generate_random_id(self): + id1 = generate_random_id() + id2 = generate_random_id() + self.assertNotEqual(id1, id2) diff --git a/appointment/utils/db_helpers.py b/appointment/utils/db_helpers.py index 68ccb11..575e565 100644 --- a/appointment/utils/db_helpers.py +++ b/appointment/utils/db_helpers.py @@ -11,7 +11,7 @@ from urllib.parse import urlparse from django.apps import apps -from django.conf import settings +from django.contrib.auth import get_user_model from django.core.cache import cache from django.core.exceptions import FieldDoesNotExist from django.urls import reverse @@ -103,8 +103,8 @@ def create_and_save_appointment(ar, client_data: dict, appointment_data: dict, r """ user = get_user_by_email(client_data['email']) appointment = Appointment.objects.create( - client=user, appointment_request=ar, - **appointment_data + client=user, appointment_request=ar, + **appointment_data ) appointment.save() logger.info(f"New appointment created: {appointment.to_dict()}") @@ -179,7 +179,7 @@ def update_appointment_reminder(appointment, new_date, new_start_time, request, schedule_email_reminder(appointment, request, new_datetime) else: logger.info( - f"Reminder for appointment {appointment.id} is not scheduled per user's preference or past datetime.") + f"Reminder for appointment {appointment.id} is not scheduled per user's preference or past datetime.") # Update the appointment's reminder preference appointment.want_reminder = want_reminder @@ -309,8 +309,8 @@ def create_payment_info_and_get_url(appointment): urlparse(APPOINTMENT_PAYMENT_URL).netloc): # It's a Django reverse URL; generate the URL payment_url = reverse( - APPOINTMENT_PAYMENT_URL, - kwargs={'object_id': payment_info.id, 'id_request': payment_info.get_id_request()} + APPOINTMENT_PAYMENT_URL, + kwargs={'object_id': payment_info.id, 'id_request': payment_info.get_id_request()} ) else: # It's an external link; return as is or append necessary data @@ -350,10 +350,10 @@ def exclude_pending_reschedules(slots, staff_member, date): # Calculate the time window for "last 5 minutes" ten_minutes_ago = timezone.now() - datetime.timedelta(minutes=5) pending_reschedules = AppointmentRescheduleHistory.objects.filter( - appointment_request__staff_member=staff_member, - date=date, - reschedule_status='pending', - created_at__gte=ten_minutes_ago + appointment_request__staff_member=staff_member, + date=date, + reschedule_status='pending', + created_at__gte=ten_minutes_ago ) # Filter out slots that overlap with any pending rescheduling @@ -478,10 +478,10 @@ def get_appointments_for_date_and_time(date, start_time, end_time, staff_member) :return: QuerySet, all appointments that overlap with the specified date and time range """ return Appointment.objects.filter( - appointment_request__date=date, - appointment_request__start_time__lte=end_time, - appointment_request__end_time__gte=start_time, - appointment_request__staff_member=staff_member + appointment_request__date=date, + appointment_request__start_time__lte=end_time, + appointment_request__end_time__gte=start_time, + appointment_request__staff_member=staff_member ) @@ -606,14 +606,6 @@ def get_times_from_config(date): return start_time, end_time, slot_duration, buff_time -def get_user_model(): - """Get the client models from the settings file. - - :return: The user model - """ - return apps.get_model(settings.AUTH_USER_MODEL) - - def get_user_by_email(email: str): """Get a user by their email address. From fcadece4d21e621cedd25573228dc28b6c66e5d2 Mon Sep 17 00:00:00 2001 From: Adams Pierre David <57180807+adamspd@users.noreply.github.com> Date: Sat, 18 May 2024 07:12:31 +0200 Subject: [PATCH 7/7] Fix dependant tests --- appointment/models.py | 30 +++++----------- appointment/tests/test_services.py | 56 ++++++++++++++++++++++++++---- 2 files changed, 58 insertions(+), 28 deletions(-) diff --git a/appointment/models.py b/appointment/models.py index 0bec7ee..9007d0b 100644 --- a/appointment/models.py +++ b/appointment/models.py @@ -454,30 +454,16 @@ def save(self, *args, **kwargs): if self.id_request is None: self.id_request = f"{get_timestamp()}{self.appointment_request.id}{generate_random_id()}" - - try: - # Ensure `amount_to_pay` is a Decimal and handle both int and float inputs - if self.amount_to_pay is None: - self.amount_to_pay = self._calculate_amount_to_pay() - - self.amount_to_pay = self._to_decimal(self.amount_to_pay) - except InvalidOperation: - raise ValidationError("Invalid amount format for payment.") - + if self.amount_to_pay is None or self.amount_to_pay == 0: + payment_type = self.appointment_request.payment_type + if payment_type == 'full': + self.amount_to_pay = self.appointment_request.get_service_price() + elif payment_type == 'down': + self.amount_to_pay = self.appointment_request.get_service_down_payment() + else: + self.amount_to_pay = 0 return super().save(*args, **kwargs) - def _calculate_amount_to_pay(self): - payment_type = self.appointment_request.payment_type - if payment_type == 'full': - return self.appointment_request.get_service_price() - elif payment_type == 'down': - return self.appointment_request.get_service_down_payment() - else: - return Decimal('0.00') - - def _to_decimal(self, value): - return Decimal(f"{value}").quantize(Decimal('0.01')) - def get_client_name(self): if hasattr(self.client, 'get_full_name') and callable(getattr(self.client, 'get_full_name')): name = self.client.get_full_name() diff --git a/appointment/tests/test_services.py b/appointment/tests/test_services.py index 14904d3..a863b92 100644 --- a/appointment/tests/test_services.py +++ b/appointment/tests/test_services.py @@ -41,24 +41,30 @@ def tearDownClass(cls): def setUp(self): self.tomorrow = timezone.now().date() + datetime.timedelta(days=1) - ar = self.create_appt_request_for_sm1(date_=self.tomorrow) + ar = self.create_appt_request_for_sm1(date_=self.tomorrow, start_time=time(11, 0), end_time=time(12, 0)) self.appointment = self.create_appt_for_sm1(appointment_request=ar) + @override_settings(DEBUG=True) + def tearDown(self): + Config.objects.all().delete() + super().tearDown() + cache.clear() + def test_get_available_slots(self): slots = get_available_slots(self.tomorrow, [self.appointment]) self.assertIsInstance(slots, list) - self.assertNotIn('09:00 AM', slots) + self.assertNotIn('11:00 AM', slots) def test_get_available_slots_with_config(self): Config.objects.create( - lead_time=datetime.time(8, 0), - finish_time=datetime.time(17, 0), + lead_time=datetime.time(11, 0), + finish_time=datetime.time(15, 0), slot_duration=30, appointment_buffer_time=2.0 ) slots = get_available_slots(self.tomorrow, [self.appointment]) self.assertIsInstance(slots, list) - self.assertNotIn('09:00 AM', slots) + self.assertNotIn('11:00 AM', slots) class FetchUserAppointmentsTests(BaseTest): @@ -244,6 +250,13 @@ def test_regular_user_with_non_existent_staff_user_id(self): class HandleEntityManagementRequestTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() def setUp(self): super().setUp() @@ -254,6 +267,10 @@ def setUp(self): self.request = self.factory.post('/') self.request.user = self.staff_member1.user + def tearDown(self): + WorkingHours.objects.all().delete() + super().tearDown() + def test_staff_member_none(self): """A day off cannot be created for a staff member that doesn't exist.""" response = handle_entity_management_request(self.request, None, 'day_off') @@ -318,10 +335,21 @@ def test_working_hours_post(self): class HandleWorkingHoursFormTest(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() def setUp(self): super().setUp() + def tearDown(self): + WorkingHours.objects.all().delete() + super().tearDown() + def test_add_working_hours(self): """Test if working hours can be added.""" response = handle_working_hours_form(self.staff_member1, 1, '09:00 AM', '05:00 PM', True) @@ -535,6 +563,14 @@ def setUp(self): Config.objects.create(slot_duration=60, lead_time=datetime.time(9, 0), finish_time=datetime.time(17, 0), appointment_buffer_time=0) + @override_settings(DEBUG=True) + def tearDown(self): + WorkingHours.objects.all().delete() + DayOff.objects.all().delete() + Config.objects.all().delete() + cache.clear() + super().tearDown() + def test_day_off(self): """Test if a day off is handled correctly when getting available slots.""" # Ask for slots for it, and it should return an empty list since next Monday is a day off @@ -587,6 +623,7 @@ def test_booked_slots(self): hour in range(9, 17) if hour != 10] self.assertEqual(slots, expected_slots) + @override_settings(DEBUG=True) def test_no_working_hours(self): """If a staff member doesn't have working hours on a given day, no slots should be available.""" # Let's ask for slots on a Thursday, which the staff member doesn't work @@ -856,6 +893,14 @@ def test_get_finish_button_text_paid_service(self): class SlotAvailabilityTest(BaseTest, ConfigMixin): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + def setUp(self): self.service = self.create_service_(duration=timedelta(hours=2)) self.config = self.create_config_(lead_time=time(11, 0), finish_time=time(15, 0), slot_duration=120) @@ -864,7 +909,6 @@ def setUp(self): @override_settings(DEBUG=True) def tearDown(self): self.service.delete() - self.config.delete() cache.clear() def test_slot_availability_without_appointments(self):