# payments/tests/test_payments.py import graphene from django.contrib.auth import get_user_model from django.contrib.auth.models import AnonymousUser from django.test import TestCase from graphene.test import Client as GrapheneClient from rest_framework import status from rest_framework.test import APIRequestFactory, force_authenticate from payments.graphene.mutations import Deposit # the GraphQL Deposit mutation from payments.models import Balance, Transaction from payments.views import CallbackAPIView, DepositView ############################################################################### # Model Tests ############################################################################### # noinspection PyArgumentList class BalanceModelTests(TestCase): def setUp(self): self.user_model = get_user_model() # Create a user – the post-save signal will auto-create a Balance. self.user = self.user_model.objects.create_user(email="test@example.com", password="pass") self.balance = Balance.objects.get(user=self.user) def test_balance_rounding(self): """ If the balance amount has more than two decimal places, the save() method should round it to 2 decimals. """ self.balance.amount = 10.129 self.balance.save() self.balance.refresh_from_db() # round(10.129, 2) == 10.13 self.assertAlmostEqual(float(self.balance.amount), 10.13, places=2) # noinspection PyArgumentList class TransactionModelTests(TestCase): def setUp(self): self.user_model = get_user_model() self.user = self.user_model.objects.create_user(email="trans_test@example.com", password="pass") self.balance = Balance.objects.get(user=self.user) def test_transaction_rounding(self): """ When a Transaction is saved with an amount having more than two decimals, it should be rounded to 2 decimal places. """ t = Transaction(balance=self.balance, amount=5.6789, currency="EUR", payment_method="card") t.save() self.assertAlmostEqual(t.amount, 5.68, places=2) def test_transaction_zero_amount_raises(self): """ Saving a Transaction with a 0 amount should raise a ValueError. """ with self.assertRaises(ValueError): t = Transaction(balance=self.balance, amount=0.0, currency="EUR", payment_method="card") t.save() ############################################################################### # API (View) Tests ############################################################################### # noinspection PyArgumentList class DepositViewTests(TestCase): def setUp(self): self.factory = APIRequestFactory() self.user_model = get_user_model() self.user = self.user_model.objects.create_user(email="deposit@example.com", password="pass") self.deposit_view = DepositView.as_view() def test_deposit_view_unauthenticated(self): """ An unauthenticated user should receive a 401 Unauthorized response. """ request = self.factory.post("/deposit/", {"amount": 100.0}) # Do not attach a user to the request. response = self.deposit_view(request) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_deposit_view_authenticated(self): """ An authenticated user posting a deposit should get a 201 Created response and a transaction with processing details. """ request = self.factory.post("/deposit/", {"amount": 100.0}) force_authenticate(request, user=self.user) response = self.deposit_view(request) self.assertEqual(response.status_code, status.HTTP_201_CREATED) # The response should include the 'process' field. self.assertIn("process", response.data) class CallbackViewTests(TestCase): def setUp(self): self.factory = APIRequestFactory() self.callback_view = CallbackAPIView.as_view() def test_callback_view_unknown_gateway(self): """ If an unknown gateway is specified, the callback should return a 500 error. """ data = {"sample": "data"} request = self.factory.post("/callback/?gateway=unknown", data, format="json") response = self.callback_view(request) self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) ############################################################################### # Signal Tests ############################################################################### # noinspection PyArgumentList class SignalTests(TestCase): def setUp(self): self.user_model = get_user_model() def test_create_balance_on_user_creation(self): """ When a new User is created, the post_save signal should create an associated Balance. """ user = self.user_model.objects.create_user(email="signal@example.com", password="pass") balance = Balance.objects.get(user=user) self.assertIsNotNone(balance) ############################################################################### # GraphQL Tests ############################################################################### # noinspection PyArgumentList class GraphQLDepositTests(TestCase): def setUp(self): self.user_model = get_user_model() self.user = self.user_model.objects.create_user(email="graphql@example.com", password="pass") # Ensure a Balance exists via the signal. self.balance = Balance.objects.get(user=self.user) # Create a minimal schema including the Deposit mutation. class Mutation(graphene.ObjectType): deposit = Deposit.Field() self.schema = graphene.Schema(mutation=Mutation) self.client = GrapheneClient(self.schema) def test_graphql_deposit_authenticated(self): """ An authenticated GraphQL deposit mutation should create a Transaction. """ mutation = """ mutation Deposit($amount: Float!) { deposit(amount: $amount) { transaction { id amount } } } """ result = self.client.post(mutation, variable_values={"amount": 100.0}, context_value={"user": self.user}) # There should be no errors. self.assertNotIn("errors", result) transaction_data = result.get("data", "") self.assertIsNotNone(transaction_data) self.assertAlmostEqual(float(transaction_data), 100.0, places=2) def test_graphql_deposit_unauthenticated(self): """ An unauthenticated GraphQL deposit mutation should raise a permission error. """ mutation = """ mutation Deposit($amount: Float!) { deposit(amount: $amount) { transaction { id amount } } } """ result = self.client.post(mutation, variable_values={"amount": 100.0}, context_value={"user": AnonymousUser()}) self.assertIn("errors", result) error_message = result["errors"][0] self.assertIn("permission", error_message.lower())