7de047af6ebdba14d7011d6b689954d659f73974
[wolnelektury.git] / src / paypal / tests.py
1 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
3 #
4 from django.contrib.auth.models import User
5 from unittest.mock import MagicMock, Mock, patch, DEFAULT
6 from catalogue.test_utils import WLTestCase
7 from .models import BillingAgreement, BillingPlan
8 from .rest import user_is_subscribed
9 from paypalrestsdk import ResourceNotFound
10
11
12 BillingPlanMock = Mock(
13     return_value=Mock(
14         id='some-billing-plan-id'
15     )
16 )
17
18 BillingAgreementMock = Mock(
19     # BillingAgreement() has a .links[]
20     return_value=MagicMock(
21         links=[
22             Mock(
23                 rel="approval_url",
24                 href="http://paypal.test/approval/"
25             )
26         ]
27     ),
28     # BillingAgreement.execute(token)
29     execute=Mock(
30         return_value=Mock(
31             error=None,
32             id='some-billing-agreement-id',
33             plan=Mock(
34                 payment_definitions=[
35                     Mock(
36                         amount={'value': '100'}
37                     )
38                 ]
39             )
40         )
41     ),
42     # Later we can BillingAgreement.find(...).state == 'Active'
43     find=Mock(
44         return_value=Mock(
45             state='Active'
46         )
47     ),
48 )
49
50
51 class PaypalTests(WLTestCase):
52     @classmethod
53     def setUpClass(cls):
54         cls.user = User(username='test')
55         cls.user.set_password('test')
56         cls.user.save()
57
58     @classmethod
59     def tearDownClass(cls):
60         cls.user.delete()
61
62     def test_paypal_form(self):
63         response = self.client.get('/paypal/form/')
64         self.assertEqual(response.status_code, 200)
65
66     def test_paypal_form_unauthorized(self):
67         """Legacy flow: only allow payment for logged-in users."""
68         response = self.client.post('/paypal/form/', {"amount": "0"})
69         self.assertEqual(response.status_code, 403)
70
71     def test_paypal_form_invalid(self):
72         """Paypal form: error on bad input."""
73         self.client.login(username='test', password='test')
74
75         response = self.client.post('/paypal/form/', {"amount": "0"})
76         self.assertEqual(response.status_code, 200)
77         self.assertEqual(
78             len(response.context['form'].errors['amount']),
79             1)
80
81     @patch.multiple('paypalrestsdk',
82         BillingPlan=BillingPlanMock,
83         BillingAgreement=BillingAgreementMock,
84     )
85     def test_paypal_form_valid(self):
86         """PayPal form created a BillingPlan."""
87         self.client.login(username='test', password='test')
88         response = self.client.post('/paypal/form/', {"amount": "100"})
89         self.assertRedirects(response, 'http://paypal.test/approval/',
90             fetch_redirect_response=False)
91         self.assertEqual(BillingPlan.objects.all().count(), 1)
92
93         # Posting the form a second time does not create another plan.
94         response = self.client.post('/paypal/form/', {"amount": "100"})
95         self.assertRedirects(response, 'http://paypal.test/approval/',
96             fetch_redirect_response=False)
97         self.assertEqual(BillingPlan.objects.all().count(), 1)
98
99         # No BillingAgreement created in our DB yet.
100         self.assertEqual(BillingAgreement.objects.all().count(), 0)
101
102     @patch('paypalrestsdk.BillingPlan', BillingPlanMock)
103     def test_paypal_form_error(self):
104         """On PayPal error, plan does not get created."""
105         self.client.login(username='test', password='test')
106
107         # It can choke on BillingPlan().create().
108         with patch('paypalrestsdk.BillingPlan', Mock(
109                 return_value=Mock(create=Mock(return_value=None)))):
110             response = self.client.post('/paypal/form/', {"amount": "100"})
111             self.assertEqual(response.status_code, 200)
112
113         # Or it can choke on BillingPlan().activate().
114         with patch('paypalrestsdk.BillingPlan', Mock(
115                 return_value=Mock(activate=Mock(return_value=None)))):
116             response = self.client.post('/paypal/form/', {"amount": "100"})
117             self.assertEqual(response.status_code, 200)
118
119         # No plan is created yet.
120         self.assertEqual(BillingPlan.objects.all().count(), 0)
121
122         # Or it can choke later, on BillingAgreement().create()
123         with patch('paypalrestsdk.BillingAgreement', Mock(
124                 return_value=Mock(create=Mock(return_value=None)))):
125             response = self.client.post('/paypal/form/', {"amount": "100"})
126             self.assertEqual(response.status_code, 200)
127
128         # But now the plan should be created.
129         self.assertEqual(BillingPlan.objects.all().count(), 1)
130
131     @patch.multiple('paypalrestsdk',
132         BillingPlan=BillingPlanMock,
133         BillingAgreement=BillingAgreementMock,
134     )
135     def test_paypal_app_form_valid(self):
136         """App form creates a BillingPlan."""
137         self.client.login(username='test', password='test')
138         response = self.client.post('/paypal/app-form/', {"amount": "100"})
139         self.assertRedirects(response, 'http://paypal.test/approval/',
140             fetch_redirect_response=False)
141         self.assertEqual(BillingPlan.objects.all().count(), 1)
142
143     @patch('paypalrestsdk.BillingAgreement', BillingAgreementMock)
144     def test_paypal_return(self):
145         self.client.login(username='test', password='test')
146         BillingPlan.objects.create(amount=100)
147
148         # No token = no agreement.
149         response = self.client.get('/paypal/return/')
150         self.assertEqual(response.status_code, 404)
151         self.assertEqual(BillingAgreement.objects.all().count(), 0)
152
153         response = self.client.get('/paypal/return/?token=secret-token')
154         self.assertEqual(response.status_code, 200)
155         self.assertEqual(BillingAgreement.objects.all().count(), 1)
156
157         # Repeated returns will not generate further agreements.
158         response = self.client.get('/paypal/return/?token=secret-token')
159         self.assertEqual(response.status_code, 200)
160         self.assertEqual(BillingAgreement.objects.all().count(), 1)
161
162         self.assertTrue(user_is_subscribed(self.user))
163
164     @patch('paypalrestsdk.BillingAgreement', BillingAgreementMock)
165     def test_paypal_app_return(self):
166         self.client.login(username='test', password='test')
167         BillingPlan.objects.create(amount=100)
168         response = self.client.get('/paypal/app-return/?token=secret-token')
169         self.assertRedirects(
170             response, 'wolnelekturyapp://paypal_return',
171             fetch_redirect_response=False)
172
173         # Repeated returns will not generate further agreements.
174         response = self.client.get('/paypal/app-return/?token=secret-token')
175         self.assertRedirects(
176             response, 'wolnelekturyapp://paypal_return',
177             fetch_redirect_response=False)
178         self.assertEqual(BillingAgreement.objects.all().count(), 1)
179
180         self.assertTrue(user_is_subscribed(self.user))
181
182     def test_paypal_return_error(self):
183         self.client.login(username='test', password='test')
184         BillingPlan.objects.create(amount=100)
185
186         # It can choke on BillingAgreement.execute()
187         with patch('paypalrestsdk.BillingAgreement', Mock(
188                 execute=Mock(return_value=Mock(id=None)))):
189             self.client.get('/paypal/app-return/?token=secret-token')
190             response = self.client.get('/paypal/app-return/?token=secret-token')
191             self.assertRedirects(
192                 response, 'wolnelekturyapp://paypal_error',
193                 fetch_redirect_response=False)
194
195         # No agreement created in our DB if not executed successfully.
196         self.assertEqual(BillingAgreement.objects.all().count(), 0)
197
198         # It can execute all right, but just not be findable later.
199         with patch('paypalrestsdk.BillingAgreement', Mock(
200                 execute=BillingAgreementMock.execute,
201                 find=Mock(side_effect=ResourceNotFound(None)))):
202             response = self.client.get('/paypal/app-return/?token=secret-token')
203             self.assertRedirects(
204                 response, 'wolnelekturyapp://paypal_return',
205                 fetch_redirect_response=False)
206
207         # Now the agreement exists in our DB, but is not active.
208         self.assertEqual([b.active for b in BillingAgreement.objects.all()], [False])
209
210         with patch('paypalrestsdk.BillingAgreement', Mock(
211                 find=Mock(return_value=Mock(state='Mocked')))):
212             self.assertFalse(user_is_subscribed(self.user))
213
214     def test_paypal_cancel(self):
215         response = self.client.get('/paypal/cancel/')
216         self.assertEqual(response.status_code, 200)