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