Coverage for website/payments/tests/test_views.py: 100.00%
268 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 import mock
2from unittest.mock import ANY, MagicMock, Mock, PropertyMock, patch
4from django.apps import apps
5from django.contrib.auth import get_user_model
6from django.test import Client, TestCase, override_settings
7from django.urls import reverse
8from django.utils import timezone
10from freezegun import freeze_time
12from members.models import Member
13from payments.exceptions import PaymentError
14from payments.models import BankAccount, Payment, PaymentUser
15from payments.payables import payables
16from payments.tests.__mocks__ import MockModel
17from payments.tests.test_services import MockPayable
20@freeze_time("2019-01-01")
21@override_settings(SUSPEND_SIGNALS=True)
22class BankAccountCreateViewTest(TestCase):
23 """Test for the BankAccountCreateView."""
25 fixtures = ["members.json"]
27 @classmethod
28 def setUpTestData(cls):
29 cls.login_user = PaymentUser.objects.filter(last_name="Wiggers").first()
30 cls.new_user = get_user_model().objects.create_user(
31 username="username", first_name="Johnny", last_name="Test"
32 )
33 cls.account = BankAccount.objects.create(
34 owner=cls.login_user,
35 initials="J",
36 last_name="Test",
37 iban="NL91ABNA0417164300",
38 )
39 BankAccount.objects.create(
40 owner=None, initials="Someone", last_name="Else", iban="BE68539007547034"
41 )
43 def setUp(self):
44 self.client = Client()
45 self.client.force_login(self.login_user)
47 def test_not_logged_in(self):
48 """
49 If there is no logged-in user they should redirect
50 to the authentication page
51 """
52 self.client.logout()
54 response = self.client.get(reverse("payments:bankaccount-add"), follow=True)
55 self.assertEqual(200, response.status_code)
56 self.assertEqual(
57 [("/user/account/login/?next=" + reverse("payments:bankaccount-add"), 302)],
58 response.redirect_chain,
59 )
61 def test_shows_correct_reference(self):
62 """
63 The page should show the reference that will be used to identify
64 a new mandate if direct debit is enabled
65 """
66 response = self.client.get(reverse("payments:bankaccount-add"), follow=True)
67 self.assertEqual(200, response.status_code)
68 self.assertEqual("1-1", response.context["mandate_no"])
69 self.assertContains(response, "1-1")
71 def test_account_no_mandate_saves_correctly(self):
72 """
73 If an account with direct debit enabled is saved
74 we should redirect to the account list page
75 with a success alert showing. And the BankAccount should be the only
76 one in the account since the previous one had no mandate.
77 BankAccounts by others should be untouched.
78 """
79 response = self.client.post(
80 reverse("payments:bankaccount-add"),
81 data={
82 "initials": "S",
83 "last_name": "Versteeg",
84 "iban": "DE12500105170648489890",
85 "bic": "NBBEBEBB",
86 },
87 follow=True,
88 )
89 self.assertEqual(200, response.status_code)
90 self.assertEqual(
91 [(reverse("payments:bankaccount-list"), 302)], response.redirect_chain
92 )
93 self.assertContains(response, "Bank account saved successfully.")
95 self.assertEqual(1, BankAccount.objects.filter(owner=self.login_user).count())
97 self.assertTrue(
98 BankAccount.objects.filter(
99 owner=self.login_user, iban="DE12500105170648489890"
100 ).exists()
101 )
103 self.assertTrue(BankAccount.objects.filter(iban="BE68539007547034").exists())
105 def test_account_with_mandate_saves_correctly(self):
106 """
107 If an account with direct debit enabled is saved
108 we should redirect to the account list page
109 with a success alert showing. And the BankAccount should be the only
110 one in the account since the previous one had no mandate.
111 BankAccounts by others should be untouched.
112 """
113 response = self.client.post(
114 reverse("payments:bankaccount-add"),
115 data={
116 "initials": "S",
117 "last_name": "Versteeg",
118 "iban": "DE12500105170648489890",
119 "bic": "NBBEBEBB",
120 "direct_debit": "",
121 "signature": "sig",
122 },
123 follow=True,
124 )
125 self.assertEqual(200, response.status_code)
126 self.assertEqual(
127 [(reverse("payments:bankaccount-list"), 302)], response.redirect_chain
128 )
129 self.assertContains(response, "Bank account saved successfully.")
131 self.assertEqual(1, BankAccount.objects.filter(owner=self.login_user).count())
133 self.assertTrue(
134 BankAccount.objects.filter(
135 owner=self.login_user, iban="DE12500105170648489890", mandate_no="1-1"
136 ).exists()
137 )
139 self.assertTrue(BankAccount.objects.filter(iban="BE68539007547034").exists())
141 def test_account_save_keeps_old_mandates(self):
142 """
143 If an account is saved and there are previous accounts that
144 were authorised for direct debit then we should keep those
145 but they should be revoked.
146 """
147 BankAccount.objects.create(
148 owner=self.login_user,
149 initials="J",
150 last_name="Test",
151 iban="NL91ABNA0417164300",
152 valid_from="2019-03-01",
153 signature="sig",
154 mandate_no="11-2",
155 )
157 self.client.post(
158 reverse("payments:bankaccount-add"),
159 data={
160 "initials": "S",
161 "last_name": "Versteeg",
162 "iban": "DE12500105170648489890",
163 "bic": "NBBEBEBB",
164 },
165 follow=True,
166 )
168 self.assertEqual(2, BankAccount.objects.filter(owner=self.login_user).count())
170 self.assertTrue(
171 BankAccount.objects.filter(
172 owner=self.login_user, iban="DE12500105170648489890"
173 ).exists()
174 )
176 self.assertFalse(
177 BankAccount.objects.filter(owner=self.login_user, iban="NL91ABNA0417164300")
178 .first()
179 .valid
180 )
183@override_settings(SUSPEND_SIGNALS=True)
184class BankAccountRevokeViewTest(TestCase):
185 """Test for the BankAccountRevokeView."""
187 fixtures = ["members.json"]
189 @classmethod
190 def setUpTestData(cls):
191 cls.login_user = PaymentUser.objects.filter(last_name="Wiggers").first()
192 cls.account1 = BankAccount.objects.create(
193 owner=cls.login_user,
194 initials="J1",
195 last_name="Test",
196 iban="NL91ABNA0417164300",
197 )
198 cls.account2 = BankAccount.objects.create(
199 owner=cls.login_user,
200 initials="J2",
201 last_name="Test",
202 iban="BE68539007547034",
203 bic="NBBEBEBB",
204 valid_from="2019-03-01",
205 signature="sig",
206 mandate_no="11-2",
207 )
209 def setUp(self):
210 self.account1.refresh_from_db()
211 self.account2.refresh_from_db()
212 self.client = Client()
213 self.client.force_login(self.login_user)
215 def test_not_logged_in(self):
216 """
217 If there is no logged-in user they should redirect
218 to the authentication page
219 """
220 self.client.logout()
222 response = self.client.post(
223 reverse("payments:bankaccount-revoke", args=(self.account1.pk,)),
224 follow=True,
225 )
226 self.assertEqual(200, response.status_code)
227 self.assertEqual(
228 [
229 (
230 "/user/account/login/?next="
231 + reverse("payments:bankaccount-revoke", args=(self.account1.pk,)),
232 302,
233 )
234 ],
235 response.redirect_chain,
236 )
238 def test_no_post(self):
239 """If the request is not a post it should redirect to the list."""
240 response = self.client.get(
241 reverse("payments:bankaccount-revoke", args=(self.account2.pk,)),
242 follow=True,
243 )
244 self.assertEqual(200, response.status_code)
245 self.assertEqual(
246 [(reverse("payments:bankaccount-list"), 302)], response.redirect_chain
247 )
249 def test_cannot_revoke_no_mandate(self):
250 """If the selected account has no valid mandate it should return a 404."""
251 self.account2.valid_until = "2019-04-01"
252 self.account2.save()
254 response = self.client.post(
255 reverse("payments:bankaccount-revoke", args=(self.account1.pk,)),
256 follow=True,
257 )
258 self.assertEqual(404, response.status_code)
260 response = self.client.post(
261 reverse("payments:bankaccount-revoke", args=(self.account2.pk,)),
262 follow=True,
263 )
264 self.assertEqual(404, response.status_code)
266 def test_cannot_revoke_cannot_revoke(self):
267 """If a bank account cannot be revoked, an error should be displayed."""
268 with patch(
269 "payments.models.BankAccount.can_be_revoked", new_callable=mock.PropertyMock
270 ) as can_be_revoked:
271 can_be_revoked.return_value = False
273 response = self.client.post(
274 reverse("payments:bankaccount-revoke", args=(self.account2.pk,)),
275 follow=True,
276 )
277 self.assertEqual(200, response.status_code)
278 self.assertEqual(
279 [(reverse("payments:bankaccount-list"), 302)], response.redirect_chain
280 )
281 self.assertContains(response, "cannot be revoked")
283 def test_revoke_successful(self):
284 """
285 If an account with direct debit is revoked it should
286 redirect to the list with the right success message.
287 """
288 self.assertTrue(
289 BankAccount.objects.filter(
290 owner=PaymentUser.objects.get(pk=self.login_user.pk),
291 iban="BE68539007547034",
292 )
293 .first()
294 .valid
295 )
297 response = self.client.post(
298 reverse("payments:bankaccount-revoke", args=(self.account2.pk,)),
299 follow=True,
300 )
301 self.assertEqual(200, response.status_code)
302 self.assertEqual(
303 [(reverse("payments:bankaccount-list"), 302)], response.redirect_chain
304 )
305 self.assertContains(
306 response, "Direct debit authorisation successfully revoked."
307 )
309 self.assertFalse(
310 BankAccount.objects.filter(
311 owner=self.login_user,
312 iban="BE68539007547034",
313 )
314 .first()
315 .valid
316 )
319@override_settings(SUSPEND_SIGNALS=True)
320class BankAccountListViewTest(TestCase):
321 """Test for the BankAccountListView."""
323 fixtures = ["members.json"]
325 @classmethod
326 def setUpTestData(cls):
327 cls.login_user = PaymentUser.objects.filter(last_name="Wiggers").first()
328 cls.account1 = BankAccount.objects.create(
329 owner=cls.login_user,
330 initials="J1",
331 last_name="Test",
332 iban="NL91ABNA0417164300",
333 )
334 cls.account2 = BankAccount.objects.create(
335 owner=PaymentUser.objects.exclude(last_name="Wiggers").first(),
336 initials="J2",
337 last_name="Test",
338 iban="BE68539007547034",
339 bic="NBBEBEBB",
340 valid_from="2019-03-01",
341 signature="sig",
342 mandate_no="11-2",
343 )
345 def setUp(self):
346 self.account1.refresh_from_db()
347 self.account2.refresh_from_db()
348 self.client = Client()
349 self.client.force_login(self.login_user)
351 def test_not_logged_in(self):
352 """
353 If there is no logged-in user they should redirect
354 to the authentication page
355 """
356 self.client.logout()
358 response = self.client.post(
359 reverse("payments:bankaccount-list"),
360 follow=True,
361 )
362 self.assertEqual(200, response.status_code)
363 self.assertEqual(
364 [
365 (
366 "/user/account/login/?next=" + reverse("payments:bankaccount-list"),
367 302,
368 )
369 ],
370 response.redirect_chain,
371 )
373 def test_accounts(self):
374 """The page should show only accounts of the logged-in user."""
375 response = self.client.get(
376 reverse("payments:bankaccount-list"),
377 follow=True,
378 )
379 self.assertEqual(200, response.status_code)
380 self.assertContains(response, "NL91ABNA0417164300")
381 self.assertNotContains(response, "BE68539007547034")
384@freeze_time("2019-04-01")
385@override_settings(SUSPEND_SIGNALS=True)
386class PaymentListViewTest(TestCase):
387 """Test for the PaymentListView."""
389 fixtures = ["members.json"]
391 @classmethod
392 def setUpTestData(cls):
393 cls.login_user = PaymentUser.objects.filter(last_name="Wiggers").first()
394 cls.account1 = BankAccount.objects.create(
395 owner=cls.login_user,
396 initials="J1",
397 last_name="Test",
398 iban="NL91ABNA0417164300",
399 valid_from="2019-03-01",
400 signature="sig",
401 mandate_no="11-2",
402 )
403 cls.payment1 = Payment.objects.create(
404 created_at=timezone.datetime(year=2019, month=3, day=1),
405 paid_by=cls.login_user,
406 processed_by=cls.login_user,
407 notes="Testing Payment 1",
408 amount=10,
409 type=Payment.CARD,
410 )
412 def setUp(self):
413 self.account1.refresh_from_db()
414 self.payment1.refresh_from_db()
415 self.client = Client()
416 self.client.force_login(self.login_user)
418 def test_not_logged_in(self):
419 """
420 If there is no logged-in user they should redirect
421 to the authentication page
422 """
423 self.client.logout()
425 response = self.client.post(
426 reverse("payments:payment-list"),
427 follow=True,
428 )
429 self.assertEqual(200, response.status_code)
430 self.assertEqual(
431 [
432 (
433 "/user/account/login/?next=" + reverse("payments:payment-list"),
434 302,
435 )
436 ],
437 response.redirect_chain,
438 )
440 def test_contents(self):
441 """Test if the view shows payments."""
442 response = self.client.get(
443 reverse("payments:payment-list", kwargs={"year": 2019, "month": 3}),
444 follow=True,
445 )
446 self.assertEqual(200, response.status_code)
447 self.assertContains(response, "Testing Payment 1")
450@freeze_time("2020-09-01")
451@override_settings(SUSPEND_SIGNALS=True, THALIA_PAY_ENABLED_PAYMENT_METHOD=True)
452@patch("payments.models.PaymentUser.tpay_allowed", PropertyMock, True)
453class PaymentProcessViewTest(TestCase):
454 """Test for the PaymentProcessView."""
456 fixtures = ["members.json"]
458 test_body = {
459 "app_label": "mock_app",
460 "model_name": "mock_model",
461 "payable": "mock_payable_pk",
462 "payable_hash": "placeholder",
463 "next": "/mock_next",
464 }
466 @classmethod
467 def setUpTestData(cls):
468 cls.user = Member.objects.filter(last_name="Wiggers").first()
469 cls.account1 = BankAccount.objects.create(
470 owner=cls.user,
471 initials="J1",
472 last_name="Test",
473 iban="NL91ABNA0417164300",
474 valid_from="2019-03-01",
475 signature="sig",
476 mandate_no="11-2",
477 )
478 cls.user = PaymentUser.objects.get(pk=cls.user.pk)
480 def setUp(self):
481 payables.register(MockModel, MockPayable)
483 self.account1.refresh_from_db()
484 self.client = Client()
485 self.client.force_login(self.user)
487 self.model = MockModel(payer=self.user)
489 self.original_get_model = apps.get_model
490 mock_get_model = MagicMock()
492 def side_effect(*args, **kwargs):
493 if "app_label" in kwargs and kwargs["app_label"] == "mock_app":
494 return mock_get_model
495 return self.original_get_model(*args, **kwargs)
497 apps.get_model = Mock(side_effect=side_effect)
498 mock_get_model.objects.get.return_value = self.model
500 self.test_body["payable_hash"] = str(hash(payables.get_payable(self.model)))
502 def tearDown(self):
503 apps.get_model = self.original_get_model
504 payables._unregister(MockModel)
506 def test_not_logged_in(self):
507 """
508 If there is no logged-in user they should redirect
509 to the authentication page
510 """
511 self.client.logout()
513 response = self.client.post(
514 reverse("payments:payment-process"),
515 follow=True,
516 )
517 self.assertEqual(200, response.status_code)
518 self.assertEqual(
519 [
520 (
521 "/user/account/login/?next=" + reverse("payments:payment-process"),
522 302,
523 )
524 ],
525 response.redirect_chain,
526 )
528 @override_settings(THALIA_PAY_ENABLED_PAYMENT_METHOD=False)
529 def test_member_has_tpay_enabled(self):
530 response = self.client.post(
531 reverse("payments:payment-process"), follow=True, data=self.test_body
532 )
533 self.assertEqual(403, response.status_code)
535 @mock.patch("django.contrib.messages.error")
536 def test_tpay_not_allowed(self, messages_error):
537 with mock.patch("payments.payables.Payable.tpay_allowed") as mock_tpay_allowed:
538 mock_tpay_allowed.__get__ = mock.Mock(return_value=False)
540 response = self.client.post(
541 reverse("payments:payment-process"), follow=False, data=self.test_body
542 )
544 messages_error.assert_called_with(
545 ANY, "You are not allowed to use Thalia Pay for this payment."
546 )
548 self.assertEqual(302, response.status_code)
549 self.assertEqual("/mock_next", response.url)
551 def test_missing_parameters(self):
552 response = self.client.post(
553 reverse("payments:payment-process"), follow=True, data={}
554 )
555 self.assertEqual(400, response.status_code)
557 def test_disallowed_redirect(self):
558 response = self.client.post(
559 reverse("payments:payment-process"),
560 follow=True,
561 data={**self.test_body, "next": "https://ru.nl/"},
562 )
563 self.assertEqual(400, response.status_code)
565 @mock.patch("django.contrib.messages.error")
566 def test_different_member(self, messages_error):
567 self.model.payer = PaymentUser()
569 response = self.client.post(
570 reverse("payments:payment-process"), follow=False, data=self.test_body
571 )
573 messages_error.assert_called_with(
574 ANY, "You are not allowed to process this payment."
575 )
577 self.assertEqual(302, response.status_code)
578 self.assertEqual("/mock_next", response.url)
580 @mock.patch("django.contrib.messages.error")
581 def test_already_paid(self, messages_error):
582 self.model.payment = Payment(amount=8)
584 response = self.client.post(
585 reverse("payments:payment-process"), follow=False, data=self.test_body
586 )
588 messages_error.assert_called_with(ANY, "This object has already been paid for.")
590 self.assertEqual(302, response.status_code)
591 self.assertEqual("/mock_next", response.url)
593 @mock.patch("django.contrib.messages.error")
594 def test_zero_payment(self, messages_error):
595 self.model.amount = 0
597 response = self.client.post(
598 reverse("payments:payment-process"), follow=False, data=self.test_body
599 )
601 messages_error.assert_called_with(
602 ANY, "No payment required for amount of €0.00"
603 )
605 self.assertEqual(302, response.status_code)
606 self.assertEqual("/mock_next", response.url)
608 def test_renders_confirmation(self):
609 response = self.client.post(
610 reverse("payments:payment-process"), follow=False, data=self.test_body
611 )
613 self.assertEqual(200, response.status_code)
614 self.assertEqual(
615 payables.get_payable(self.model).pk, response.context_data["payable"].pk
616 )
617 self.assertContains(response, "Please confirm your payment.")
618 self.assertContains(response, 'name="_save"')
620 @mock.patch("django.contrib.messages.success")
621 @mock.patch("payments.services.create_payment")
622 def test_creates_payment(self, create_payment, messages_success):
623 response = self.client.post(
624 reverse("payments:payment-process"),
625 follow=False,
626 data={**self.test_body, "_save": True},
627 )
629 create_payment.assert_called_with(ANY, self.user, Payment.TPAY)
630 self.assertEqual(create_payment.call_args.args[0].pk, self.model.pk)
632 messages_success.assert_called_with(
633 ANY, "Your payment has been processed successfully."
634 )
636 self.assertEqual(302, response.status_code)
637 self.assertEqual("/mock_next", response.url)
639 @mock.patch("django.contrib.messages.error")
640 @mock.patch("payments.services.create_payment")
641 def test_payment_create_error(self, create_payment, messages_error):
642 create_payment.side_effect = PaymentError("Test error")
644 response = self.client.post(
645 reverse("payments:payment-process"),
646 follow=False,
647 data={**self.test_body, "_save": True},
648 )
650 messages_error.assert_called_with(ANY, "Test error")
652 self.assertEqual(302, response.status_code)
653 self.assertEqual("/mock_next", response.url)
655 def test_payment_deleted_error(self):
656 test_body = {
657 "app_label": "sales",
658 "model_name": "orders",
659 "payable": "63be888b-2852-4811-8b72-82cd86ea0b9f",
660 "payable_hash": "non_existent",
661 "next": "/mock_next",
662 }
663 response = self.client.get(
664 reverse("payments:payment-process"), follow=False, data=test_body
665 )
666 self.assertEqual(404, response.status_code)
668 def test_payment_accept_deleted_error(self):
669 test_body = {
670 "app_label": "sales",
671 "model_name": "order",
672 "payable": "63be888b-2852-4811-8b72-82cd86ea0b9f",
673 "next": "/mock_next",
674 "payable_hash": "non_existent",
675 "_save": True,
676 }
677 response = self.client.post(
678 reverse("payments:payment-process"), follow=False, data=test_body
679 )
680 self.assertEqual(404, response.status_code)
682 def test_app_does_not_exist(self):
683 test_body = {
684 "app_label": "payments",
685 "model_name": "does_not_exist",
686 "payable": "63be888b-2852-4811-8b72-82cd86ea0b9f",
687 "next": "/mock_next",
688 "payable_hash": "non_existent",
689 }
690 response = self.client.post(
691 reverse("payments:payment-process"), follow=False, data=test_body
692 )
693 self.assertEqual(404, response.status_code)
695 def test_model_does_not_exist(self):
696 test_body = {
697 "app_label": "does_not_exist",
698 "model_name": "does_not_exist",
699 "payable": "63be888b-2852-4811-8b72-82cd86ea0b9f",
700 "next": "/mock_next",
701 "payable_hash": "non_existent",
702 }
703 response = self.client.post(
704 reverse("payments:payment-process"), follow=False, data=test_body
705 )
706 self.assertEqual(404, response.status_code)
708 @mock.patch("django.contrib.messages.error")
709 def test_payment_changed_payable(self, messages_error):
710 body = self.test_body
711 body["payable_hash"] = "987654321"
712 response = self.client.post(
713 reverse("payments:payment-process"),
714 follow=False,
715 data={**body, "_save": True},
716 )
717 messages_error.assert_called_with(
718 ANY, "This object has been changed in the mean time. You have not paid."
719 )
721 self.assertEqual(302, response.status_code)
722 self.assertEqual("/mock_next", response.url)