init
Some checks failed
CI/CD / Backend & Frontend Checks (push) Has been cancelled
CI/CD / Deploy to Production (push) Has been cancelled

This commit is contained in:
2026-05-18 11:34:07 +03:30
commit 7a8ddeabed
279 changed files with 37390 additions and 0 deletions

View File

View File

View File

@@ -0,0 +1,540 @@
import io
import json
import tempfile
import uuid
from datetime import timedelta
from types import SimpleNamespace
from PIL import Image
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase, override_settings
from django.utils import timezone
from api.authentication import create_jwt_token
from api.schemas.events import (
EventSchema,
EventGallerySchema,
EventListSchema,
RegistrationSchema,
PaymentAdminSchema,
EventAdminDetailSchema,
)
from api.views.events import list_events
from events.models import Event, Registration
from gallery.models import Gallery
from payments.models import DiscountCode
from users.models import Major, University, User
MEDIA_ROOT = tempfile.mkdtemp()
@override_settings(MEDIA_ROOT=MEDIA_ROOT)
class EventsAPIIntegrationTests(TestCase):
password = "TestPass123!"
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(
username="event_user",
email="event.user@example.com",
password=cls.password,
)
cls.user.is_email_verified = True
cls.user.save(update_fields=["is_email_verified"])
cls.staff = User.objects.create_user(
username="event_staff",
email="event.staff@example.com",
password=cls.password,
is_staff=True,
)
cls.staff.is_email_verified = True
cls.staff.save(update_fields=["is_email_verified"])
cls.major, _ = Major.objects.get_or_create(code="CS", defaults={"name": "Computer Science"})
cls.university, _ = University.objects.get_or_create(code="UT", defaults={"name": "University of Tehran"})
cls.user.major = cls.major
cls.user.university = cls.university
cls.user.save(update_fields=["major", "university"])
cls.staff.major = cls.major
cls.staff.university = cls.university
cls.staff.save(update_fields=["major", "university"])
def setUp(self):
super().setUp()
self.token = create_jwt_token(self.user)
self.staff_token = create_jwt_token(self.staff)
self.event = self._create_event(
title="Integration Event",
description="Integration description.",
status=Event.StatusChoices.PUBLISHED,
price=0,
)
self.other_event = self._create_event(
title="Other Published",
description="Searchable",
status=Event.StatusChoices.PUBLISHED,
price=0,
)
def _auth_headers(self, token):
return {"HTTP_AUTHORIZATION": f"Bearer {token}"}
def _create_event(self, **overrides):
now = timezone.now()
defaults = {
"title": "Event Title",
"description": "Description",
"start_time": now,
"end_time": now + timedelta(hours=2),
"registration_start_date": now - timedelta(days=1),
"registration_end_date": now + timedelta(days=5),
"slug": f"event-{uuid.uuid4().hex[:6]}",
"location": "Campus",
"online_link": "https://meet.example.com",
"price": 0,
"capacity": 10,
"status": Event.StatusChoices.PUBLISHED,
}
defaults.update(overrides)
return Event.objects.create(**defaults)
def _create_gallery_image(self):
buffer = io.BytesIO()
Image.new("RGB", (10, 10), color="blue").save(buffer, format="PNG")
buffer.seek(0)
file = SimpleUploadedFile("gallery.png", buffer.read(), content_type="image/png")
return Gallery.objects.create(
title="Gallery image",
description="desc",
image=file,
uploaded_by=self.user,
)
def _create_paid_event(self):
return self._create_event(price=30000, capacity=5)
def _create_registration(self, event, user, status=Registration.StatusChoices.PENDING):
return Registration.objects.create(event=event, user=user, status=status, final_price=event.price)
# Basic event endpoints ------------------------------------------------
def test_list_events_filters_and_search(self):
# Act
response = self.client.get("/api/events/", {"status": "published", "search": "Searchable"})
data = response.json()
# Assert
self.assertEqual(response.status_code, 200)
self.assertTrue(any(item["id"] == self.other_event.id for item in data))
def test_get_event_by_id_and_slug(self):
response_id = self.client.get(f"/api/events/{self.event.id}")
response_slug = self.client.get(f"/api/events/slug/{self.event.slug}")
self.assertEqual(response_id.status_code, 200)
self.assertEqual(response_slug.status_code, 200)
self.assertEqual(response_id.json()["id"], self.event.id)
self.assertEqual(response_slug.json()["slug"], self.event.slug)
def test_create_update_and_delete_event(self):
payload = {
"title": "New Event",
"description": "Desc",
"start_time": (timezone.now() + timedelta(days=1)).isoformat(),
"end_time": (timezone.now() + timedelta(days=1, hours=1)).isoformat(),
"event_type": Event.TypeChoices.ON_SITE,
"status": Event.StatusChoices.DRAFT,
"price": 5000,
}
created = self.client.post(
"/api/events/",
data=json.dumps(payload),
content_type="application/json",
)
self.assertEqual(created.status_code, 200)
event_id = created.json()["id"]
updated = self.client.put(
f"/api/events/{event_id}",
data=json.dumps({"title": "Updated Event"}),
content_type="application/json",
)
self.assertEqual(updated.status_code, 200)
self.assertEqual(updated.json()["title"], "Updated Event")
deleted = self.client.delete(f"/api/events/{event_id}")
self.assertEqual(deleted.status_code, 200)
def test_admin_detail_and_registration_list_requires_staff(self):
staff_headers = self._auth_headers(self.staff_token)
user_headers = self._auth_headers(self.token)
_ = self._create_registration(self.event, self.user, status=Registration.StatusChoices.CONFIRMED)
# Non staff forbidden
list_resp = self.client.get(f"/api/events/{self.event.id}/admin-registrations", **user_headers)
self.assertEqual(list_resp.status_code, 403)
# Staff allowed
list_resp = self.client.get(f"/api/events/{self.event.id}/admin-registrations", **staff_headers)
detail_resp = self.client.get(f"/api/events/{self.event.id}/admin-detail", **staff_headers)
self.assertEqual(list_resp.status_code, 200)
self.assertEqual(detail_resp.status_code, 200)
def test_list_events_filters_by_event_type_and_search(self):
event = self._create_event(
title="Special Search",
description="Unique discovery",
event_type=Event.TypeChoices.ONLINE,
status=Event.StatusChoices.PUBLISHED,
)
response = self.client.get(
"/api/events/",
{
"event_type": Event.TypeChoices.ONLINE,
"search": "Unique discovery",
},
)
self.assertEqual(response.status_code, 200)
self.assertTrue(any(item["id"] == event.id for item in response.json()))
def test_list_events_handles_comma_status_parameter(self):
event = self._create_event(
title="Comma Event",
status=Event.StatusChoices.PUBLISHED,
)
results = list_events(
None,
status=f"{Event.StatusChoices.PUBLISHED},{Event.StatusChoices.DRAFT}",
event_type=None,
search=None,
limit=10,
offset=0,
)
self.assertIn(event, list(results))
def test_create_event_attaches_gallery_images(self):
gallery = self._create_gallery_image()
payload = {
"title": "Gallery Event",
"description": "Gallery desc",
"start_time": (timezone.now() + timedelta(days=1)).isoformat(),
"end_time": (timezone.now() + timedelta(days=1, hours=1)).isoformat(),
"event_type": Event.TypeChoices.ON_SITE,
"status": Event.StatusChoices.DRAFT,
"price": 5000,
"gallery_image_ids": [gallery.id],
}
response = self.client.post(
"/api/events/",
data=json.dumps(payload),
content_type="application/json",
)
body = response.json()
self.assertEqual(response.status_code, 200)
self.assertTrue(body["gallery_images"])
updated = self.client.put(
f"/api/events/{body['id']}",
data=json.dumps(
{
"title": "Gallery Event Updated",
"gallery_image_ids": [gallery.id],
}
),
content_type="application/json",
)
self.assertEqual(updated.status_code, 200)
self.assertEqual(updated.json()["slug"], "gallery-event-updated")
self.assertTrue(updated.json()["gallery_images"])
def test_admin_registration_filters_include_university_major_and_search(self):
event = self.event
self._create_registration(event, self.user, status=Registration.StatusChoices.CONFIRMED)
headers = self._auth_headers(self.staff_token)
response = self.client.get(
f"/api/events/{event.id}/admin-registrations",
{
"university": self.user.university.code,
"major": self.user.major.code,
"search": self.user.username,
"status": [Registration.StatusChoices.CONFIRMED, Registration.StatusChoices.PENDING],
},
**headers,
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["count"], 1)
def test_register_before_start_and_after_end_dates_fail(self):
future_event = self._create_event(registration_start_date=timezone.now() + timedelta(days=1))
future_response = self.client.post(
f"/api/events/{future_event.id}/register",
HTTP_AUTHORIZATION=f"Bearer {self.token}",
)
self.assertEqual(future_response.status_code, 400)
closed_event = self._create_event(registration_end_date=timezone.now() - timedelta(hours=1))
closed_response = self.client.post(
f"/api/events/{closed_event.id}/register",
HTTP_AUTHORIZATION=f"Bearer {self.token}",
)
self.assertEqual(closed_response.status_code, 400)
def test_register_recreates_after_cancelled_registration(self):
event = self._create_event(price=0)
Registration.objects.create(
event=event,
user=self.user,
status=Registration.StatusChoices.CANCELLED,
final_price=0,
)
response = self.client.post(
f"/api/events/{event.id}/register",
HTTP_AUTHORIZATION=f"Bearer {self.token}",
content_type="application/json",
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["status"], Registration.StatusChoices.CONFIRMED)
def test_register_updates_final_price_when_none(self):
event = self._create_paid_event()
registration = Registration.objects.create(
event=event,
user=self.user,
status=Registration.StatusChoices.PENDING,
final_price=None,
)
response = self.client.post(
f"/api/events/{event.id}/register",
HTTP_AUTHORIZATION=f"Bearer {self.token}",
content_type="application/json",
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["final_price"], event.price)
def _create_discount_code(self, event):
code = DiscountCode.objects.create(
code=f"CODE-{uuid.uuid4().hex[:4]}",
value=50,
type=DiscountCode.Type.PERCENT,
is_active=True,
)
code.applicable_events.add(event)
return code
def test_register_for_event_with_free_price_confirms(self):
event = self._create_event(price=0)
response = self.client.post(
f"/api/events/{event.id}/register",
HTTP_AUTHORIZATION=f"Bearer {self.token}",
content_type="application/json",
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["status"], Registration.StatusChoices.CONFIRMED)
def test_register_for_event_with_discount_updates_final_price(self):
event = self._create_paid_event()
code = self._create_discount_code(event)
response = self.client.post(
f"/api/events/{event.id}/register",
data=json.dumps({"discount_code": code.code}),
content_type="application/json",
HTTP_AUTHORIZATION=f"Bearer {self.token}",
)
result = response.json()
self.assertEqual(response.status_code, 200)
self.assertEqual(result["discount_code"], code.code)
self.assertEqual(result["discount_amount"], event.price // 2)
self.assertEqual(result["final_price"], event.price // 2)
def test_register_fails_when_capacity_full(self):
event = self._create_event(capacity=1)
other = self._create_event_user("other_user", "other@example.com")
Registration.objects.create(
event=event,
user=other,
status=Registration.StatusChoices.CONFIRMED,
final_price=0,
)
response = self.client.post(
f"/api/events/{event.id}/register",
HTTP_AUTHORIZATION=f"Bearer {self.token}",
content_type="application/json",
)
self.assertEqual(response.status_code, 400)
def _create_event_user(self, username, email):
user = User.objects.create_user(username=username, email=email, password=self.password)
user.is_email_verified = True
user.save(update_fields=["is_email_verified"])
user.major = self.user.major
user.university = self.user.university
user.save(update_fields=["major", "university"])
return user
def test_register_rejects_duplicate_confirmed(self):
event = self._create_event(price=0)
Registration.objects.create(
event=event,
user=self.user,
status=Registration.StatusChoices.CONFIRMED,
final_price=0,
)
response = self.client.post(
f"/api/events/{event.id}/register",
HTTP_AUTHORIZATION=f"Bearer {self.token}",
content_type="application/json",
)
self.assertEqual(response.status_code, 400)
def test_registration_status_update_and_cancel(self):
event = self._create_event(price=0)
registration = self._create_registration(event, self.user)
update = self.client.put(
f"/api/events/registrations/{registration.id}",
data=json.dumps({"status": Registration.StatusChoices.ATTENDED}),
content_type="application/json",
HTTP_AUTHORIZATION=f"Bearer {self.token}",
)
self.assertEqual(update.status_code, 200)
self.assertEqual(update.json()["status"], Registration.StatusChoices.ATTENDED)
cancel = self.client.delete(
f"/api/events/registrations/{registration.id}",
HTTP_AUTHORIZATION=f"Bearer {self.token}",
)
self.assertEqual(cancel.status_code, 200)
self.assertEqual(cancel.json()["message"], "ثبت‌نام شما لغو شد :(")
def test_verify_registration_and_my_registrations(self):
event = self._create_event(price=0)
registration = self._create_registration(event, self.user, status=Registration.StatusChoices.CONFIRMED)
verify = self.client.get(
f"/api/events/registerations/verify/{registration.ticket_id}",
HTTP_AUTHORIZATION=f"Bearer {self.token}",
)
self.assertEqual(verify.status_code, 200)
self.assertEqual(verify.json()["ticket_id"], str(registration.ticket_id))
my_regs = self.client.get(
"/api/events/my-registrations",
HTTP_AUTHORIZATION=f"Bearer {self.token}",
)
self.assertEqual(my_regs.status_code, 200)
self.assertGreater(len(my_regs.json()), 0)
status_resp = self.client.get(
f"/api/events/{event.id}/is-registered",
HTTP_AUTHORIZATION=f"Bearer {self.token}",
)
self.assertEqual(status_resp.status_code, 200)
self.assertTrue(status_resp.json()["is_registered"])
def test_list_event_registrations(self):
event = self.event
self._create_registration(event, self.user)
response = self.client.get(f"/api/events/{event.id}/registrations")
self.assertEqual(response.status_code, 200)
self.assertTrue(response.json())
def test_list_event_registrations_admin_filters(self):
event = self.event
self._create_registration(event, self.user, status=Registration.StatusChoices.PENDING)
headers = self._auth_headers(self.staff_token)
response = self.client.get(
f"/api/events/{event.id}/admin-registrations",
{"status": [Registration.StatusChoices.PENDING]},
**headers,
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["count"], 1)
class EventSchemasIntegrationTests(TestCase):
password = "SchemaPass!123"
def setUp(self):
self.user = User.objects.create_user(
username="schema_user",
email="schema.user@example.com",
password=self.password,
)
self.user.is_email_verified = True
self.user.save(update_fields=["is_email_verified"])
self.event = Event.objects.create(
title="Schema Event",
description="**bold**",
start_time=timezone.now(),
end_time=timezone.now() + timedelta(hours=1),
registration_start_date=timezone.now() - timedelta(days=1),
registration_end_date=timezone.now() + timedelta(days=1),
price=1000,
slug="schema-event",
)
Registration.objects.create(
event=self.event,
user=self.user,
status=Registration.StatusChoices.CONFIRMED,
final_price=0,
)
Registration.objects.create(
event=self.event,
user=self.user,
status=Registration.StatusChoices.ATTENDED,
final_price=0,
)
def _mock_request(self):
return SimpleNamespace(build_absolute_uri=lambda path: f"https://test{path}")
def test_gallery_schema_returns_full_url(self):
obj = SimpleNamespace(image=SimpleNamespace(url="/media/gallery.png"))
result = EventGallerySchema.resolve_absolute_image_url(obj, {"request": self._mock_request()})
self.assertEqual(result, "https://test/media/gallery.png")
def test_event_schema_resolvers(self):
context = {"request": self._mock_request()}
event_obj = SimpleNamespace(featured_image=SimpleNamespace(url="/media/feat.png"), registrations=self.event.registrations)
self.assertEqual(EventSchema.resolve_absolute_featured_image_url(event_obj, context), "https://test/media/feat.png")
self.assertEqual(EventSchema.resolve_registration_count(self.event), 2)
self.assertIn("<p>", EventSchema.resolve_description_html(self.event))
def test_event_list_schema_resolvers(self):
obj = SimpleNamespace(featured_image=SimpleNamespace(url="/media/feat.png"), registrations=self.event.registrations)
context = {"request": self._mock_request()}
self.assertEqual(EventListSchema.resolve_absolute_featured_image_url(obj, context), "https://test/media/feat.png")
self.assertEqual(EventListSchema.resolve_registration_count(self.event), 2)
def test_registration_schema_resolves_discount_code(self):
discount = DiscountCode.objects.create(code="SCHEMA", type=DiscountCode.Type.FIXED, value=100, is_active=True)
discount.applicable_events.add(self.event)
registration = Registration.objects.create(
event=self.event,
user=self.user,
status=Registration.StatusChoices.CONFIRMED,
final_price=900,
discount_code=discount,
)
self.assertEqual(RegistrationSchema.resolve_discount_code(registration), discount.code)
def test_payment_admin_schema_normalizes_discount_code(self):
self.assertIsNone(PaymentAdminSchema.normalize_discount_code(None))
self.assertEqual(PaymentAdminSchema.normalize_discount_code("123"), "123")
self.assertEqual(PaymentAdminSchema.normalize_discount_code(SimpleNamespace(code="ABC")), "ABC")
def test_event_admin_detail_resolves_registrations(self):
registrations = EventAdminDetailSchema.resolve_registrations(self.event)
self.assertTrue(list(registrations))
# TODO registration-related tests

View File

@@ -0,0 +1,282 @@
import json
from datetime import timedelta
from unittest import mock
from django.test import TestCase, override_settings
from django.utils import timezone
from api.authentication import create_jwt_token
from events.models import Event, Registration
from payments.models import Payment, DiscountCode
from users.models import User
@override_settings(
ZARINPAL_MERCHANT_ID="MID",
ZARINPAL_REQUEST_URL="https://zarinpal/request",
ZARINPAL_STARTPAY="https://zarinpal/start/",
ZARINPAL_VERIFY_URL="https://zarinpal/verify",
ZARINPAL_CALLBACK_URL="https://frontend/callback",
)
class PaymentsAPIIntegrationTests(TestCase):
password = "PaymentPass!123"
@classmethod
def setUpTestData(cls):
cls.user = User.objects.create_user(
username="pay_user",
email="pay.user@example.com",
password=cls.password,
)
cls.user.is_email_verified = True
cls.user.save(update_fields=["is_email_verified"])
def setUp(self):
super().setUp()
self.event = Event.objects.create(
title="Pay Event",
description="Payment event",
start_time=timezone.now(),
end_time=timezone.now() + timedelta(hours=2),
registration_start_date=timezone.now() - timedelta(days=1),
registration_end_date=timezone.now() + timedelta(days=1),
slug="pay-event",
price=50000,
capacity=10,
status=Event.StatusChoices.PUBLISHED,
)
self.token = create_jwt_token(self.user)
def _headers(self):
return {"HTTP_AUTHORIZATION": f"Bearer {self.token}"}
def _create_paid_event(self):
return Event.objects.create(
title="Paid Event",
description="Paid",
start_time=timezone.now(),
end_time=timezone.now() + timedelta(hours=1),
registration_start_date=timezone.now() - timedelta(days=1),
registration_end_date=timezone.now() + timedelta(days=2),
slug=f"paid-{timezone.now().timestamp()}",
price=20000,
capacity=5,
status=Event.StatusChoices.PUBLISHED,
)
def _create_discount_code(self, event):
code = DiscountCode.objects.create(
code="DISC50",
value=50,
is_active=True,
)
code.applicable_events.add(event)
return code
def test_create_payment_for_free_event(self):
free = Event.objects.create(
title="Free",
description="Zero",
start_time=timezone.now(),
end_time=timezone.now() + timedelta(hours=1),
registration_start_date=timezone.now() - timedelta(days=1),
registration_end_date=timezone.now() + timedelta(days=1),
slug="free-event",
price=0,
capacity=10,
status=Event.StatusChoices.PUBLISHED,
)
response = self.client.post(
"/api/payments/create",
data=json.dumps(
{
"event_id": free.id,
"description": "Free registration",
}
),
content_type="application/json",
**self._headers(),
)
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(data["amount"], 0)
self.assertIsNone(data["start_pay_url"])
@mock.patch("api.views.payments.requests.post")
def test_create_payment_with_discount(self, mock_post):
mock_response = mock.Mock()
mock_response.json.return_value = {"data": {"code": 100, "authority": "AUTH"}}
mock_post.return_value = mock_response
code = self._create_discount_code(self.event)
response = self.client.post(
"/api/payments/create",
data=json.dumps(
{
"event_id": self.event.id,
"description": "Pay with discount",
"discount_code": code.code,
}
),
content_type="application/json",
**self._headers(),
)
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["discount_amount"], self.event.price // 2)
self.assertEqual(payload["amount"], self.event.price // 2)
self.assertIn("start_pay_url", payload)
payment = Payment.objects.get(user=self.user, event=self.event)
self.assertEqual(payment.discount_code, code)
@mock.patch("api.views.payments.requests.post")
def test_callback_success_marks_paid(self, mock_post):
payment = Payment.objects.create(
user=self.user,
event=self.event,
base_amount=self.event.price,
amount=self.event.price,
status=Payment.OrderStatusChoices.PENDING,
authority="AUTH123",
)
mock_resp = mock.Mock()
mock_resp.json.return_value = {"data": {"code": 100, "ref_id": "REF", "card_pan": "123", "card_hash": "ABC"}}
mock_post.return_value = mock_resp
response = self.client.get(
"/api/payments/callback",
{"Authority": "AUTH123", "Status": "OK"},
)
payment.refresh_from_db()
self.assertEqual(payment.status, Payment.OrderStatusChoices.PAID)
self.assertTrue("status=success" in response.url)
@mock.patch("api.views.payments.requests.post")
def test_callback_failure_redirects_failed(self, mock_post):
payment = Payment.objects.create(
user=self.user,
event=self.event,
base_amount=self.event.price,
amount=self.event.price,
status=Payment.OrderStatusChoices.PENDING,
authority="AUTH456",
)
mock_resp = mock.Mock()
mock_resp.json.return_value = {"data": {"code": 101, "ref_id": "REF"}}
mock_post.return_value = mock_resp
response = self.client.get(
"/api/payments/callback",
{"Authority": "AUTH456", "Status": "OK"},
)
payment.refresh_from_db()
self.assertEqual(payment.status, Payment.OrderStatusChoices.PAID)
self.assertTrue("status=success" in response.url)
def test_callback_missing_authority_returns_error(self):
response = self.client.get("/api/payments/callback", {"Status": "OK"})
self.assertEqual(response.status_code, 400)
def test_callback_not_ok_cancels(self):
payment = Payment.objects.create(
user=self.user,
event=self.event,
base_amount=self.event.price,
amount=self.event.price,
status=Payment.OrderStatusChoices.PENDING,
authority="AUTH789",
)
response = self.client.get(
"/api/payments/callback",
{"Authority": "AUTH789", "Status": "NOK"},
)
payment.refresh_from_db()
self.assertEqual(payment.status, Payment.OrderStatusChoices.CANCELED)
self.assertIn("status=failed", response.url)
@mock.patch("api.views.payments.requests.post", side_effect=RuntimeError("down"))
def test_create_payment_gateway_failure(self, mock_post):
response = self.client.post(
"/api/payments/create",
data=json.dumps(
{
"event_id": self.event.id,
"description": "Gateway fail",
}
),
content_type="application/json",
**self._headers(),
)
self.assertEqual(response.status_code, 502)
self.assertFalse(Payment.objects.filter(user=self.user).exists())
def test_create_payment_when_already_paid(self):
Payment.objects.create(
user=self.user,
event=self.event,
base_amount=self.event.price,
amount=self.event.price,
status=Payment.OrderStatusChoices.PAID,
)
response = self.client.post(
"/api/payments/create",
data=json.dumps({"event_id": self.event.id, "description": "Duplicate"}),
content_type="application/json",
**self._headers(),
)
self.assertEqual(response.status_code, 400)
@mock.patch("api.views.payments.requests.post")
def test_registration_final_price_none_updates(self, mock_post):
registration = Registration.objects.create(
event=self.event,
user=self.user,
status=Registration.StatusChoices.PENDING,
final_price=None,
)
mock_response = mock.Mock()
mock_response.json.return_value = {"data": {"code": 100, "authority": "AUTH"}}
mock_post.return_value = mock_response
response = self.client.post(
"/api/payments/create",
data=json.dumps({"event_id": self.event.id, "description": "Update"}),
content_type="application/json",
**self._headers(),
)
self.assertEqual(response.status_code, 200)
registration.refresh_from_db()
if registration.final_price is None:
self.fail("final_price should be populated")
def test_coupon_check_success_and_errors(self):
code = DiscountCode.objects.create(code="PAYCO", value=20, is_active=True, type=DiscountCode.Type.PERCENT)
code.applicable_events.add(self.event)
# missing code
missing = self.client.post(
"/api/payments/coupon/check",
data=json.dumps({"event_id": self.event.id}),
content_type="application/json",
**self._headers(),
)
self.assertEqual(missing.status_code, 422)
# invalid code
invalid = self.client.post(
"/api/payments/coupon/check",
data=json.dumps({"event_id": self.event.id, "code": "INVALID"}),
content_type="application/json",
**self._headers(),
)
self.assertEqual(invalid.status_code, 404)
success = self.client.post(
"/api/payments/coupon/check",
data=json.dumps({"event_id": self.event.id, "code": code.code}),
content_type="application/json",
**self._headers(),
)
self.assertEqual(success.status_code, 200)
self.assertIn("final_price", success.json())

View File

@@ -0,0 +1,724 @@
import json
import shutil
import tempfile
import uuid
from datetime import timedelta
from unittest import mock
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase, override_settings
from django.utils import timezone
import jwt
from users.models import User, Major, University
class UsersAPIIntegrationTests(TestCase):
password = "Sup3rSecure!123"
@classmethod
def setUpTestData(cls):
super().setUpTestData()
cls.major_cs, _ = Major.objects.get_or_create(
code="CS", defaults={"name": "Computer Science"}
)
cls.major_gil, _ = Major.objects.get_or_create(
code="GIL_CS", defaults={"name": "Gilan Computer Science"}
)
cls.university_ut, _ = University.objects.get_or_create(
code="UT", defaults={"name": "University of Tehran"}
)
cls.university_gilan, _ = University.objects.get_or_create(
code="GILAN", defaults={"name": "Gilan University"}
)
def setUp(self):
super().setUp()
patchers = [
mock.patch("users.tasks.send_verification_email.delay"),
mock.patch("users.signals.send_verification_email.delay"),
mock.patch("users.tasks.send_password_reset_email.delay"),
]
(
self.mock_send_verification_task,
self.mock_signal_verification_task,
self.mock_password_reset_task,
) = [patcher.start() for patcher in patchers]
for patcher in patchers:
self.addCleanup(patcher.stop)
# Helper utilities -----------------------------------------------------
def _numeric_student_id(self) -> str:
return str(uuid.uuid4().int)[-10:]
def _resolve_major(self, value):
if value is None:
return None
if isinstance(value, Major):
return value
return Major.objects.filter(code=value).first()
def _resolve_university(self, value):
if value is None:
return None
if isinstance(value, University):
return value
return University.objects.filter(code=value).first()
def _create_user(self, **overrides) -> User:
unique = uuid.uuid4().hex[:8]
defaults = {
"username": f"user_{unique}",
"email": f"{unique}@example.com",
"student_id": self._numeric_student_id(),
"first_name": "Test",
"last_name": "User",
"year_of_study": 2,
"major": self.major_cs,
"university": self.university_ut,
}
defaults.update(overrides)
if isinstance(defaults.get("major"), str):
defaults["major"] = self._resolve_major(defaults["major"])
if isinstance(defaults.get("university"), str):
defaults["university"] = self._resolve_university(defaults["university"])
password = defaults.pop("password", self.password)
return User.objects.create_user(password=password, **defaults)
def _auth_headers(self, token: str) -> dict:
return {"HTTP_AUTHORIZATION": f"Bearer {token}"}
def _login_and_get_tokens(self, user: User, password: str | None = None) -> dict:
response = self.client.post(
"/api/auth/login",
data=json.dumps({"email": user.email, "password": password or self.password}),
content_type="application/json",
)
self.assertEqual(response.status_code, 200)
return response.json()
def _refresh_token_value(self, user: User | None = None, **overrides) -> str:
now = timezone.now()
payload = {
"type": "refresh",
"exp": now + timedelta(minutes=5),
"iat": now,
}
if user is not None:
payload["user_id"] = user.id
payload.update(overrides)
return jwt.encode(payload, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
# Registration ---------------------------------------------------------
def test_register_creates_user_and_enqueues_signal(self):
# Arrange
payload = {
"username": "integration_user",
"email": "integration@example.com",
"password": "RegisterPass!9",
"student_id": "2023123456",
"first_name": "Integration",
"last_name": "Tester",
"university": self.university_ut.code,
"major": self.major_cs.code,
"year_of_study": 3,
}
# Act
response = self.client.post(
"/api/auth/register", data=json.dumps(payload), content_type="application/json"
)
# Assert
self.assertEqual(response.status_code, 201)
self.assertTrue(User.objects.filter(email=payload["email"]).exists())
self.assertTrue(self.mock_signal_verification_task.called)
def test_register_rejects_short_student_id(self):
# Arrange
payload = {
"username": "short_id",
"email": "short@example.com",
"password": "RegisterPass!9",
"student_id": "123456789", # 9 digits
}
# Act
response = self.client.post(
"/api/auth/register", data=json.dumps(payload), content_type="application/json"
)
# Assert
self.assertEqual(response.status_code, 400)
def test_register_rejects_duplicate_username(self):
# Arrange
existing = self._create_user(username="duplicate")
payload = {
"username": existing.username,
"email": "someone@example.com",
"password": "RegisterPass!9",
}
# Act
response = self.client.post(
"/api/auth/register", data=json.dumps(payload), content_type="application/json"
)
# Assert
self.assertEqual(response.status_code, 400)
def test_register_rejects_duplicate_email(self):
# Arrange
existing = self._create_user(email="duplicate@example.com")
payload = {
"username": "newuser",
"email": existing.email,
"password": "RegisterPass!9",
}
# Act
response = self.client.post(
"/api/auth/register", data=json.dumps(payload), content_type="application/json"
)
# Assert
self.assertEqual(response.status_code, 400)
def test_register_rejects_duplicate_student_id_in_same_university(self):
# Arrange
student_id = "2023012345"
self._create_user(student_id=student_id, university=self.university_gilan)
payload = {
"username": "dupstudent",
"email": "dupstudent@example.com",
"password": "RegisterPass!9",
"student_id": student_id,
"university": self.university_gilan.code,
}
# Act
response = self.client.post(
"/api/auth/register", data=json.dumps(payload), content_type="application/json"
)
# Assert
self.assertEqual(response.status_code, 400)
# Login & Refresh ------------------------------------------------------
def test_login_returns_tokens_for_verified_user(self):
# Arrange
user = self._create_user()
user.is_email_verified = True
user.save(update_fields=["is_email_verified"])
# Act
response = self.client.post(
"/api/auth/login",
data=json.dumps({"email": user.email, "password": self.password}),
content_type="application/json",
)
# Assert
self.assertEqual(response.status_code, 200)
body = response.json()
self.assertIn("access_token", body)
self.assertIn("refresh_token", body)
def test_login_rejects_unverified_user(self):
# Arrange
user = self._create_user()
# Act
response = self.client.post(
"/api/auth/login",
data=json.dumps({"email": user.email, "password": self.password}),
content_type="application/json",
)
# Assert
self.assertEqual(response.status_code, 401)
def test_login_rejects_inactive_user(self):
# Arrange
user = self._create_user(is_email_verified=True, is_active=False)
# Act
response = self.client.post(
"/api/auth/login",
data=json.dumps({"email": user.email, "password": self.password}),
content_type="application/json",
)
# Assert
self.assertEqual(response.status_code, 401)
def test_refresh_returns_tokens(self):
# Arrange
user = self._create_user(is_email_verified=True)
tokens = self._login_and_get_tokens(user)
# Act
response = self.client.post(
"/api/auth/refresh",
data=json.dumps({"refresh_token": tokens["refresh_token"]}),
content_type="application/json",
)
# Assert
self.assertEqual(response.status_code, 200)
refreshed = response.json()
self.assertIn("access_token", refreshed)
self.assertIn("refresh_token", refreshed)
def test_refresh_rejects_non_refresh_token(self):
# Arrange
user = self._create_user(is_email_verified=True)
tokens = self._login_and_get_tokens(user)
# Act
response = self.client.post(
"/api/auth/refresh",
data=json.dumps({"refresh_token": tokens["access_token"]}),
content_type="application/json",
)
# Assert
self.assertEqual(response.status_code, 401)
def test_refresh_rejects_missing_user_id(self):
# Arrange
token = self._refresh_token_value()
# Act
response = self.client.post(
"/api/auth/refresh",
data=json.dumps({"refresh_token": token}),
content_type="application/json",
)
# Assert
self.assertEqual(response.status_code, 401)
def test_refresh_rejects_unverified_user(self):
# Arrange
user = self._create_user()
token = self._refresh_token_value(user=user)
# Act
response = self.client.post(
"/api/auth/refresh",
data=json.dumps({"refresh_token": token}),
content_type="application/json",
)
# Assert
self.assertEqual(response.status_code, 401)
def test_refresh_rejects_inactive_user(self):
# Arrange
user = self._create_user(is_email_verified=True, is_active=False)
token = self._refresh_token_value(user=user)
# Act
response = self.client.post(
"/api/auth/refresh",
data=json.dumps({"refresh_token": token}),
content_type="application/json",
)
# Assert
self.assertEqual(response.status_code, 401)
def test_refresh_rejects_expired_token(self):
# Arrange
user = self._create_user(is_email_verified=True)
token = self._refresh_token_value(
user=user,
exp=timezone.now() - timedelta(minutes=1),
)
# Act
response = self.client.post(
"/api/auth/refresh",
data=json.dumps({"refresh_token": token}),
content_type="application/json",
)
# Assert
self.assertEqual(response.status_code, 401)
def test_refresh_rejects_invalid_token_string(self):
# Arrange
token = "not-a-valid-token"
# Act
response = self.client.post(
"/api/auth/refresh",
data=json.dumps({"refresh_token": token}),
content_type="application/json",
)
# Assert
self.assertEqual(response.status_code, 401)
# Email verification ---------------------------------------------------
def test_verify_email_marks_user_verified(self):
# Arrange
user = self._create_user()
token = str(user.email_verification_token)
# Act
response = self.client.get(f"/api/auth/verify-email/{token}")
# Assert
self.assertEqual(response.status_code, 200)
user.refresh_from_db()
self.assertTrue(user.is_email_verified)
def test_verify_email_rejects_unknown_token(self):
# Arrange
token = uuid.uuid4()
# Act
response = self.client.get(f"/api/auth/verify-email/{token}")
# Assert
self.assertEqual(response.status_code, 404)
def test_resend_verification_rejects_unknown_email(self):
# Arrange
payload = {"email": "missing@example.com"}
# Act
response = self.client.post(f"/api/auth/resend-verification?email={payload['email']}")
# Assert
self.assertEqual(response.status_code, 404)
# Profiles -------------------------------------------------------------
def test_get_profile_returns_schema_fields(self):
# Arrange
user = self._create_user(major=self.major_cs, university=self.university_gilan)
user.is_email_verified = True
user.save(update_fields=["is_email_verified"])
tokens = self._login_and_get_tokens(user)
# Act
response = self.client.get("/api/auth/profile", **self._auth_headers(tokens["access_token"]))
# Assert
self.assertEqual(response.status_code, 200)
profile = response.json()
self.assertEqual(profile["major"], user.get_major_display())
self.assertEqual(profile["university"], user.get_university_display())
def test_get_profile_requires_authentication(self):
# Arrange
# No token supplied.
# Act
response = self.client.get("/api/auth/profile")
# Assert
self.assertEqual(response.status_code, 401)
def test_update_profile_persists_changes(self):
# Arrange
user = self._create_user(is_email_verified=True)
tokens = self._login_and_get_tokens(user)
payload = {"bio": "Updated bio", "year_of_study": 4}
# Act
response = self.client.put(
"/api/auth/profile",
data=json.dumps(payload),
content_type="application/json",
**self._auth_headers(tokens["access_token"]),
)
# Assert
self.assertEqual(response.status_code, 200)
user.refresh_from_db()
self.assertEqual(user.bio, payload["bio"])
self.assertEqual(user.year_of_study, payload["year_of_study"])
@override_settings(MEDIA_URL="/media/", MEDIA_ROOT=tempfile.gettempdir())
def test_upload_profile_picture_succeeds(self):
# Arrange
user = self._create_user(is_email_verified=True)
tokens = self._login_and_get_tokens(user)
image = SimpleUploadedFile(
"avatar.png", b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR", content_type="image/png"
)
# Act
response = self.client.post(
"/api/auth/profile/picture", {"file": image}, **self._auth_headers(tokens["access_token"])
)
# Assert
self.assertEqual(response.status_code, 200)
profile = self.client.get(
"/api/auth/profile", **self._auth_headers(tokens["access_token"])
).json()
self.assertIn("profile_pictures", profile["profile_picture"])
def test_upload_profile_picture_requires_file(self):
# Arrange
user = self._create_user(is_email_verified=True)
tokens = self._login_and_get_tokens(user)
# Act
response = self.client.post(
"/api/auth/profile/picture", **self._auth_headers(tokens["access_token"])
)
# Assert
self.assertEqual(response.status_code, 400)
def test_upload_profile_picture_rejects_invalid_type(self):
# Arrange
user = self._create_user(is_email_verified=True)
tokens = self._login_and_get_tokens(user)
text_file = SimpleUploadedFile("doc.txt", b"text", content_type="text/plain")
# Act
response = self.client.post(
"/api/auth/profile/picture",
{"file": text_file},
**self._auth_headers(tokens["access_token"]),
)
# Assert
self.assertEqual(response.status_code, 400)
def test_upload_profile_picture_rejects_large_files(self):
# Arrange
user = self._create_user(is_email_verified=True)
tokens = self._login_and_get_tokens(user)
large_content = b"x" * (5 * 1024 * 1024 + 1)
large_file = SimpleUploadedFile("large.png", large_content, content_type="image/png")
# Act
response = self.client.post(
"/api/auth/profile/picture",
{"file": large_file},
**self._auth_headers(tokens["access_token"]),
)
# Assert
self.assertEqual(response.status_code, 400)
def test_delete_profile_picture_removes_file(self):
# Arrange
temp_media = tempfile.mkdtemp()
self.addCleanup(lambda: shutil.rmtree(temp_media, ignore_errors=True))
user = self._create_user(is_email_verified=True)
tokens = self._login_and_get_tokens(user)
with override_settings(MEDIA_ROOT=temp_media, MEDIA_URL="/media/"):
image = SimpleUploadedFile("avatar.png", b"data", content_type="image/png")
self.client.post(
"/api/auth/profile/picture",
{"file": image},
**self._auth_headers(tokens["access_token"]),
)
# Act
response = self.client.delete(
"/api/auth/profile/picture", **self._auth_headers(tokens["access_token"])
)
# Assert
self.assertEqual(response.status_code, 200)
user.refresh_from_db()
self.assertFalse(bool(user.profile_picture))
# Password reset ------------------------------------------------------
def test_request_password_reset_enqueues_email(self):
# Arrange
user = self._create_user()
# Act
response = self.client.post(
"/api/auth/request-password-reset",
data=json.dumps({"email": user.email}),
content_type="application/json",
)
# Assert
self.assertEqual(response.status_code, 200)
user.refresh_from_db()
self.assertIsNotNone(user.password_reset_token)
self.mock_password_reset_task.assert_called_once()
def test_request_password_reset_unknown_email_returns_error(self):
# Arrange
payload = {"email": "missing@example.com"}
# Act
response = self.client.post(
"/api/auth/request-password-reset",
data=json.dumps(payload),
content_type="application/json",
)
# Assert
self.assertEqual(response.status_code, 400)
def test_reset_password_confirm_updates_credentials(self):
# Arrange
user = self._create_user()
user.set_password_reset_token()
payload = {"token": str(user.password_reset_token), "new_password": "BrandNewPass!9"}
# Act
response = self.client.post(
"/api/auth/reset-password-confirm",
data=json.dumps(payload),
content_type="application/json",
)
# Assert
self.assertEqual(response.status_code, 200)
user.refresh_from_db()
self.assertIsNone(user.password_reset_token)
self.assertTrue(user.check_password(payload["new_password"]))
def test_reset_password_confirm_rejects_expired_token(self):
# Arrange
user = self._create_user()
user.set_password_reset_token()
user.password_reset_token_expires_at = timezone.now() - timedelta(minutes=1)
user.save(update_fields=["password_reset_token_expires_at"])
payload = {"token": str(user.password_reset_token), "new_password": "New!!!Pass"}
# Act
response = self.client.post(
"/api/auth/reset-password-confirm",
data=json.dumps(payload),
content_type="application/json",
)
# Assert
self.assertEqual(response.status_code, 400)
def test_reset_password_confirm_rejects_unknown_token(self):
# Arrange
payload = {"token": str(uuid.uuid4()), "new_password": "AnotherPass!9"}
# Act
response = self.client.post(
"/api/auth/reset-password-confirm",
data=json.dumps(payload),
content_type="application/json",
)
# Assert
self.assertEqual(response.status_code, 400)
# Admin utilities -----------------------------------------------------
def test_list_deleted_users_requires_privileged_user(self):
# Arrange
user = self._create_user(is_email_verified=True)
tokens = self._login_and_get_tokens(user)
# Act
response = self.client.get(
"/api/auth/users/deleted", **self._auth_headers(tokens["access_token"])
)
# Assert
self.assertEqual(response.status_code, 403)
def test_list_deleted_users_returns_payload_for_staff(self):
# Arrange
deleted = self._create_user(is_deleted=True, deleted_at=timezone.now())
staff = self._create_user(is_email_verified=True, is_staff=True)
tokens = self._login_and_get_tokens(staff)
# Act
response = self.client.get(
"/api/auth/users/deleted", **self._auth_headers(tokens["access_token"])
)
# Assert
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertTrue(any(item["id"] == deleted.id for item in payload))
def test_restore_user_requires_privileged_user(self):
# Arrange
target = self._create_user(is_deleted=True, deleted_at=timezone.now())
user = self._create_user(is_email_verified=True)
tokens = self._login_and_get_tokens(user)
# Act
response = self.client.post(
f"/api/auth/users/{target.id}/restore", **self._auth_headers(tokens["access_token"])
)
# Assert
self.assertEqual(response.status_code, 403)
def test_restore_user_restores_record_for_staff(self):
# Arrange
target = self._create_user(is_deleted=True, deleted_at=timezone.now())
staff = self._create_user(is_email_verified=True, is_staff=True)
tokens = self._login_and_get_tokens(staff)
# Act
response = self.client.post(
f"/api/auth/users/{target.id}/restore", **self._auth_headers(tokens["access_token"])
)
# Assert
self.assertEqual(response.status_code, 200)
target.refresh_from_db()
self.assertFalse(target.is_deleted)
def test_restore_user_missing_returns_error(self):
# Arrange
staff = self._create_user(is_email_verified=True, is_staff=True)
tokens = self._login_and_get_tokens(staff)
# Act
response = self.client.post(
"/api/auth/users/999/restore", **self._auth_headers(tokens["access_token"])
)
# Assert
self.assertEqual(response.status_code, 400)
# Username checks ------------------------------------------------------
def test_check_username_reports_existing(self):
# Arrange
user = self._create_user()
# Act
response = self.client.get("/api/auth/check-username", {"username": user.username})
# Assert
self.assertEqual(response.status_code, 200)
self.assertTrue(response.json()["exists"])
def test_check_username_reports_availability(self):
# Arrange
username = "available_user"
# Act
response = self.client.get("/api/auth/check-username", {"username": username})
# Assert
self.assertEqual(response.status_code, 200)
self.assertFalse(response.json()["exists"])

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,194 @@
import uuid
from datetime import timedelta
from types import SimpleNamespace
from django.core.exceptions import ValidationError
from django.test import TestCase
from django.utils import timezone
from django.contrib.admin import AdminSite
from payments.admin import DiscountCodeAdmin
from payments.models import DiscountCode, Payment
from payments.resources import DiscountResource, PaymentResource
from events.models import Event
from users.models import User
from ninja.errors import HttpError
class PaymentTestMixin:
@staticmethod
def _create_user(**overrides):
data = {
"username": f"user_{uuid.uuid4().hex[:6]}",
"email": f"user_{uuid.uuid4().hex[:6]}@example.com",
"password": "Test!1234",
}
data.update(overrides)
return User.objects.create_user(**data)
@staticmethod
def _create_event(**overrides):
now = timezone.now()
defaults = {
"title": "Sample",
"description": "Desc",
"start_time": now,
"end_time": now + timedelta(hours=2),
"registration_start_date": now - timedelta(days=1),
"registration_end_date": now + timedelta(days=5),
"slug": f"event-{uuid.uuid4().hex[:6]}",
"price": 100000,
"capacity": 10,
"status": Event.StatusChoices.PUBLISHED,
}
defaults.update(overrides)
return Event.objects.create(**defaults)
@staticmethod
def _discount_code(**overrides):
defaults = {
"code": f"CODE{uuid.uuid4().hex[:4]}",
"value": 50,
"is_active": True,
"type": DiscountCode.Type.PERCENT,
}
defaults.update(overrides)
return DiscountCode.objects.create(**defaults)
class DiscountCodeModelTests(TestCase, PaymentTestMixin):
def setUp(self):
self.event = self._create_event()
self.user = self._create_user(is_email_verified=True)
def test_zero_price_returns_zero_discount(self):
event = self._create_event(price=0)
code = self._discount_code()
code.applicable_events.add(event)
self.assertEqual(code.calculate_discount(event, self.user), (0, 0))
def test_inactive_raises_error(self):
code = self._discount_code(is_active=False)
code.applicable_events.add(self.event)
with self.assertRaises(HttpError):
code.calculate_discount(self.event, self.user)
def test_start_date_validation(self):
code = self._discount_code(starts_at=timezone.now() + timedelta(days=1))
code.applicable_events.add(self.event)
with self.assertRaises(HttpError):
code.calculate_discount(self.event, self.user)
def test_end_date_validation(self):
code = self._discount_code(ends_at=timezone.now() - timedelta(days=1))
code.applicable_events.add(self.event)
with self.assertRaises(HttpError):
code.calculate_discount(self.event, self.user)
def test_applicable_events_enforcement(self):
code = self._discount_code()
other_event = self._create_event()
code.applicable_events.add(other_event)
with self.assertRaises(HttpError):
code.calculate_discount(self.event, self.user)
def test_min_amount_guard(self):
code = self._discount_code(min_amount=200000)
code.applicable_events.add(self.event)
with self.assertRaises(HttpError):
code.calculate_discount(self.event, self.user)
def test_usage_limit_total(self):
code = self._discount_code(usage_limit_total=1)
code.applicable_events.add(self.event)
Payment.objects.create(
user=self.user,
event=self.event,
base_amount=self.event.price,
amount=self.event.price,
discount_amount=0,
status=Payment.OrderStatusChoices.PAID,
discount_code=code,
)
with self.assertRaises(HttpError):
code.calculate_discount(self.event, self.user)
def test_usage_limit_per_user(self):
code = self._discount_code(usage_limit_per_user=1)
code.applicable_events.add(self.event)
Payment.objects.create(
user=self.user,
event=self.event,
base_amount=self.event.price,
amount=self.event.price,
discount_amount=0,
status=Payment.OrderStatusChoices.PENDING,
discount_code=code,
)
with self.assertRaises(HttpError):
code.calculate_discount(self.event, self.user)
def test_final_price_below_min_post_discount(self):
event = self._create_event(price=15000)
code = self._discount_code(value=80)
code.applicable_events.add(event)
with self.assertRaises(HttpError):
code.calculate_discount(event, self.user)
def test_fixed_discount_type(self):
code = self._discount_code(type=DiscountCode.Type.FIXED, value=5000)
code.applicable_events.add(self.event)
final, disc = code.calculate_discount(self.event, self.user)
self.assertEqual(disc, 5000)
self.assertEqual(final, self.event.price - 5000)
class PaymentModelAndResourceTests(TestCase, PaymentTestMixin):
def setUp(self):
self.event = self._create_event()
self.user = self._create_user(is_email_verified=True)
def test_payment_clean_validates_amount(self):
payment = Payment(
user=self.user,
event=self.event,
base_amount=1000,
amount=500,
discount_amount=400,
status=Payment.OrderStatusChoices.INIT,
)
with self.assertRaises(ValidationError):
payment.full_clean()
def test_payment_resource_defers_user_event(self):
payment = Payment.objects.create(
user=self.user,
event=self.event,
base_amount=1000,
amount=1000,
discount_amount=0,
status=Payment.OrderStatusChoices.INIT,
)
resource = PaymentResource()
user_cell = resource.fields["user"].widget.clean(self.user.username, None)
self.assertEqual(user_cell, self.user)
event_cell = resource.fields["event"].widget.clean(self.event.title, None)
self.assertEqual(event_cell, self.event)
def test_discount_resource_expands_events(self):
resource = DiscountResource()
widget = resource.fields["event"].widget
self.assertEqual(widget.separator, "||")
class DiscountCodeAdminTests(TestCase, PaymentTestMixin):
def setUp(self):
self.admin = DiscountCodeAdmin(DiscountCode, AdminSite())
def test_deactivate_codes_action(self):
code = self._discount_code()
queryset = DiscountCode.objects.filter(pk=code.pk)
request = SimpleNamespace(_messages=SimpleNamespace(add=lambda *args, **kwargs: None))
self.admin.deactivate_codes(request, queryset)
code.refresh_from_db()
self.assertFalse(code.is_active)

View File

@@ -0,0 +1,400 @@
import uuid
from datetime import timedelta
from unittest import mock
from django.db.models.signals import post_save
from django.test import SimpleTestCase, TestCase, override_settings
from django.utils import timezone
from import_export.widgets import BooleanWidget
from users.models import User, Major, University
from users.resources import UserResource
from users.signals import send_verification_email_on_registration
from users.tasks import (
send_email_verified_success,
send_password_reset_email,
send_verification_email,
)
class UserFactoryMixin:
def _ensure_reference_objects(self):
if not hasattr(self, "_default_major"):
self._default_major, _ = Major.objects.get_or_create(
code="CS",
defaults={"name": "Computer Science"},
)
self._default_university, _ = University.objects.get_or_create(
code="UT",
defaults={"name": "University of Tehran"},
)
def _resolve_major(self, value):
if value is None:
return None
if isinstance(value, Major):
return value
obj, _ = Major.objects.get_or_create(code=value, defaults={"name": value})
return obj
def _resolve_university(self, value):
if value is None:
return None
if isinstance(value, University):
return value
obj, _ = University.objects.get_or_create(code=value, defaults={"name": value})
return obj
def create_user(self, **extra_fields):
self._ensure_reference_objects()
unique = uuid.uuid4().hex
data = {
"email": f"user_{unique}@example.com",
"username": f"user_{unique[:10]}",
"first_name": "Test",
"last_name": "User",
}
password = extra_fields.pop("password", "StrongPass!123")
major = extra_fields.pop("major", self._default_major)
university = extra_fields.pop("university", self._default_university)
if isinstance(major, str):
major = self._resolve_major(major)
if isinstance(university, str):
university = self._resolve_university(university)
data.update(extra_fields)
data.setdefault("major", major)
data.setdefault("university", university)
return User.objects.create_user(password=password, **data)
class UserModelTests(UserFactoryMixin, TestCase):
def setUp(self):
super().setUp()
patcher = mock.patch("users.signals.send_verification_email.delay")
patcher.start()
self.addCleanup(patcher.stop)
def test_str_returns_full_name_with_email(self):
# Arrange
user = self.create_user(first_name="Ada", last_name="Lovelace")
# Act
result = str(user)
# Assert
expected = f"{user.get_full_name()} ({user.email})"
self.assertEqual(result, expected)
def test_get_full_name_handles_missing_names(self):
# Arrange
user = self.create_user(first_name="Grace", last_name="")
# Act
result = user.get_full_name()
# Assert
self.assertEqual(result, "Grace")
def test_regenerate_verification_token_generates_new_value(self):
# Arrange
user = self.create_user()
original_token = user.email_verification_token
# Act
user.regenerate_verification_token()
# Assert
self.assertNotEqual(user.email_verification_token, original_token)
def test_set_password_reset_token_assigns_future_expiry(self):
# Arrange
user = self.create_user()
frozen = timezone.now()
# Act
with mock.patch("users.models.timezone.now", return_value=frozen):
user.set_password_reset_token()
# Assert
self.assertIsNotNone(user.password_reset_token)
self.assertEqual(
user.password_reset_token_expires_at,
frozen + timedelta(hours=1),
)
def test_save_triggers_verified_task_on_state_change(self):
# Arrange
user = self.create_user()
# Act
with mock.patch("users.tasks.send_email_verified_success.delay") as mock_delay:
user.is_email_verified = True
user.save()
# Assert
mock_delay.assert_called_once_with(user.id)
def test_save_skips_task_when_already_verified(self):
# Arrange
user = self.create_user(is_email_verified=True)
# Act
with mock.patch("users.tasks.send_email_verified_success.delay") as mock_delay:
user.bio = "Updated bio"
user.save()
# Assert
mock_delay.assert_not_called()
class UserSignalTests(TestCase):
def setUp(self):
super().setUp()
post_save.disconnect(send_verification_email_on_registration, sender=User)
self.addCleanup(
post_save.connect,
send_verification_email_on_registration,
User,
False,
)
@override_settings(FRONTEND_ROOT="https://frontend.example/")
@mock.patch("users.signals.send_verification_email.delay")
@mock.patch("users.signals.uuid.uuid4")
def test_signal_sets_username_timestamp_and_dispatches_email(
self,
mock_uuid,
mock_delay,
):
# Arrange
fake_uuid = uuid.UUID("12345678-1234-5678-1234-567812345678")
mock_uuid.return_value = fake_uuid
fake_now = timezone.now()
user = User.objects.create(
email="new.user@example.com",
username="",
password="pass",
is_email_verified=False,
)
# Act
with mock.patch("users.signals.timezone.now", return_value=fake_now):
send_verification_email_on_registration(User, user, created=True)
# Assert
user.refresh_from_db()
self.assertEqual(user.username, str(fake_uuid)[:10])
self.assertEqual(user.email_verification_sent_at, fake_now)
expected_url = (
f"https://frontend.example/verify-email/{user.email_verification_token}"
)
mock_delay.assert_called_once_with(user.id, expected_url)
@override_settings(FRONTEND_ROOT="https://frontend.example/")
@mock.patch("users.signals.send_verification_email.delay")
def test_signal_preserves_existing_username(self, mock_delay):
# Arrange
fake_now = timezone.now()
user = User.objects.create(
email="existing@example.com",
username="existing_name",
password="pass",
is_email_verified=False,
)
# Act
with mock.patch("users.signals.timezone.now", return_value=fake_now):
send_verification_email_on_registration(User, user, created=True)
# Assert
user.refresh_from_db()
self.assertEqual(user.username, "existing_name")
self.assertEqual(user.email_verification_sent_at, fake_now)
mock_delay.assert_called_once()
@mock.patch("users.signals.send_verification_email.delay")
def test_signal_skips_when_user_already_verified(self, mock_delay):
# Arrange
user = User.objects.create(
email="verified@example.com",
username="verified_user",
password="pass",
is_email_verified=True,
)
# Act
send_verification_email_on_registration(User, user, created=True)
# Assert
self.assertIsNone(user.email_verification_sent_at)
mock_delay.assert_not_called()
@mock.patch("users.signals.send_verification_email.delay")
def test_signal_skips_when_email_missing(self, mock_delay):
# Arrange
user = User.objects.create(
email="",
username="no_email",
password="pass",
is_email_verified=False,
)
# Act
send_verification_email_on_registration(User, user, created=True)
# Assert
self.assertIsNone(user.email_verification_sent_at)
mock_delay.assert_not_called()
@mock.patch("users.signals.send_verification_email.delay")
def test_signal_ignores_updates_to_existing_users(self, mock_delay):
# Arrange
user = User.objects.create(
email="existing-update@example.com",
username="existing_update",
password="pass",
is_email_verified=False,
)
# Act
send_verification_email_on_registration(User, user, created=False)
# Assert
self.assertIsNone(user.email_verification_sent_at)
mock_delay.assert_not_called()
class UserTaskTests(UserFactoryMixin, TestCase):
def setUp(self):
super().setUp()
patcher = mock.patch("users.signals.send_verification_email.delay")
patcher.start()
self.addCleanup(patcher.stop)
@override_settings(DEFAULT_FROM_EMAIL="no-reply@example.com")
@mock.patch("users.tasks.send_mail")
@mock.patch("users.tasks.render_to_string", return_value="<p>Hi</p>")
def test_send_verification_email_task_sends_expected_payload(
self,
mock_render,
mock_send_mail,
):
# Arrange
user = self.create_user()
verification_url = "https://example.com/verify"
# Act
result = send_verification_email.run(user.id, verification_url)
# Assert
self.assertEqual(result, f"Verification email sent to {user.email}")
mock_render.assert_called_once_with(
"emails/verification_email.html",
{"user": user, "verification_url": verification_url},
)
kwargs = mock_send_mail.call_args.kwargs
self.assertEqual(kwargs["recipient_list"], [user.email])
self.assertEqual(kwargs["from_email"], "no-reply@example.com")
self.assertEqual(kwargs["message"], "Hi")
@override_settings(DEFAULT_FROM_EMAIL="support@example.com")
@mock.patch("users.tasks.send_mail")
@mock.patch("users.tasks.render_to_string", return_value="<p>Reset</p>")
def test_send_password_reset_email_task_uses_reset_template(
self,
mock_render,
mock_send_mail,
):
# Arrange
user = self.create_user()
reset_url = "https://example.com/reset"
# Act
result = send_password_reset_email.run(user.id, reset_url)
# Assert
self.assertEqual(result, f"Password reset email sent to {user.email}")
mock_render.assert_called_once_with(
"emails/password_reset_email.html",
{"user": user, "reset_url": reset_url},
)
kwargs = mock_send_mail.call_args.kwargs
self.assertEqual(kwargs["recipient_list"], [user.email])
self.assertEqual(kwargs["from_email"], "support@example.com")
self.assertEqual(kwargs["message"], "Reset")
@override_settings(
DEFAULT_FROM_EMAIL="success@example.com",
FRONTEND_ROOT="https://frontend.example/",
)
@mock.patch("users.tasks.send_mail")
@mock.patch("users.tasks.render_to_string", return_value="<p>Success</p>")
def test_send_email_verified_success_task_renders_success_template(
self,
mock_render,
mock_send_mail,
):
# Arrange
user = self.create_user()
# Act
result = send_email_verified_success.run(user.id)
# Assert
self.assertEqual(result, f"verified success email sent to {user.email}")
mock_render.assert_called_once_with(
"emails/verification_success.html",
{"user": user, "home_url": "https://frontend.example/"},
)
kwargs = mock_send_mail.call_args.kwargs
self.assertEqual(kwargs["recipient_list"], [user.email])
self.assertEqual(kwargs["from_email"], "success@example.com")
self.assertEqual(kwargs["message"], "Success")
def test_send_verification_email_task_retries_on_lookup_error(self):
# Arrange
retry_patch = mock.patch.object(
send_verification_email,
"retry",
side_effect=RuntimeError("retry"),
)
# Act / Assert
with mock.patch(
"users.tasks.User.objects.get",
side_effect=ValueError("missing"),
), retry_patch as mock_retry:
with self.assertRaises(RuntimeError):
send_verification_email.run(999, "https://example.com/verify")
self.assertEqual(mock_retry.call_args.kwargs.get("countdown"), 60)
self.assertIsInstance(mock_retry.call_args.kwargs.get("exc"), ValueError)
class UserResourceTests(SimpleTestCase):
def test_boolean_fields_use_boolean_widget(self):
# Arrange
resource = UserResource()
# Act
widgets = [
resource.fields["is_staff"].widget,
resource.fields["is_superuser"].widget,
resource.fields["is_email_verified"].widget,
]
# Assert
for widget in widgets:
self.assertIsInstance(widget, BooleanWidget)
def test_field_order_matches_meta_definition(self):
# Arrange
resource = UserResource()
# Act
field_names = tuple(resource.fields.keys())
# Assert
self.assertEqual(resource._meta.export_order, resource._meta.fields)
self.assertSetEqual(set(field_names), set(resource._meta.fields))