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

1from unittest.mock import MagicMock 

2 

3from django.core.exceptions import ObjectDoesNotExist 

4from django.test import TestCase 

5 

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 

16 

17 

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) 

23 

24 def test_not_registered_payable(self): 

25 with self.assertRaises(NotRegistered): 

26 payables.get_payable(MockModel) 

27 

28 

29class ImmutablePayablesTest(TestCase): 

30 def setUp(self): 

31 payables.register(MockModel, MockPayable) 

32 

33 def tearDown(self): 

34 payables._unregister(MockModel) 

35 

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) 

41 

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) 

49 

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) 

62 

63 with self.subTest("Unlinking a payment"): 

64 model_after.payment = None 

65 self.assertRaises(PaymentError, prevent_saving, MockModel, model_after) 

66 

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) 

71 

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 

76 

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 

82 

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 

94 

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 

99 

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 ) 

104 

105 MockModel.objects.get = MagicMock(return_value=model_before) 

106 prevent_saving(MockModel, model_after) 

107 

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) 

114 

115 self.assertFalse(Payable.immutable_after_payment) 

116 

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) 

125 

126 self.assertEqual([], Payable.immutable_model_fields_after_payment) 

127 

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 ) 

136 

137 related_model_before = MockModel2(payer=None) 

138 related_model_before.test_related_field = model 

139 related_model_before.test_field = "test" 

140 

141 related_model_after = MockModel2(payer=None) 

142 related_model_after.test_related_field = model 

143 related_model_after.test_field = "changed" 

144 

145 MockPayable.immutable_after_payment = True 

146 MockPayable.immutable_foreign_key_models = {MockModel2: ["test_related_field"]} 

147 

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 ) 

156 

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 ) 

163 

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 ) 

169 

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 ) 

176 

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 ) 

188 

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 )