Coverage for website/payments/tests/test_payables.py: 100.00%
123 statements
« prev ^ index » next coverage.py v7.6.7, created at 2025-08-14 10:31 +0000
« prev ^ index » next coverage.py v7.6.7, created at 2025-08-14 10:31 +0000
1from unittest.mock import MagicMock
3from django.core.exceptions import ObjectDoesNotExist
4from django.test import TestCase
6from payments.models import Payment
7from payments.payables import (
8 NotRegistered,
9 Payable,
10 PaymentError,
11 payables,
12 prevent_saving,
13 prevent_saving_related,
14)
15from payments.tests.__mocks__ import MockModel, MockPayable
18class PayablesTest(TestCase):
19 def test_registered_payable(self):
20 payables.register(MockModel, MockPayable)
21 self.assertIsInstance(payables.get_payable(MockModel), MockPayable)
22 payables._unregister(MockModel)
24 def test_not_registered_payable(self):
25 with self.assertRaises(NotRegistered):
26 payables.get_payable(MockModel)
29class ImmutablePayablesTest(TestCase):
30 def setUp(self):
31 payables.register(MockModel, MockPayable)
33 def tearDown(self):
34 payables._unregister(MockModel)
36 def test_register(self):
37 payables._unregister(MockModel)
38 with self.subTest("Register not immutable"):
39 MockPayable.immutable_after_payment = False
40 payables.register(MockModel, MockPayable)
42 payables._unregister(MockModel)
43 with self.subTest("Register immutable"):
44 MockPayable.immutable_after_payment = True
45 MockPayable.immutable_model_fields_after_payment = [
46 "test_field",
47 ]
48 payables.register(MockModel, MockPayable)
50 def test_prevent_unlinking_payment_from_payable(self):
51 model_before = MockModel(payer=None)
52 model_after = MockModel(payer=None)
53 model_before.payment = Payment.objects.create(
54 type=Payment.CARD, amount=2, notes="test payment", topic="test topic"
55 )
56 MockPayable.immutable_after_payment = True
57 MockPayable.immutable_model_fields_after_payment = [
58 "test_field",
59 ]
60 MockPayable.get_payment = MagicMock(return_value=model_before.payment)
61 MockModel.objects.get = MagicMock(return_value=model_before)
63 with self.subTest("Unlinking a payment"):
64 model_after.payment = None
65 self.assertRaises(PaymentError, prevent_saving, MockModel, model_after)
67 with self.subTest("Changing an immutable model field after payment"):
68 model_after.payment = model_before.payment
69 model_after.test_field = "changed"
70 self.assertRaises(PaymentError, prevent_saving, MockModel, model_after)
72 def test_prevent_saving_no_existing_model(self):
73 model = MockModel(payer=None)
74 model.pk = None
75 prevent_saving(MockModel, model) # this should not crash
77 def test_prevent_saving_not_immutable(self):
78 MockPayable.immutable_after_payment = False
79 model = MockModel(payer=None)
80 model.pk = 1
81 prevent_saving(MockModel, model) # this should not crash
83 def test_prevent_saving_model_with_pk_but_not_in_db(self):
84 MockPayable.immutable_after_payment = True
85 model = MockModel(payer=None)
86 model.payment = Payment.objects.create(
87 type=Payment.CARD, amount=2, notes="test payment", topic="test topic"
88 )
89 MockPayable.get_payment = MagicMock(return_value=model.payment)
90 MockModel.DoesNotExist = ObjectDoesNotExist
91 MockModel.objects.get = MagicMock(side_effect=MockModel.DoesNotExist)
92 model.pk = 1
93 prevent_saving(MockModel, model) # this should not crash
95 def test_allow_adding_a_payment_to_unpaid_model(self):
96 MockPayable.immutable_after_payment = True
97 model_before = MockModel(payer=None)
98 model_before.payment = None
100 model_after = MockModel(payer=None)
101 model_after.payment = Payment.objects.create(
102 type=Payment.CARD, amount=2, notes="test payment", topic="test topic"
103 )
105 MockModel.objects.get = MagicMock(return_value=model_before)
106 prevent_saving(MockModel, model_after)
108 def test_mutable_model(self):
109 payable = payables.get_payable(MockModel)
110 MockPayable.immutable_after_payment = False
111 self.assertFalse(payable.immutable_after_payment)
112 MockPayable.immutable_after_payment = True
113 self.assertTrue(payable.immutable_after_payment)
115 self.assertFalse(Payable.immutable_after_payment)
117 def test_immutable_fields(self):
118 payable = payables.get_payable(MockModel)
119 MockPayable.immutable_model_fields_after_payment = ["test1", "test2"]
120 self.assertEqual(
121 ["test1", "test2"], payable.immutable_model_fields_after_payment
122 )
123 MockPayable.immutable_model_fields_after_payment = []
124 self.assertEqual([], payable.immutable_model_fields_after_payment)
126 self.assertEqual([], Payable.immutable_model_fields_after_payment)
128 def test_prevent_saving_changed_related_model_field(self):
129 MockModel2 = MockModel # noqa: N806
130 MockPayable.immutable_after_payment = True
131 MockPayable.immutable_model_fields_after_payment = {MockModel2: ["test_field"]}
132 model = MockModel(payer=None)
133 model.payment = Payment.objects.create(
134 type=Payment.CARD, amount=2, notes="test payment", topic="test topic"
135 )
137 related_model_before = MockModel2(payer=None)
138 related_model_before.test_related_field = model
139 related_model_before.test_field = "test"
141 related_model_after = MockModel2(payer=None)
142 related_model_after.test_related_field = model
143 related_model_after.test_field = "changed"
145 MockPayable.immutable_after_payment = True
146 MockPayable.immutable_foreign_key_models = {MockModel2: ["test_related_field"]}
148 with self.subTest("Prevent saving changed field in related model"):
149 MockModel2.objects.get = MagicMock(return_value=related_model_before)
150 self.assertRaises(
151 PaymentError,
152 prevent_saving_related("test_related_field"),
153 MockModel2,
154 related_model_after,
155 )
157 with self.subTest("Do nothing if field is not changed related model"):
158 MockModel2.objects.get = MagicMock(return_value=related_model_before)
159 related_model_after.test_field = "test"
160 prevent_saving_related("test_related_field")(
161 MockModel2, related_model_after
162 )
164 with self.subTest("Do nothing if parent is not immutable"):
165 MockPayable.immutable_after_payment = False
166 prevent_saving_related("test_related_field")(
167 MockModel2, related_model_after
168 )
170 with self.subTest("Do nothing if parent is not paid"):
171 MockPayable.immutable_after_payment = True
172 model.payment = None
173 prevent_saving_related("test_related_field")(
174 MockModel2, related_model_after
175 )
177 with self.subTest(
178 "Do nothing if related model changed fields are not labelled immutable"
179 ):
180 model.payment = Payment.objects.create(
181 type=Payment.CARD, amount=2, notes="test payment", topic="test topic"
182 )
183 MockPayable.immutable_after_payment = True
184 MockPayable.immutable_model_fields_after_payment = {MockModel2: []}
185 prevent_saving_related("test_related_field")(
186 MockModel2, related_model_after
187 )
189 with self.subTest("Error if model does not exist"):
190 MockPayable.immutable_after_payment = True
191 MockPayable.immutable_foreign_key_models = {
192 MockModel2: ["test_related_field"]
193 }
194 MockModel2.DoesNotExist = ObjectDoesNotExist
195 MockModel2.objects.get = MagicMock(side_effect=MockModel2.DoesNotExist)
196 self.assertRaises(
197 PaymentError,
198 prevent_saving_related("test_related_field"),
199 MockModel2,
200 related_model_after,
201 )