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..9007d0b 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 @@ -67,7 +68,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)]) @@ -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 @@ -596,7 +597,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'), @@ -664,6 +665,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 ef36486..5b9af17 100644 --- a/appointment/tests/base/base_test.py +++ b/appointment/tests/base/base_test.py @@ -2,51 +2,84 @@ from django.test import TestCase +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(TestCase, 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_( + 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) - def create_appt_request_for_sm1(self, **kwargs): + @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, 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_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 +92,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..11788ed 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,11 +26,13 @@ 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=50000, + description="Assess the Quantum Mirror"): return Service.objects.create( name=name, duration=duration, - price=price + price=price, + description=description ) @@ -62,7 +63,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 +72,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="+12392340543", + address="Stargate Command, Cheyenne Mountain Complex, Colorado Springs, CO"): return Appointment.objects.create( client=user, appointment_request=appointment_request, @@ -80,7 +82,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 +92,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_, 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 new file mode 100644 index 0000000..f31ee7b --- /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_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) + 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 index 702d44d..a3079db 100644 --- a/appointment/tests/models/test_appointment_reschedule_history.py +++ b/appointment/tests/models/test_appointment_reschedule_history.py @@ -7,58 +7,106 @@ from appointment.tests.base.base_test import BaseTest -class AppointmentRescheduleHistoryTestCase(BaseTest): +class AppointmentRescheduleHistoryCreationTests(BaseTest): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + def setUp(self): - super().setUp() self.appointment_request = self.create_appt_request_for_sm1() + self.future_date = timezone.now().date() + timedelta(days=3) + return super().setUp() - def test_successful_creation(self): + def test_reschedule_history_creation_with_valid_data(self): reschedule_history = AppointmentRescheduleHistory.objects.create( appointment_request=self.appointment_request, - date=timezone.now().date() + timedelta(days=1), # Future date + 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.id_request) # Auto-generated id_request + self.assertIsNotNone(reschedule_history) + self.assertEqual(reschedule_history.reschedule_status, 'pending') self.assertTrue(reschedule_history.still_valid()) - def test_date_in_past_validation(self): + 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=timezone.now().date() - timedelta(days=1), # Past date + 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_validation(self): + def test_invalid_date_format_raises_type_error(self): with self.assertRaises(TypeError): AppointmentRescheduleHistory.objects.create( appointment_request=self.appointment_request, - date="invalid-date", # Invalid date format + date="invalid-date", start_time=timezone.now().time(), end_time=(timezone.now() + timedelta(hours=1)).time(), staff_member=self.staff_member1 ) - def test_still_valid(self): + +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=timezone.now().date() + timedelta(days=1), + 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' ) - # Directly test the still_valid method self.assertTrue(reschedule_history.still_valid()) - # Simulate passages of time beyond the validity window + 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_model_config.py b/appointment/tests/models/test_config.py similarity index 54% rename from appointment/tests/models/test_model_config.py rename to appointment/tests/models/test_config.py index 9d5c5f3..0c28a5c 100644 --- a/appointment/tests/models/test_model_config.py +++ b/appointment/tests/models/test_config.py @@ -1,84 +1,129 @@ from datetime import time +from django.core.cache import cache from django.core.exceptions import ValidationError -from django.test import TestCase +from django.test import TestCase, override_settings from appointment.models import Config -class ConfigModelTestCase(TestCase): +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="My Website") + website_name="Stargate Command") - def test_config_creation(self): - """Test if a configuration can be created.""" + @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, "My Website") + 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.""" + 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) - 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)) +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") - 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() + @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 = 40 + self.config.slot_duration = 41 + self.config.website_name = "Cheyeene Mountain Complex" self.config.save() - def test_slot_duration_of_zero(self): - """Test that a configuration cannot be created with a slot duration of zero.""" + 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_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_day_off.py similarity index 65% rename from appointment/tests/models/test_model_day_off.py rename to appointment/tests/models/test_day_off.py index 15cebdc..ac4fc1b 100644 --- a/appointment/tests/models/test_model_day_off.py +++ b/appointment/tests/models/test_day_off.py @@ -1,3 +1,4 @@ +from copy import deepcopy from datetime import date, timedelta from django.core.exceptions import ValidationError @@ -7,19 +8,48 @@ from appointment.tests.base.base_test import BaseTest -class DayOffModelTestCase(BaseTest): +class DayOffCreationTestCase(BaseTest): + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + @classmethod + def setUpTestData(cls): + super().setUpTestData() + 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) + end_date=date.today() + timedelta(days=2) ) + super().setUp() - def test_day_off_creation(self): + 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.""" @@ -30,11 +60,6 @@ def test_day_off_start_date_before_end_date(self): 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): @@ -42,12 +67,3 @@ def test_day_off_without_staff_member(self): 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_email_verification.py similarity index 71% rename from appointment/tests/models/test_model_email_verification.py rename to appointment/tests/models/test_email_verification.py index b967a08..877f12c 100644 --- a/appointment/tests/models/test_model_email_verification.py +++ b/appointment/tests/models/test_email_verification.py @@ -6,19 +6,24 @@ from appointment.tests.mixins.base_mixin import UserMixin -class EmailVerificationCodeModelTestCase(TestCase, UserMixin): +class EmailVerificationCodeBasicTestCase(TestCase, UserMixin): def setUp(self): self.user = self.create_user_() self.code = EmailVerificationCode.generate_code(self.user) - def test_code_creation(self): + 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) - - def test_code_length(self): - """Test that the code is six characters long.""" + 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): @@ -26,21 +31,6 @@ def test_code_content(self): 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. 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_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_password_reset_token.py b/appointment/tests/models/test_password_reset_token.py similarity index 78% rename from appointment/tests/models/test_model_password_reset_token.py rename to appointment/tests/models/test_password_reset_token.py index c6646ce..decc0fb 100644 --- a/appointment/tests/models/test_model_password_reset_token.py +++ b/appointment/tests/models/test_password_reset_token.py @@ -7,25 +7,44 @@ from appointment.tests.base.base_test import BaseTest -class PasswordResetTokenTests(BaseTest): +class PasswordResetTokenCreationTests(BaseTest): + def setUp(self): super().setUp() - self.user = self.create_user_(username='test_user', email='test@example.com', password='test_pass123') + 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 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 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.""" - 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) + 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.""" @@ -62,6 +81,14 @@ def test_token_expiration(self): 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) @@ -72,7 +99,8 @@ 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) + + 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.""" @@ -89,14 +117,6 @@ def test_verify_token_failure_already_verified(self): 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) @@ -123,38 +143,6 @@ def test_create_multiple_tokens_for_user(self): 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 @@ -185,3 +173,49 @@ def test_token_verification_after_user_deletion(self): 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_model_payment_info.py b/appointment/tests/models/test_payment_info.py similarity index 51% rename from appointment/tests/models/test_model_payment_info.py rename to appointment/tests/models/test_payment_info.py index 2fe88d3..cd5a5d2 100644 --- a/appointment/tests/models/test_model_payment_info.py +++ b/appointment/tests/models/test_payment_info.py @@ -2,63 +2,73 @@ from appointment.tests.base.base_test import BaseTest -class PaymentInfoModelTestCase(BaseTest): +class PaymentInfoBasicTestCase(BaseTest): + @classmethod + def setUpTestData(cls): + return super().setUpTestData() + + @classmethod + def tearDownClass(cls): + return super().tearDownClass() + 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.appointment = self.create_appt_for_sm1(appointment_request=self.ar) self.payment_info = PaymentInfo.objects.create(appointment=self.appointment) + return super().setUp() - 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 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.""" + """Test if 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.""" + 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) 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()) + 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) - 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()) + +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.""" + """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) 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_service.py b/appointment/tests/models/test_service.py new file mode 100644 index 0000000..883f011 --- /dev/null +++ b/appointment/tests/models/test_service.py @@ -0,0 +1,266 @@ +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 appointment.models import Service +from appointment.tests.base.base_test import BaseTest + + +class ServiceCreationAndBasicAttributesTests(BaseTest): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + cls.service = cls.service1 + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + def test_service_creation(self): + self.assertIsNotNone(self.service) + + 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, timedelta(hours=1)) + + 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 check_price(self, price, expected_string): + service = deepcopy(self.service) + service.price = price + self.assertEqual(service.get_price_text(), expected_string) + + 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 + + @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) + + def check_duration(self, duration, expected_string): + service = deepcopy(self.service) + service.duration = duration + self.assertEqual(service.get_duration(), expected_string) + + 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 = [ + (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) + + +class ServiceDownPaymentTests(BaseTest): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + cls.service = cls.service1 + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + 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_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_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) + + +class ServiceRepresentationAndMiscTests(BaseTest): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + cls.service = cls.service1 + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + 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_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 + 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_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_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_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") + 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) + + 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 + def setUpTestData(cls): + super().setUpTestData() + cls.service = cls.service1 + + @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 " + "Portal - Series SG-1") # Exceeding the max_length (112 characters) + with self.assertRaises(ValidationError): + s.full_clean() + + 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 + 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_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" + + with self.assertRaises(ValidationError): + s.full_clean() + + s.currency = "DOLLAR" + with self.assertRaises(ValidationError): + s.full_clean() + + 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) + 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_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..f3e47a7 --- /dev/null +++ b/appointment/tests/models/test_staff_member.py @@ -0,0 +1,251 @@ +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) + 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.""" + 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) + 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.""" + 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_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_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") diff --git a/appointment/tests/models/test_model_working_hours.py b/appointment/tests/models/test_working_hours.py similarity index 82% rename from appointment/tests/models/test_model_working_hours.py rename to appointment/tests/models/test_working_hours.py index 1d4d879..7afaf58 100644 --- a/appointment/tests/models/test_model_working_hours.py +++ b/appointment/tests/models/test_working_hours.py @@ -3,6 +3,7 @@ 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 @@ -14,16 +15,39 @@ def setUp(self): 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) + 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): + 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.""" @@ -35,11 +59,6 @@ def test_working_hours_start_time_before_end_time(self): 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): @@ -49,23 +68,10 @@ def test_working_hours_without_staff_member(self): 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_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.""" @@ -87,7 +93,12 @@ def test_staff_member_weekend_status_update(self): 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)) + 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_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 index 6818219..a863b92 100644 --- a/appointment/tests/test_services.py +++ b/appointment/tests/test_services.py @@ -4,44 +4,95 @@ 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 +from django.test import Client, override_settings from django.test.client import RequestFactory -from django.utils.translation import gettext as _ +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_for_staff, handle_day_off_form, handle_entity_management_request, \ - handle_service_management_request, handle_working_hours_form, prepare_appointment_display_data, \ +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, 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('11:00 AM', slots) + + def test_get_available_slots_with_config(self): + Config.objects.create( + 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('11: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_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() + 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 - self.user1.is_superuser = True - self.user1.save() + jack = self.users['superuser'] + jack.is_superuser = True + jack.save() # Fetch appointments for superuser - appointments = fetch_user_appointments(self.user1) + appointments = fetch_user_appointments(jack) # Assert that the superuser sees all appointments self.assertIn(self.appointment_for_user1, appointments, @@ -52,7 +103,11 @@ def test_fetch_appointments_for_superuser(self): 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) + 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, @@ -63,13 +118,18 @@ def test_fetch_appointments_for_staff_member(self): 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(self.client1) + 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.""" - appointments = fetch_user_appointments(self.staff_user) + 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.") @@ -77,16 +137,27 @@ def test_fetch_appointments_for_staff_user_without_staff_member_instance(self): 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_appointment_for_user1() + 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.user2, 9999) + 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.")) @@ -94,7 +165,7 @@ def test_non_existent_appointment(self): 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) + 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.")) @@ -102,74 +173,90 @@ def test_unauthorized_user(self): 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, + 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.client1.first_name in page_title) + 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.""" - self.user1.is_superuser = True - self.user1.save() + + 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(self.user1, + 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.client1.first_name in page_title) - self.assertTrue(self.user1.first_name in page_title) + 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.""" - self.user1.is_superuser = True - self.user1.save() - data = prepare_user_profile_data(self.user1, None) + 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.user1, self.user2.pk) + 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.""" - self.user1.is_superuser = True - self.user1.save() - data = prepare_user_profile_data(self.user1, 9999) + 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.user1, self.user1.pk) + 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.user1) + 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.user1, 9999) + data = prepare_user_profile_data(self.jack, 9999) self.assertTrue(data['error']) self.assertEqual(data['status_code'], 403) class HandleEntityManagementRequestTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() def setUp(self): super().setUp() @@ -178,7 +265,11 @@ def setUp(self): # Setup request object self.request = self.factory.post('/') - self.request.user = self.user1 + 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.""" @@ -244,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) @@ -261,13 +363,14 @@ def test_update_working_hours(self): 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.""" + """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 appropriate error""" + """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()) @@ -285,7 +388,7 @@ def test_working_hours_conflict(self): 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""" + """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()) @@ -293,7 +396,8 @@ def test_invalid_working_hours_id(self): 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""" + """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()) @@ -333,21 +437,29 @@ def test_invalid_day_off_form(self): 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_appointment_for_user1() + 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 = "New Client Name" - client_email = "newclient@example.com" + client_name = "Teal'c of Chulak" + client_email = "tealc@chulak.com" start_time_str = "10:00 AM" phone_number = "+1234567890" - client_address = "123 New St, TestCity" + client_address = "123 Stargate Command, Cheyenne Mountain" service_id = self.service2.id staff_member_id = self.staff_member2.id @@ -375,8 +487,8 @@ 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() + # 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('/') @@ -413,7 +525,6 @@ def get_next_weekday(d, weekday): 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 @@ -421,7 +532,15 @@ def get_next_weekday(d, weekday): return next_day -class GetAvailableSlotsTests(BaseTest): +class GetAvailableSlotsForStaffTests(BaseTest): + + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() def setUp(self): super().setUp() @@ -444,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 @@ -487,7 +614,7 @@ def test_booked_slots(self): 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) + 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) @@ -496,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 @@ -508,18 +636,27 @@ def test_no_working_hours(self): 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.user1.email + '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.user1) + self.daniel) self.assertTrue(is_valid) self.assertIsNone(error_message) self.assertEqual(user.first_name, 'UpdatedName') @@ -528,7 +665,7 @@ def test_update_name(self): 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.daniel) # Assuming 9999 is an invalid user ID self.assertFalse(is_valid) self.assertEqual(error_message, _("User not found.")) @@ -536,7 +673,7 @@ def test_update_invalid_user_id(self): 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) + 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.")) @@ -545,7 +682,7 @@ def test_invalid_form_(self): # 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.daniel) self.assertFalse(is_valid) self.assertEqual(error_message, "email: This field is required.") @@ -554,27 +691,28 @@ class EmailChangeVerificationServiceTest(BaseTest): def setUp(self): super().setUp() - self.valid_code = EmailVerificationCode.generate_code(self.client1) + self.georges = self.users['client1'] + self.valid_code = EmailVerificationCode.generate_code(self.georges) self.invalid_code = "INVALID_CODE456" - self.old_email = self.client1.email - self.new_email = "newemail@gmail.com" + 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.client1.refresh_from_db() # Refresh the user object to get the updated email - self.assertEqual(self.client1.email, self.new_email) + 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.client1.refresh_from_db() - self.assertEqual(self.client1.email, self.old_email) # Email should not change + 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.""" @@ -585,13 +723,22 @@ def test_valid_code_no_user(self): 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") + 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() @@ -602,25 +749,26 @@ def setUp(self): 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' + '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, 'John') - self.assertEqual(user.last_name, 'Doe') - self.assertEqual(user.email, 'john.doe@gmail.com') + 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': 'Doe', - 'email': 'john.doe@gmail.com' + 'last_name': 'Langford', + 'email': 'catherine.langford@django-appointment.com' } user, success, error_message = create_staff_member_service(post_data, self.request) @@ -630,11 +778,11 @@ def test_invalid_data(self): 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") + self.create_user_() post_data = { - 'first_name': 'John', - 'last_name': 'Doe', - 'email': 'existing@gmail.com' # Using an email that already exists + '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) @@ -646,9 +794,9 @@ def test_email_already_exists(self): 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' + '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) @@ -666,38 +814,38 @@ def setUp(self): def test_create_new_service(self): """Test if a new service can be created with valid data.""" post_data = { - 'name': 'Test Service', + 'name': "Goa'uld extraction", 'duration': '1:00:00', - 'price': '100', + 'price': '10000', 'currency': 'USD', - 'down_payment': '50', + 'down_payment': '5000', } service, success, message = handle_service_management_request(post_data) self.assertTrue(success) self.assertIsNotNone(service) - self.assertEqual(service.name, 'Test Service') + self.assertEqual(service.name, "Goa'uld extraction") self.assertEqual(service.duration, datetime.timedelta(hours=1)) - self.assertEqual(service.price, Decimal('100')) - self.assertEqual(service.down_payment, Decimal('50')) + 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': 'Updated Service Name', + 'name': 'Quantum Mirror Repair', 'duration': '2:00:00', - 'price': '150', - 'down_payment': '75', + '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, 'Updated Service Name') + self.assertEqual(service.name, 'Quantum Mirror Repair') self.assertEqual(service.duration, datetime.timedelta(hours=2)) - self.assertEqual(service.price, Decimal('150')) + self.assertEqual(service.price, Decimal('15000')) self.assertEqual(service.currency, 'EUR') def test_invalid_data(self): @@ -718,13 +866,83 @@ def test_invalid_data(self): def test_service_not_found(self): """If the service ID is invalid, the service should not be updated.""" post_data = { - 'name': 'Another Test Service', + 'name': 'DHD maintenance', 'duration': '1:00:00', - 'price': '100', + '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(_("Service matching query does not exist"), message) + 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): + + @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) + 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() + 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_tasks.py b/appointment/tests/test_tasks.py index 878f54d..f07cc2a 100644 --- a/appointment/tests/test_tasks.py +++ b/appointment/tests/test_tasks.py @@ -11,12 +11,20 @@ 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_appointment_for_user1(appointment_request=appointment_request) + appointment = self.create_appt_for_sm1(appointment_request=appointment_request) # Extract necessary data for the test to_email = appointment.client.email 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 index 81197ac..6dc110a 100644 --- a/appointment/tests/test_views.py +++ b/appointment/tests/test_views.py @@ -29,10 +29,20 @@ 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 +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() @@ -40,23 +50,22 @@ def setUp(self): 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}, + 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_invalid_form(self): + 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') - # 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): @@ -97,8 +106,17 @@ def test_appointment_request_submit_invalid(self): 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('/') @@ -117,19 +135,19 @@ def setUp(self): 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) + 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.user1, 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.user1) + 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) @@ -145,10 +163,19 @@ def test_enter_verification_code_invalid(self): 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_appointment_for_user1() + self.appointment = self.create_appt_for_sm1() def remove_staff_member(self): """Remove the StaffMember instance of self.user1.""" @@ -170,9 +197,9 @@ def test_staff_user_without_staff_member_instance(self): 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.") + 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() @@ -196,10 +223,13 @@ 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.user1).exists()) + self.assertFalse(StaffMember.objects.filter(user=self.jack).exists()) # Check if it redirects to the user profile self.assertRedirects(response, reverse('appointment:user_profile')) @@ -215,11 +245,12 @@ def test_remove_staff_member_without_superuser(self): 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.user1).exists()) + self.assertTrue(StaffMember.objects.filter(user=self.jack).exists()) # Check if it redirects to the user profile self.assertRedirects(response, reverse('appointment:user_profile')) @@ -266,9 +297,17 @@ def test_is_user_staff_admin_without_staff_member(self): class AppointmentTestCase(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + def setUp(self): super().setUp() - self.appointment = self.create_appointment_for_user1() + self.appointment = self.create_appt_for_sm1() def test_delete_appointment(self): self.need_staff_login() @@ -308,7 +347,7 @@ def test_delete_appointment_without_permission(self): 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() + different_appointment = self.create_appt_for_sm2() url = reverse('appointment:delete_appointment', args=[different_appointment.id]) response = self.client.post(url) @@ -324,7 +363,7 @@ def test_delete_appointment_ajax_without_permission(self): 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() + 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') @@ -340,14 +379,23 @@ def test_delete_appointment_ajax_without_permission(self): class UpdateAppointmentTestCase(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + def setUp(self): super().setUp() - self.appointment = self.create_appointment_for_user1() + 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': 'Bryan Zap', - 'client_email': 'bz@gmail.com', 'client_phone': '+33769992738', 'client_address': 'Naples, Florida', + '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') @@ -456,6 +504,14 @@ def test_update_with_invalid_data_causing_exception(self): class ServiceViewTestCase(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + def setUp(self): super().setUp() @@ -473,12 +529,12 @@ def test_fetch_service_list_for_staff(self): 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] + 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() + test_appointment = self.create_appt_for_sm1() # Simulate a request with appointmentId url_with_appointment = f"{url}?appointmentId={test_appointment.id}" @@ -488,16 +544,17 @@ def test_fetch_service_list_for_staff(self): 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] + 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=self.user1).delete() + StaffMember.objects.filter(user=jack).delete() url = reverse('appointment:fetch_service_list_for_staff') response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') @@ -558,9 +615,17 @@ def test_delete_nonexistent_service(self): class AppointmentDisplayViewTestCase(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + def setUp(self): super().setUp() - self.appointment = self.create_appointment_for_user1() + 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): @@ -683,6 +748,14 @@ def test_delete_nonexistent_day_off(self): class ViewsTestCase(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + def setUp(self): super().setUp() self.client = Client() @@ -694,6 +767,7 @@ def setUp(self): 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 @@ -717,13 +791,20 @@ def test_default_thank_you(self): 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" + first_name="Great Tester", email="great.tester@django-appointment.com", username="great_tester" ) self.data = { "user": self.user_test.id, @@ -766,12 +847,24 @@ def test_add_staff_member_info_invalid_form_submission(self): class SetPasswordViewTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + def setUp(self): super().setUp() user_data = { - 'username': 'test_user', 'email': 'test@example.com', 'password': 'oldpassword', 'first_name': 'John', - 'last_name': 'Doe' + '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)) @@ -809,7 +902,7 @@ def test_get_request_with_invalid_token(self): 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_)) + 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()) @@ -830,10 +923,18 @@ def test_post_request_with_expired_token(self): 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_)) + 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() @@ -867,6 +968,14 @@ def test_ajax_required(self): class AppointmentClientInformationTest(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + def setUp(self): super().setUp() self.client = Client() @@ -875,12 +984,12 @@ def setUp(self): self.factory = RequestFactory() self.request = self.factory.get('/') self.valid_form_data = { - 'name': 'Test Client', + 'name': 'Adria Origin', 'service_id': '1', 'payment_type': 'full', - 'email': 'testuser@example.com', + 'email': 'adria@django-appointment.com', 'phone': '+1234567890', - 'address': '123 Test St.', + 'address': '123 Ori Temple, Celestis', } def test_get_request(self): @@ -907,6 +1016,14 @@ def test_already_submitted_session(self): class PrepareRescheduleAppointmentViewTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + def setUp(self): super().setUp() self.client = Client() @@ -942,11 +1059,19 @@ def test_reschedule_appointment_context_data(self): 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_appointment_for_user1(appointment_request=self.ar) + 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, @@ -987,24 +1112,33 @@ def test_reschedule_not_allowed(self): 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)) + _("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_appointment_for_user1(appointment_request=self.ar) + 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' + 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]) @@ -1043,9 +1177,17 @@ def test_notify_admin_about_reschedule_called(self, mock_notify_admin): class RedirectToPaymentOrThankYouPageTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + def setUp(self): super().setUp() - self.appointment = self.create_appointment_for_user1() + 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') @@ -1065,7 +1207,7 @@ def test_redirect_to_custom_thank_you_page(self): self.assertIsInstance(response, HttpResponseRedirect) self.assertTrue(response.url.startswith( - reverse('appointment:default_thank_you', kwargs={'appointment_id': self.appointment.id}))) + reverse('appointment:default_thank_you', kwargs={'appointment_id': self.appointment.id}))) @patch('appointment.views.APPOINTMENT_PAYMENT_URL', '') @patch('appointment.views.APPOINTMENT_THANK_YOU_URL', '') @@ -1075,15 +1217,23 @@ def test_redirect_to_default_thank_you_page(self): self.assertIsInstance(response, HttpResponseRedirect) self.assertTrue(response.url.startswith( - reverse('appointment:default_thank_you', kwargs={'appointment_id': self.appointment.id}))) + 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': 'John Doe', 'email': 'john@example.com'} - self.appointment_data = {'phone': '1234567890', 'want_reminder': True, 'address': '123 Test St.', + 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('/') diff --git a/appointment/tests/utils/test_date_time.py b/appointment/tests/utils/test_date_time.py index 2dc5b4a..e2bb5af 100644 --- a/appointment/tests/utils/test_date_time.py +++ b/appointment/tests/utils/test_date_time.py @@ -15,12 +15,12 @@ class Convert12HourTo24HourTimeTests(TestCase): - def test_valid_basic_conversion(self): + 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_convert_midnight_and_noon(self): + 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") @@ -42,22 +42,16 @@ def test_case_insensitivity_and_whitespace(self): 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.""" + 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") - - 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): @@ -70,18 +64,18 @@ def test_invalid_inputs(self): class Convert24HourTimeTo12HourTimeTests(TestCase): - def test_valid_24_hour_time_strings(self): + 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_time_with_seconds(self): + 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_datetime_time_object_input(self): + 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) @@ -96,8 +90,6 @@ def test_invalid_time_strings(self): 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): @@ -113,7 +105,7 @@ def test_edge_cases(self): class ConvertMinutesInHumanReadableFormatTests(TestCase): - def test_valid_basic_conversions(self): + 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") @@ -161,13 +153,13 @@ def test_invalid_inputs(self): class ConvertStrToDateTests(TestCase): - def test_valid_date_strings_with_hyphen_separator(self): + 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_strings_with_slash_separator(self): + 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)) @@ -198,14 +190,14 @@ def test_other_invalid_inputs(self): class ConvertStrToTimeTests(TestCase): - def test_convert_12h_format_str_to_time(self): + 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_convert_24h_format_str_to_time(self): + 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)) @@ -218,7 +210,7 @@ def test_case_insensitivity_and_whitespace(self): 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): + def test_invalid_time_strings(self): """Test invalid time strings""" with self.assertRaises(ValueError): convert_str_to_time("") @@ -276,14 +268,14 @@ def test_invalid_start_time_type(self): class TimeDifferenceTests(TestCase): - def test_valid_difference_with_time_objects(self): + 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_valid_difference_with_datetime_objects(self): + 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) @@ -324,7 +316,7 @@ def test_mismatched_input_types(self): class CombineDateAndTimeTests(TestCase): - def test_combine_valid_date_and_time(self): + 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) diff --git a/appointment/tests/utils/test_db_helpers.py b/appointment/tests/utils/test_db_helpers.py index bee36da..fbb7b1b 100644 --- a/appointment/tests/utils/test_db_helpers.py +++ b/appointment/tests/utils/test_db_helpers.py @@ -8,30 +8,26 @@ 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 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 DayOff, PaymentInfo +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 ( - 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, + 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_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, + 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 ) @@ -89,21 +85,36 @@ def test_one_slot_available(self): 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=1) + self.working_date = datetime.date.today() + datetime.timedelta(days=3) 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.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 - self.buffer_time = datetime.timedelta(minutes=25) + + @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) @@ -134,6 +145,9 @@ def setUp(self): 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")) @@ -160,16 +174,20 @@ def setUp(self): 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': 'tester2@gmail.com', - 'name': 'Tester2', + 'email': 'georges.s.hammond@django-appointment.com', + 'name': 'georges.hammond', } appointment_data = { 'phone': '123456789', 'want_reminder': True, - 'address': '123 Main St', - 'additional_info': 'Additional Test Info' + '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) @@ -194,22 +212,25 @@ 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): - 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) + 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): - 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) + 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.") + "Django-Q cluster is not running. Email reminder will not be scheduled.") class UpdateAppointmentReminderTest(BaseTest): @@ -217,10 +238,14 @@ def setUp(self): super().setUp() self.factory = RequestFactory() self.request = self.factory.get('/') - self.appointment = self.create_appointment_for_user1() + 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_appointment_for_user1() + appointment = self.create_appt_for_sm1() new_date = timezone.now().date() + timezone.timedelta(days=10) new_start_time = timezone.now().time() @@ -231,7 +256,7 @@ def test_update_appointment_reminder_date_time_changed(self): mock_schedule_email_reminder.assert_called_once() def test_update_appointment_reminder_no_change(self): - appointment = self.create_appointment_for_user2() + 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 @@ -253,7 +278,7 @@ def test_reminder_not_scheduled_due_to_user_preference(self, mock_logger): # 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." + 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 @@ -267,7 +292,7 @@ def test_reminder_not_scheduled_due_to_past_datetime(self, mock_logger): # 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." + f"Reminder for appointment {self.appointment.id} is not scheduled per user's preference or past datetime." ) @@ -281,7 +306,12 @@ def modify_service_rescheduling(service, **kwargs): class CanAppointmentBeRescheduledTests(BaseTest, ConfigMixin): def setUp(self): super().setUp() - self.appointment = self.create_appointment_for_user1() + 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) @@ -301,18 +331,23 @@ def test_rescheduling_allowed_exceeds_limit(self): 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.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_appointment_reschedule_for_user1(appointment_request=ar) + 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 @@ -334,66 +369,9 @@ def test_staff_change_not_allowed(self, mock_config_first): 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() + 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) @@ -407,7 +385,7 @@ 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) + 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" @@ -443,7 +421,7 @@ class TestExcludeBookedSlots(BaseTest): def setUp(self): super().setUp() - self.appointment = self.create_appointment_for_user1() + self.appointment = self.create_appt_for_sm1() # Sample slots for testing self.today = datetime.date.today() @@ -486,7 +464,7 @@ def test_appointment_intersecting_single_slot(self): 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 = self.create_appt_for_sm2(appointment_request=ar2) appointment2.save() result = exclude_booked_slots([self.appointment, appointment2], self.slots, self.slot_duration) expected = [ @@ -500,7 +478,7 @@ class TestDayOffExistsForDateRange(BaseTest): def setUp(self): super().setUp() - self.user = self.create_user_(email="tester@gmail.com") + 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", @@ -519,7 +497,8 @@ def test_day_off_does_not_exist(self): 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)) + 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 @@ -530,8 +509,8 @@ class TestGetAllAppointments(BaseTest): def setUp(self): super().setUp() - self.appointment1 = self.create_appointment_for_user1() - self.appointment2 = self.create_appointment_for_user2() + self.appointment1 = self.create_appt_for_sm1() + self.appointment2 = self.create_appt_for_sm2() def test_get_all_appointments(self): appointments = get_all_appointments() @@ -556,7 +535,7 @@ class TestGetAppointmentByID(BaseTest): def setUp(self): super().setUp() - self.appointment = self.create_appointment_for_user1() + self.appointment = self.create_appt_for_sm1() def test_existing_appointment(self): """Test fetching an existing appointment.""" @@ -576,6 +555,11 @@ def test_non_existing_appointment(self): @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 @@ -614,6 +598,13 @@ def test_config_not_set_but_constants_patched(self): class TestGetAppointmentsForDateAndTime(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() def setUp(self): super().setUp() @@ -622,6 +613,8 @@ def setUp(self): 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) @@ -665,6 +658,12 @@ 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() @@ -680,11 +679,12 @@ 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 + + # 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 @@ -739,12 +739,21 @@ def test_nonexistent_staff_member_id(self): 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_appointment_for_user1() - self.appointment2_for_user2 = self.create_appointment_for_user2() + 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.""" @@ -783,84 +792,19 @@ def test_get_weekday_num_from_date(self): 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): + @classmethod + def setUpClass(cls): + super().setUpClass() + @classmethod + def tearDownClass(cls): + super().tearDownClass() -class TestDBHelpers(BaseTest): 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.""" @@ -873,7 +817,7 @@ def test_get_staff_member_by_user_id(self): 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.""" + """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) @@ -894,12 +838,12 @@ def test_get_user_model(self): def test_get_user_by_email(self): """Retrieve a user by email.""" - user = get_user_by_email("tester1@gmail.com") + 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@gmail.com") + user = get_user_by_email("nonexistent@django-appointment.com") self.assertIsNone(user) @@ -907,10 +851,10 @@ 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) + 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): @@ -944,7 +888,8 @@ def test_is_working_day(self): 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,""" + """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 @@ -958,14 +903,19 @@ 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 + 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) @@ -993,48 +943,48 @@ def test_times_from_default_settings(self): 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'} + client_data = {'name': 'Cameron Mitchell', 'email': 'cameron.mitchell@django-appointment.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') + 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': 'John Doe', 'email': 'john.doe@example.com'} + client_data1 = {'name': 'Martouf of Malkshur', 'email': 'the.malkshur@django-appointment.com'} user1 = create_new_user(client_data1) - self.assertEqual(user1.username, 'john.doe') + self.assertEqual(user1.username, 'the.malkshur') - client_data2 = {'name': 'Jane Doe', 'email': 'john.doe@example.com'} + client_data2 = {'name': 'Jolinar of Malkshur', 'email': 'the.malkshur@django-appointment.com'} user2 = create_new_user(client_data2) - self.assertEqual(user2.username, 'john.doe01') # Suffix added + self.assertEqual(user2.username, 'the.malkshur01') # Suffix added - client_data3 = {'name': 'James Doe', 'email': 'john.doe@example.com'} + client_data3 = {'name': 'Lantash of Malkshur', 'email': 'the.malkshur@django-appointment.com'} user3 = create_new_user(client_data3) - self.assertEqual(user3.username, 'john.doe02') # Next suffix + 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 = 'john.doe@example.com' + email = 'jacob.carter@django-appointment.com' username = generate_unique_username_from_email(email) - self.assertEqual(username, 'john.doe') + 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='john.doe', email=email) + CLIENT_MODEL.objects.create_user(username='jacob.carter', email=email) new_username = generate_unique_username_from_email(email) - self.assertEqual(new_username, 'john.doe01') + self.assertEqual(new_username, 'jacob.carter01') def test_parse_name(self): """Test if parse_name function splits names correctly.""" - name = "John Doe" + name = "Garshaw of Belote" first_name, last_name = parse_name(name) - self.assertEqual(first_name, 'John') - self.assertEqual(last_name, 'Doe') + 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': 'John Doe', 'email': 'john.doe@example.com'} + 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()) @@ -1081,12 +1031,12 @@ 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" + 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)) @@ -1097,12 +1047,12 @@ def test_exclude_with_pending_reschedules_within_last_5_minutes(self): 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" + 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 @@ -1113,12 +1063,12 @@ def test_exclude_with_non_pending_reschedules_within_last_5_minutes(self): 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" + 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() diff --git a/appointment/tests/utils/test_email_ops.py b/appointment/tests/utils/test_email_ops.py index e7bfcd0..ce9b66b 100644 --- a/appointment/tests/utils/test_email_ops.py +++ b/appointment/tests/utils/test_email_ops.py @@ -1,3 +1,5 @@ +from copy import deepcopy +from datetime import datetime from unittest import mock from unittest.mock import MagicMock, patch @@ -9,66 +11,83 @@ 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, + 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 = self.user1 + self.user = deepcopy(self.users['staff1']) self.user.is_staff = True self.user.save() self.factory = RequestFactory() self.request = self.factory.get('/') - self.email = 'staff@example.com' + self.email = 'daniel.jackson@django-appointment.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.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 = 'token123' + mock_token.token = "Colonel_Samantha_Carter_a_Tau_ri_Scientist" 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' + 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) + 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']) + # 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): - ar = MagicMock() - message = get_thank_you_message(ar) + 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"): - ar = MagicMock() - ar.accepts_down_payment.return_value = True - message = get_thank_you_message(ar) + 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"): - ar = MagicMock() - ar.accepts_down_payment.return_value = False - message = get_thank_you_message(ar) + self.ar.accepts_down_payment.return_value = False + message = get_thank_you_message(self.ar) self.assertIn(thank_you_payment, message) @@ -77,37 +96,35 @@ 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): - 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) + 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(email, kwargs['recipient_list']) + 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_appointment_for_user1() + 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): - client_name = "John Doe" - - notify_admin_about_appointment(self.appointment, client_name) - + notify_admin_about_appointment(self.appointment, self.client_name) mock_notify_admin.assert_called_once() mock_send_email.assert_called_once() @@ -115,46 +132,44 @@ def test_notify_admin_about_appointment(self, mock_send_email, mock_notify_admin class SendVerificationEmailTests(BaseTest): def setUp(self): super().setUp() - self.appointment = self.create_appointment_for_user1() + 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() - email = "test@example.com" - send_verification_email(user, email) + send_verification_email(user, self.email) mock_send_email.assert_called_once_with( - recipient_list=[email], - subject=_("Email Verification"), - message=mock.ANY + 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): - # 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" + 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 = "Test" - self.email = "test@example.com" + 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://testserver/confirmation_link" + 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) @@ -167,4 +182,50 @@ def test_send_reschedule_confirmation_email(self, mock_send_email, mock_get_abso 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") + 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 index 2280a87..5d522cc 100644 --- a/appointment/tests/utils/test_json_context.py +++ b/appointment/tests/utils/test_json_context.py @@ -6,63 +6,112 @@ 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) +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): +class ConvertAppointmentToJsonTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() def setUp(self): super().setUp() self.factory = RequestFactory() - self.appointment = self.create_appointment_for_user1() + 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.""" - 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]) + 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 = "Test Message" + message = "Gate Room Under Attack" response = json_response(message=message) - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, 200, "Response status should be 200") content = json.loads(response.content.decode('utf-8')) - self.assertEqual(content['message'], message) + 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.""" - request = self.factory.get('/') - request.user = self.user1 - context = get_generic_context(request) - self.assertEqual(context['user'], self.user1) + 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.""" - 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") + 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('/') - message = "Unauthorized" - response = handle_unauthorized_response(request=request, message=message, response_type='json') - self.assertEqual(response.status_code, 403) + 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'], message) + 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/') - message = "Unauthorized" - response = handle_unauthorized_response(request, message, 'html') - self.assertEqual(response.status_code, 403) + 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 index 7854891..e6dbef6 100644 --- a/appointment/tests/utils/test_permissions.py +++ b/appointment/tests/utils/test_permissions.py @@ -2,59 +2,131 @@ # 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 +from appointment.utils.permissions import ( + check_entity_ownership, check_extensive_permissions, check_permissions, has_permission_to_delete_appointment +) -class PermissionTests(BaseTest): +class CheckEntityOwnershipTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() def setUp(self): super().setUp() - # Create users and entities for testing- - self.superuser = self.create_user_(username='superuser', email="superuser@gmail.com") + self.superuser = self.users['superuser'] 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)) + 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 index 01b5906..7bdb201 100644 --- a/appointment/tests/utils/test_session.py +++ b/appointment/tests/utils/test_session.py @@ -1,17 +1,28 @@ # 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 +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): +class HandleExistingEmailTests(BaseTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + + @classmethod + def tearDownClass(cls): + super().tearDownClass() def setUp(self): super().setUp() @@ -21,29 +32,32 @@ def setUp(self): # Setup request object self.request = self.factory.post('/') - self.request.user = self.user1 + self.hammond = self.users['client1'] + self.request.user = self.hammond # Setup session for the request - middleware = SessionMiddleware(lambda req: None) - middleware.process_request(self.request) + session_middleware = SessionMiddleware(lambda req: None) + session_middleware.process_request(self.request) self.request.session.save() # Setup messages for the request - middleware = MessageMiddleware(lambda req: None) - middleware.process_request(self.request) + messages_middleware = MessageMiddleware(lambda req: None) + messages_middleware.process_request(self.request) self.request.session.save() - def test_handle_existing_email(self): + @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': self.client1.email, - 'name': 'John Doe' + 'email': 'georges.s.hammond@django-appointment.com', + 'name': 'georges.hammond', } appointment_data = { - 'phone': '+1234567890', + 'phone': '123456789', 'want_reminder': True, - 'address': '123 Main St, City, Country', - 'additional_info': 'Some additional info' + '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) @@ -58,20 +72,75 @@ def test_handle_existing_email(self): # 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']) - def test_handle_email_change(self): + +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 = "new_email@example.com" + new_email = 'georges.hammond@django-appointment.com' - response = handle_email_change(self.request, self.user1, new_email) + 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.user1.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.""" 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.