169 lines
5.2 KiB
Python
169 lines
5.2 KiB
Python
import uuid
|
|
from datetime import timedelta
|
|
from unittest.mock import Mock, patch
|
|
|
|
import pytest
|
|
from django.urls import reverse
|
|
from django.utils import timezone
|
|
from rest_framework.test import APIClient
|
|
|
|
from apps.accounts.models import User, UserRole
|
|
from apps.bookings.models import Booking, BookingStatus
|
|
from apps.payments.models import Payment, PaymentProvider, PaymentStatus
|
|
from apps.salons.models import Salon, Service, StaffProfile
|
|
|
|
|
|
@pytest.fixture
|
|
def booking_entities():
|
|
owner = User.objects.create_user(email="owner@example.com", password="pass", role=UserRole.MANAGER)
|
|
customer = User.objects.create_user(email="customer@example.com", password="pass")
|
|
staff_user = User.objects.create_user(email="staff@example.com", password="pass", role=UserRole.STAFF)
|
|
|
|
salon = Salon.objects.create(
|
|
owner=owner,
|
|
name="Main Salon",
|
|
description="",
|
|
address="123 King Rd",
|
|
city="Riyadh",
|
|
phone_number="0512345678",
|
|
)
|
|
service = Service.objects.create(
|
|
salon=salon,
|
|
name="Haircut",
|
|
description="",
|
|
duration_minutes=60,
|
|
price_amount=120,
|
|
currency="SAR",
|
|
)
|
|
staff = StaffProfile.objects.create(user=staff_user, salon=salon)
|
|
|
|
start_time = timezone.now() + timedelta(days=1)
|
|
end_time = start_time + timedelta(minutes=60)
|
|
booking = Booking.objects.create(
|
|
salon=salon,
|
|
customer=customer,
|
|
service=service,
|
|
staff=staff,
|
|
start_time=start_time,
|
|
end_time=end_time,
|
|
status=BookingStatus.PENDING,
|
|
price_amount=service.price_amount,
|
|
currency=service.currency,
|
|
notes="",
|
|
)
|
|
return customer, booking
|
|
|
|
|
|
def _mock_gateway_response(payment_id="pay_test", status="initiated"):
|
|
response = Mock()
|
|
response.status_code = 201
|
|
response.json.return_value = {
|
|
"id": payment_id,
|
|
"status": status,
|
|
"source": {"transaction_url": "https://moyasar.example/tx"},
|
|
}
|
|
response.content = b"{}"
|
|
return response
|
|
|
|
|
|
@pytest.mark.django_db
|
|
@patch("apps.payments.services.gateway.requests.post")
|
|
def test_create_payment_idempotency_returns_existing(mock_post, booking_entities, monkeypatch):
|
|
customer, booking = booking_entities
|
|
client = APIClient()
|
|
client.force_authenticate(user=customer)
|
|
|
|
monkeypatch.setenv("MOYASAR_SECRET_KEY", "sk_test")
|
|
monkeypatch.setenv("MOYASAR_PUBLISHABLE_KEY", "pk_test")
|
|
|
|
mock_post.return_value = _mock_gateway_response()
|
|
request_id = str(uuid.uuid4())
|
|
|
|
payload = {
|
|
"booking_id": booking.id,
|
|
"provider": PaymentProvider.MOYASAR,
|
|
"idempotency_key": request_id,
|
|
"source": {"type": "stcpay", "mobile": "0500000000"},
|
|
}
|
|
|
|
response = client.post(reverse("payment-list"), payload, content_type="application/json")
|
|
assert response.status_code == 201
|
|
|
|
response_repeat = client.post(reverse("payment-list"), payload, content_type="application/json")
|
|
assert response_repeat.status_code == 200
|
|
assert Payment.objects.count() == 1
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_rejects_creditcard_source(booking_entities):
|
|
customer, booking = booking_entities
|
|
client = APIClient()
|
|
client.force_authenticate(user=customer)
|
|
|
|
payload = {
|
|
"booking_id": booking.id,
|
|
"provider": PaymentProvider.MOYASAR,
|
|
"idempotency_key": str(uuid.uuid4()),
|
|
"source": {"type": "creditcard", "number": "4111111111111111"},
|
|
}
|
|
|
|
response = client.post(reverse("payment-list"), payload, content_type="application/json")
|
|
assert response.status_code == 400
|
|
assert "source" in response.json()
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_webhook_paid_updates_status(booking_entities, monkeypatch):
|
|
_, booking = booking_entities
|
|
monkeypatch.setenv("MOYASAR_WEBHOOK_SECRET", "secret")
|
|
|
|
payment = Payment.objects.create(
|
|
booking=booking,
|
|
provider=PaymentProvider.MOYASAR,
|
|
status=PaymentStatus.INITIATED,
|
|
amount=booking.price_amount,
|
|
currency=booking.currency,
|
|
external_id="pay_webhook",
|
|
metadata={},
|
|
)
|
|
|
|
payload = {
|
|
"type": "payment_paid",
|
|
"secret_token": "secret",
|
|
"data": {"id": "pay_webhook"},
|
|
}
|
|
|
|
client = APIClient()
|
|
response = client.post(reverse("payment-webhook"), payload, content_type="application/json")
|
|
assert response.status_code == 200
|
|
|
|
payment.refresh_from_db()
|
|
assert payment.status == PaymentStatus.PAID
|
|
assert payment.paid_at is not None
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_webhook_invalid_secret_is_rejected(booking_entities, monkeypatch):
|
|
_, booking = booking_entities
|
|
monkeypatch.setenv("MOYASAR_WEBHOOK_SECRET", "secret")
|
|
|
|
Payment.objects.create(
|
|
booking=booking,
|
|
provider=PaymentProvider.MOYASAR,
|
|
status=PaymentStatus.INITIATED,
|
|
amount=booking.price_amount,
|
|
currency=booking.currency,
|
|
external_id="pay_webhook",
|
|
metadata={},
|
|
)
|
|
|
|
payload = {
|
|
"type": "payment_paid",
|
|
"secret_token": "wrong",
|
|
"data": {"id": "pay_webhook"},
|
|
}
|
|
|
|
client = APIClient()
|
|
response = client.post(reverse("payment-webhook"), payload, content_type="application/json")
|
|
assert response.status_code == 401
|