Tests updated & minor environment notes for agents
This commit is contained in:
@@ -53,6 +53,7 @@ Build a reliable, maintainable salon booking platform with Django (backend) and
|
|||||||
- Python is invoked as `python3`.
|
- Python is invoked as `python3`.
|
||||||
- A virtualenv is in use.
|
- A virtualenv is in use.
|
||||||
- DB: PostgreSQL in production, SQLite allowed for local dev.
|
- DB: PostgreSQL in production, SQLite allowed for local dev.
|
||||||
|
- Backend tests must run with the venv active and `pytest-django` installed; run from `backend/` so `backend/pytest.ini` is picked up and `DJANGO_SETTINGS_MODULE` resolves.
|
||||||
|
|
||||||
## Collaboration Rules for Agents
|
## Collaboration Rules for Agents
|
||||||
- Don’t delete or rewrite unrelated work.
|
- Don’t delete or rewrite unrelated work.
|
||||||
@@ -62,4 +63,4 @@ Build a reliable, maintainable salon booking platform with Django (backend) and
|
|||||||
|
|
||||||
# ExecPlans
|
# ExecPlans
|
||||||
|
|
||||||
When writing complex features or significant refactors, use an ExecPlan (as described in PLANS.md) from design to implementation. The active ExecPlan is `docs/execplans/arabic-localization.md`.
|
When writing complex features or significant refactors, use an ExecPlan (as described in PLANS.md) from design to implementation. The active ExecPlan is `docs/execplans/booking-integrity.md`.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
@@ -21,16 +22,19 @@ def validate_booking_request(service, staff, start_time, end_time):
|
|||||||
if expected_end != end_time:
|
if expected_end != end_time:
|
||||||
raise serializers.ValidationError({"end_time": _("End time must match service duration")})
|
raise serializers.ValidationError({"end_time": _("End time must match service duration")})
|
||||||
|
|
||||||
|
start_local = timezone.localtime(start_time)
|
||||||
|
end_local = timezone.localtime(end_time)
|
||||||
|
|
||||||
availability_qs = StaffAvailability.objects.filter(
|
availability_qs = StaffAvailability.objects.filter(
|
||||||
staff=staff,
|
staff=staff,
|
||||||
day_of_week=start_time.weekday(),
|
day_of_week=start_local.weekday(),
|
||||||
is_active=True,
|
is_active=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
if availability_qs.exists():
|
if availability_qs.exists():
|
||||||
within_window = availability_qs.filter(
|
within_window = availability_qs.filter(
|
||||||
start_time__lte=start_time.time(),
|
start_time__lte=start_local.time(),
|
||||||
end_time__gte=end_time.time(),
|
end_time__gte=end_local.time(),
|
||||||
).exists()
|
).exists()
|
||||||
if not within_window:
|
if not within_window:
|
||||||
raise serializers.ValidationError({"start_time": _("Booking is outside staff availability")})
|
raise serializers.ValidationError({"start_time": _("Booking is outside staff availability")})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from datetime import timedelta
|
|||||||
import pytest
|
import pytest
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from rest_framework.test import APIClient
|
||||||
|
|
||||||
from apps.accounts.models import User, UserRole
|
from apps.accounts.models import User, UserRole
|
||||||
from apps.bookings.models import Booking, BookingStatus
|
from apps.bookings.models import Booking, BookingStatus
|
||||||
@@ -46,15 +47,34 @@ def booking_payload(service, staff, start_time, end_time, notes=""):
|
|||||||
|
|
||||||
|
|
||||||
def next_day_at(hour, minute=0):
|
def next_day_at(hour, minute=0):
|
||||||
now = timezone.now()
|
now_local = timezone.localtime(timezone.now())
|
||||||
target = now + timedelta(days=1)
|
target = now_local + timedelta(days=1)
|
||||||
return target.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
return target.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_requires_staff_assignment(client, base_entities):
|
def test_requires_authentication(base_entities):
|
||||||
|
_, staff, service = base_entities
|
||||||
|
client = APIClient()
|
||||||
|
|
||||||
|
start_time = next_day_at(9)
|
||||||
|
end_time = start_time + timedelta(minutes=service.duration_minutes)
|
||||||
|
payload = booking_payload(service, staff, start_time, end_time)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
reverse("booking-list"),
|
||||||
|
payload,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_requires_staff_assignment(base_entities):
|
||||||
customer, staff, service = base_entities
|
customer, staff, service = base_entities
|
||||||
client.force_login(customer)
|
client = APIClient()
|
||||||
|
client.force_authenticate(user=customer)
|
||||||
|
|
||||||
start_time = next_day_at(9)
|
start_time = next_day_at(9)
|
||||||
end_time = start_time + timedelta(minutes=service.duration_minutes)
|
end_time = start_time + timedelta(minutes=service.duration_minutes)
|
||||||
@@ -71,9 +91,10 @@ def test_requires_staff_assignment(client, base_entities):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_rejects_end_before_start(client, base_entities):
|
def test_rejects_end_before_start(base_entities):
|
||||||
customer, staff, service = base_entities
|
customer, staff, service = base_entities
|
||||||
client.force_login(customer)
|
client = APIClient()
|
||||||
|
client.force_authenticate(user=customer)
|
||||||
|
|
||||||
start_time = next_day_at(9)
|
start_time = next_day_at(9)
|
||||||
end_time = start_time - timedelta(minutes=service.duration_minutes)
|
end_time = start_time - timedelta(minutes=service.duration_minutes)
|
||||||
@@ -90,9 +111,10 @@ def test_rejects_end_before_start(client, base_entities):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_rejects_duration_mismatch(client, base_entities):
|
def test_rejects_duration_mismatch(base_entities):
|
||||||
customer, staff, service = base_entities
|
customer, staff, service = base_entities
|
||||||
client.force_login(customer)
|
client = APIClient()
|
||||||
|
client.force_authenticate(user=customer)
|
||||||
|
|
||||||
start_time = next_day_at(9)
|
start_time = next_day_at(9)
|
||||||
end_time = start_time + timedelta(minutes=30)
|
end_time = start_time + timedelta(minutes=30)
|
||||||
@@ -109,16 +131,18 @@ def test_rejects_duration_mismatch(client, base_entities):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_rejects_outside_availability_when_defined(client, base_entities):
|
def test_rejects_outside_availability_when_defined(base_entities):
|
||||||
customer, staff, service = base_entities
|
customer, staff, service = base_entities
|
||||||
client.force_login(customer)
|
client = APIClient()
|
||||||
|
client.force_authenticate(user=customer)
|
||||||
|
|
||||||
start_time = next_day_at(8)
|
start_time = next_day_at(8)
|
||||||
end_time = start_time + timedelta(minutes=service.duration_minutes)
|
end_time = start_time + timedelta(minutes=service.duration_minutes)
|
||||||
|
|
||||||
|
start_local = timezone.localtime(start_time)
|
||||||
StaffAvailability.objects.create(
|
StaffAvailability.objects.create(
|
||||||
staff=staff,
|
staff=staff,
|
||||||
day_of_week=start_time.weekday(),
|
day_of_week=start_local.weekday(),
|
||||||
start_time=timezone.datetime(2000, 1, 1, 9, 0).time(),
|
start_time=timezone.datetime(2000, 1, 1, 9, 0).time(),
|
||||||
end_time=timezone.datetime(2000, 1, 1, 17, 0).time(),
|
end_time=timezone.datetime(2000, 1, 1, 17, 0).time(),
|
||||||
)
|
)
|
||||||
@@ -135,9 +159,10 @@ def test_rejects_outside_availability_when_defined(client, base_entities):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_allows_booking_without_availability_records(client, base_entities):
|
def test_allows_booking_without_availability_records(base_entities):
|
||||||
customer, staff, service = base_entities
|
customer, staff, service = base_entities
|
||||||
client.force_login(customer)
|
client = APIClient()
|
||||||
|
client.force_authenticate(user=customer)
|
||||||
|
|
||||||
start_time = next_day_at(10)
|
start_time = next_day_at(10)
|
||||||
end_time = start_time + timedelta(minutes=service.duration_minutes)
|
end_time = start_time + timedelta(minutes=service.duration_minutes)
|
||||||
@@ -153,9 +178,10 @@ def test_allows_booking_without_availability_records(client, base_entities):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_rejects_overlapping_pending_or_confirmed_bookings(client, base_entities):
|
def test_rejects_overlapping_pending_or_confirmed_bookings(base_entities):
|
||||||
customer, staff, service = base_entities
|
customer, staff, service = base_entities
|
||||||
client.force_login(customer)
|
client = APIClient()
|
||||||
|
client.force_authenticate(user=customer)
|
||||||
|
|
||||||
start_time = next_day_at(9)
|
start_time = next_day_at(9)
|
||||||
end_time = start_time + timedelta(minutes=service.duration_minutes)
|
end_time = start_time + timedelta(minutes=service.duration_minutes)
|
||||||
@@ -188,9 +214,10 @@ def test_rejects_overlapping_pending_or_confirmed_bookings(client, base_entities
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_allows_overlap_with_cancelled_or_completed(client, base_entities):
|
def test_allows_overlap_with_cancelled_or_completed(base_entities):
|
||||||
customer, staff, service = base_entities
|
customer, staff, service = base_entities
|
||||||
client.force_login(customer)
|
client = APIClient()
|
||||||
|
client.force_authenticate(user=customer)
|
||||||
|
|
||||||
start_time = next_day_at(9)
|
start_time = next_day_at(9)
|
||||||
end_time = start_time + timedelta(minutes=service.duration_minutes)
|
end_time = start_time + timedelta(minutes=service.duration_minutes)
|
||||||
|
|||||||
Reference in New Issue
Block a user