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

1from unittest import mock 

2from unittest.mock import ANY, MagicMock, Mock, PropertyMock, patch 

3 

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 

9 

10from freezegun import freeze_time 

11 

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 

18 

19 

20@freeze_time("2019-01-01") 

21@override_settings(SUSPEND_SIGNALS=True) 

22class BankAccountCreateViewTest(TestCase): 

23 """Test for the BankAccountCreateView.""" 

24 

25 fixtures = ["members.json"] 

26 

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 ) 

42 

43 def setUp(self): 

44 self.client = Client() 

45 self.client.force_login(self.login_user) 

46 

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() 

53 

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 ) 

60 

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") 

70 

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.") 

94 

95 self.assertEqual(1, BankAccount.objects.filter(owner=self.login_user).count()) 

96 

97 self.assertTrue( 

98 BankAccount.objects.filter( 

99 owner=self.login_user, iban="DE12500105170648489890" 

100 ).exists() 

101 ) 

102 

103 self.assertTrue(BankAccount.objects.filter(iban="BE68539007547034").exists()) 

104 

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.") 

130 

131 self.assertEqual(1, BankAccount.objects.filter(owner=self.login_user).count()) 

132 

133 self.assertTrue( 

134 BankAccount.objects.filter( 

135 owner=self.login_user, iban="DE12500105170648489890", mandate_no="1-1" 

136 ).exists() 

137 ) 

138 

139 self.assertTrue(BankAccount.objects.filter(iban="BE68539007547034").exists()) 

140 

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 ) 

156 

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 ) 

167 

168 self.assertEqual(2, BankAccount.objects.filter(owner=self.login_user).count()) 

169 

170 self.assertTrue( 

171 BankAccount.objects.filter( 

172 owner=self.login_user, iban="DE12500105170648489890" 

173 ).exists() 

174 ) 

175 

176 self.assertFalse( 

177 BankAccount.objects.filter(owner=self.login_user, iban="NL91ABNA0417164300") 

178 .first() 

179 .valid 

180 ) 

181 

182 

183@override_settings(SUSPEND_SIGNALS=True) 

184class BankAccountRevokeViewTest(TestCase): 

185 """Test for the BankAccountRevokeView.""" 

186 

187 fixtures = ["members.json"] 

188 

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 ) 

208 

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) 

214 

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() 

221 

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 ) 

237 

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 ) 

248 

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() 

253 

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) 

259 

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) 

265 

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 

272 

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") 

282 

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 ) 

296 

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 ) 

308 

309 self.assertFalse( 

310 BankAccount.objects.filter( 

311 owner=self.login_user, 

312 iban="BE68539007547034", 

313 ) 

314 .first() 

315 .valid 

316 ) 

317 

318 

319@override_settings(SUSPEND_SIGNALS=True) 

320class BankAccountListViewTest(TestCase): 

321 """Test for the BankAccountListView.""" 

322 

323 fixtures = ["members.json"] 

324 

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 ) 

344 

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) 

350 

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() 

357 

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 ) 

372 

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") 

382 

383 

384@freeze_time("2019-04-01") 

385@override_settings(SUSPEND_SIGNALS=True) 

386class PaymentListViewTest(TestCase): 

387 """Test for the PaymentListView.""" 

388 

389 fixtures = ["members.json"] 

390 

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 ) 

411 

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) 

417 

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() 

424 

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 ) 

439 

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") 

448 

449 

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.""" 

455 

456 fixtures = ["members.json"] 

457 

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 } 

465 

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) 

479 

480 def setUp(self): 

481 payables.register(MockModel, MockPayable) 

482 

483 self.account1.refresh_from_db() 

484 self.client = Client() 

485 self.client.force_login(self.user) 

486 

487 self.model = MockModel(payer=self.user) 

488 

489 self.original_get_model = apps.get_model 

490 mock_get_model = MagicMock() 

491 

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) 

496 

497 apps.get_model = Mock(side_effect=side_effect) 

498 mock_get_model.objects.get.return_value = self.model 

499 

500 self.test_body["payable_hash"] = str(hash(payables.get_payable(self.model))) 

501 

502 def tearDown(self): 

503 apps.get_model = self.original_get_model 

504 payables._unregister(MockModel) 

505 

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() 

512 

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 ) 

527 

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) 

534 

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) 

539 

540 response = self.client.post( 

541 reverse("payments:payment-process"), follow=False, data=self.test_body 

542 ) 

543 

544 messages_error.assert_called_with( 

545 ANY, "You are not allowed to use Thalia Pay for this payment." 

546 ) 

547 

548 self.assertEqual(302, response.status_code) 

549 self.assertEqual("/mock_next", response.url) 

550 

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) 

556 

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) 

564 

565 @mock.patch("django.contrib.messages.error") 

566 def test_different_member(self, messages_error): 

567 self.model.payer = PaymentUser() 

568 

569 response = self.client.post( 

570 reverse("payments:payment-process"), follow=False, data=self.test_body 

571 ) 

572 

573 messages_error.assert_called_with( 

574 ANY, "You are not allowed to process this payment." 

575 ) 

576 

577 self.assertEqual(302, response.status_code) 

578 self.assertEqual("/mock_next", response.url) 

579 

580 @mock.patch("django.contrib.messages.error") 

581 def test_already_paid(self, messages_error): 

582 self.model.payment = Payment(amount=8) 

583 

584 response = self.client.post( 

585 reverse("payments:payment-process"), follow=False, data=self.test_body 

586 ) 

587 

588 messages_error.assert_called_with(ANY, "This object has already been paid for.") 

589 

590 self.assertEqual(302, response.status_code) 

591 self.assertEqual("/mock_next", response.url) 

592 

593 @mock.patch("django.contrib.messages.error") 

594 def test_zero_payment(self, messages_error): 

595 self.model.amount = 0 

596 

597 response = self.client.post( 

598 reverse("payments:payment-process"), follow=False, data=self.test_body 

599 ) 

600 

601 messages_error.assert_called_with( 

602 ANY, "No payment required for amount of €0.00" 

603 ) 

604 

605 self.assertEqual(302, response.status_code) 

606 self.assertEqual("/mock_next", response.url) 

607 

608 def test_renders_confirmation(self): 

609 response = self.client.post( 

610 reverse("payments:payment-process"), follow=False, data=self.test_body 

611 ) 

612 

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"') 

619 

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 ) 

628 

629 create_payment.assert_called_with(ANY, self.user, Payment.TPAY) 

630 self.assertEqual(create_payment.call_args.args[0].pk, self.model.pk) 

631 

632 messages_success.assert_called_with( 

633 ANY, "Your payment has been processed successfully." 

634 ) 

635 

636 self.assertEqual(302, response.status_code) 

637 self.assertEqual("/mock_next", response.url) 

638 

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") 

643 

644 response = self.client.post( 

645 reverse("payments:payment-process"), 

646 follow=False, 

647 data={**self.test_body, "_save": True}, 

648 ) 

649 

650 messages_error.assert_called_with(ANY, "Test error") 

651 

652 self.assertEqual(302, response.status_code) 

653 self.assertEqual("/mock_next", response.url) 

654 

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) 

667 

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) 

681 

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) 

694 

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) 

707 

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 ) 

720 

721 self.assertEqual(302, response.status_code) 

722 self.assertEqual("/mock_next", response.url)