source: OpenWorkouts-current/ow/tests/views/test_user.py @ 1d92bf2

currentfeature/docs
Last change on this file since 1d92bf2 was 1d92bf2, checked in by borja <borja@…>, 5 years ago

(#37) Allow login using email address instead of username:

  • Use user uids as keys in the root folder for referencing user objects (instead of username)
  • Use uids for referencing users all over the place (auth, permissions, traversal urls, etc)
  • Replaced the username concept with nickname. This nickname will be used as a shortcut to access "public profile" pages for users
  • Reworked lots of basic methods in the OpenWorkouts root object (s/username/nickname, marked as properties some methods like users, emails, etc)
  • Added new add_user() and delete_user() helpers to the OpenWorkouts root object
  • Fixed bug in the dashboard redirect view, causing an endless loop if an authenticated user does not exist anymore when loading a page.
  • Lots of tests fixes, adaptations and catch up.
  • Property mode set to 100644
File size: 17.0 KB
Line 
1import os
2from datetime import datetime, timedelta, timezone
3from shutil import copyfileobj
4from unittest.mock import Mock, patch
5
6import pytest
7
8from ZODB.blob import Blob
9
10from pyramid.testing import DummyRequest
11from pyramid.httpexceptions import HTTPFound
12from pyramid.response import Response
13
14from webob.multidict import MultiDict
15
16from ow.models.root import OpenWorkouts
17from ow.models.user import User
18from ow.models.workout import Workout
19from ow.views.renderers import OWFormRenderer
20import ow.views.user as user_views
21
22
23class TestUserViews(object):
24
25    @pytest.fixture
26    def john(self):
27        user = User(firstname='John', lastname='Doe',
28                    email='john.doe@example.net')
29        user.password = 's3cr3t'
30        return user
31
32    @pytest.fixture
33    def root(self, john):
34        root = OpenWorkouts()
35        root.add_user(john)
36        workout = Workout(
37            start=datetime(2015, 6, 28, 12, 55, tzinfo=timezone.utc),
38            duration=timedelta(minutes=60),
39            distance=30
40        )
41        john.add_workout(workout)
42        return root
43
44    @pytest.fixture
45    def dummy_request(self, root):
46        request = DummyRequest()
47        request.root = root
48        return request
49
50    @pytest.fixture
51    def profile_post_request(self, root, john):
52        """
53        This is a valid POST request to update an user profile.
54        Form will validate, but nothing will be really updated/changed.
55        """
56        user = john
57        request = DummyRequest()
58        request.root = root
59        request.method = 'POST'
60        request.POST = MultiDict({
61            'submit': True,
62            'firstname': user.firstname,
63            'lastname': user.lastname,
64            'email': user.email,
65            'bio': user.bio,
66            'weight': user.weight,
67            'height': user.height,
68            'gender': user.gender,
69            'birth_date': user.birth_date,
70            'picture': user.picture,
71            })
72        return request
73
74    @pytest.fixture
75    def passwd_post_request(self, root):
76        """
77        This is a valid POST request to change the user password, but
78        the form will not validate (empty fields)
79        """
80        request = DummyRequest()
81        request.root = root
82        request.method = 'POST'
83        request.POST = MultiDict({
84            'submit': True,
85            'old_password': '',
86            'password': '',
87            'password_confirm': ''
88            })
89        return request
90
91    @pytest.fixture
92    def signup_post_request(self, root):
93        """
94        This is a valid POST request to signup a new user.
95        """
96        request = DummyRequest()
97        request.root = root
98        request.method = 'POST'
99        request.POST = MultiDict({
100            'submit': True,
101            'nickname': 'JackBlack',
102            'email': 'jack.black@example.net',
103            'firstname': 'Jack',
104            'lastname': 'Black',
105            'password': 'j4ck s3cr3t',
106            'password_confirm': 'j4ck s3cr3t'
107            })
108        return request
109
110    def test_dashboard_redirect_unauthenticated(self, root):
111        """
112        Anoymous access to the root object, send the user to the login page.
113
114        Instead of reusing the DummyRequest from the request fixture, we do
115        Mock fully the request here, because we need to use
116        authenticated_userid, which cannot be easily set in the DummyRequest
117        """
118        request = DummyRequest()
119        request.root = root
120        response = user_views.dashboard_redirect(root, request)
121        assert isinstance(response, HTTPFound)
122        assert response.location == request.resource_url(root, 'login')
123
124    def test_dashboard_redirect_authenticated(self, root):
125        """
126        Authenticated user accesing the root object, send the user to her
127        dashboard
128
129        Instead of reusing the DummyRequest from the request fixture, we do
130        Mock fully the request here, because we need to use
131        authenticated_userid, which cannot be easily set in the DummyRequest
132        """
133        request = Mock()
134        request.root = root
135        request.authenticated_userid = 'john'
136        request.resource_url.return_value = '/dashboard'
137        response = user_views.dashboard_redirect(root, request)
138        assert isinstance(response, HTTPFound)
139        assert response.location == '/dashboard'
140
141    def test_dashboard(self, dummy_request, john):
142        """
143        Renders the user dashboard
144        """
145        request = dummy_request
146        response = user_views.dashboard(john, request)
147        assert response == {}
148
149    def test_profile(self, dummy_request, john):
150        """
151        Renders the user profile page
152        """
153        request = dummy_request
154        response = user_views.profile(john, request)
155        assert response == {}
156
157    def test_login_get(self, dummy_request):
158        """
159        GET request to access the login page
160        """
161        request = dummy_request
162        response = user_views.login(request.root, request)
163        assert response['message'] == ''
164        assert response['email'] == ''
165        assert response['password'] == ''
166        assert response['redirect_url'] == request.resource_url(request.root)
167
168    def test_login_get_return_to(self, dummy_request, john):
169        """
170        GET request to access the login page, if there is a page set to where
171        the user should be sent to, the response "redirect_url" key will have
172        such url
173        """
174        request = dummy_request
175        workout = john.workouts()[0]
176        workout_url = request.resource_url(workout)
177        request.params['return_to'] = workout_url
178        response = user_views.login(request.root, request)
179        assert response['redirect_url'] == workout_url
180
181    def test_login_post_wrong_email(self, dummy_request):
182        request = dummy_request
183        request.method = 'POST'
184        request.POST['submit'] = True
185        request.POST['email'] = 'jack@example.net'
186        response = user_views.login(request.root, request)
187        assert response['message'] == u'Wrong email address'
188
189    def test_login_post_wrong_password(self, dummy_request):
190        request = dummy_request
191        request.method = 'POST'
192        request.POST['submit'] = True
193        request.POST['email'] = 'john.doe@example.net'
194        request.POST['password'] = 'badpassword'
195        response = user_views.login(request.root, request)
196        assert response['message'] == u'Wrong password'
197
198    @patch('ow.views.user.remember')
199    def test_login_post_ok(self, rem, dummy_request):
200        request = dummy_request
201        request.method = 'POST'
202        request.POST['submit'] = True
203        request.POST['email'] = 'john.doe@example.net'
204        request.POST['password'] = 's3cr3t'
205        response = user_views.login(request.root, request)
206        assert isinstance(response, HTTPFound)
207        assert rem.called
208        assert response.location == request.resource_url(request.root)
209
210    @patch('ow.views.user.forget')
211    def test_logout(self, forg, dummy_request):
212        request = dummy_request
213        response = user_views.logout(request.root, request)
214        assert isinstance(response, HTTPFound)
215        assert forg.called
216        assert response.location == request.resource_url(request.root)
217
218    extensions = ('png', 'jpg', 'jpeg', 'gif')
219
220    @pytest.mark.parametrize('extension', extensions)
221    def test_profile_picture(self, extension, dummy_request, john):
222        """
223        GET request to get the profile picture of an user.
224        """
225        request = dummy_request
226        # Get the user
227        user = john
228        # Get the path to the image, then open it and copy it to a new Blob
229        # object
230        path = 'fixtures/image.' + extension
231        image_path = os.path.join(
232            os.path.dirname(os.path.dirname(__file__)), path)
233        blob = Blob()
234        with open(image_path, 'rb') as infile, blob.open('w') as out:
235            infile.seek(0)
236            copyfileobj(infile, out)
237
238        # Set the blob with the picture
239        user.picture = blob
240
241        # Call the profile_picture view
242        response = user_views.profile_picture(user, request)
243        assert isinstance(response, Response)
244        assert response.status_int == 200
245        assert response.content_type == 'image'
246
247    def test_edit_profile_get(self, dummy_request, john):
248        """
249        GET request to the edit profile page, returns the form ready to
250        be rendered
251        """
252        request = dummy_request
253        user = john
254        response = user_views.edit_profile(user, request)
255        assert isinstance(response['form'], OWFormRenderer)
256        # no errors in the form (first load)
257        assert response['form'].errorlist() == ''
258        # the form carries along the proper data keys, taken from the
259        # loaded user profile
260        data = ['firstname', 'lastname', 'email', 'bio', 'birth_date',
261                'height', 'weight', 'gender']
262        assert list(response['form'].data.keys()) == data
263        # and check the email to see data is properly loaded
264        assert response['form'].data['email'] == 'john.doe@example.net'
265
266    def test_edit_profile_post_ok(self, profile_post_request, john):
267        request = profile_post_request
268        user = john
269        # Update the bio field
270        bio = 'Some text about this user'
271        request.POST['bio'] = bio
272        response = user_views.edit_profile(user, request)
273        assert isinstance(response, HTTPFound)
274        assert response.location == request.resource_url(user, 'profile')
275        assert user.bio == bio
276
277    def test_edit_profile_post_missing_required(
278            self, profile_post_request, john):
279        request = profile_post_request
280        request.POST['email'] = ''
281        user = john
282        response = user_views.edit_profile(user, request)
283        assert isinstance(response['form'], OWFormRenderer)
284        # error on the missing email field
285        error = u'Please enter an email address'
286        html_error = u'<ul class="error"><li>' + error + '</li></ul>'
287        assert response['form'].errorlist() == html_error
288        assert response['form'].errors_for('email') == [error]
289
290    def test_change_password_get(self, dummy_request, john):
291        request = dummy_request
292        user = john
293        response = user_views.change_password(user, request)
294        assert isinstance(response['form'], OWFormRenderer)
295        # no errors in the form (first load)
296        assert response['form'].errorlist() == ''
297
298    def test_change_password_post_ok(self, passwd_post_request, john):
299        request = passwd_post_request
300        user = john
301        request.POST['old_password'] = 's3cr3t'
302        request.POST['password'] = 'h1dd3n s3cr3t'
303        request.POST['password_confirm'] = 'h1dd3n s3cr3t'
304        response = user_views.change_password(user, request)
305        assert isinstance(response, HTTPFound)
306        assert response.location == request.resource_url(user, 'profile')
307        # password was changed
308        assert not user.check_password('s3cr3t')
309        assert user.check_password('h1dd3n s3cr3t')
310
311    def test_change_password_post_no_values(self, passwd_post_request, john):
312        request = passwd_post_request
313        user = john
314        response = user_views.change_password(user, request)
315        assert isinstance(response['form'], OWFormRenderer)
316        error = u'Please enter a value'
317        html_error = u'<ul class="error">'
318        html_error += ('<li>' + error + '</li>') * 3  # 3 fields
319        html_error += '</ul>'
320        errorlist = response['form'].errorlist().replace('\n', '')
321        assert errorlist == html_error
322        assert response['form'].errors_for('old_password') == [error]
323        assert response['form'].errors_for('password') == [error]
324        assert response['form'].errors_for('password_confirm') == [error]
325        # password was not changed
326        assert user.check_password('s3cr3t')
327
328    def test_change_password_post_bad_old_password(
329            self, passwd_post_request, john):
330        request = passwd_post_request
331        user = john
332        request.POST['old_password'] = 'FAIL PASSWORD'
333        request.POST['password'] = 'h1dd3n s3cr3t'
334        request.POST['password_confirm'] = 'h1dd3n s3cr3t'
335        response = user_views.change_password(user, request)
336        assert isinstance(response['form'], OWFormRenderer)
337        error = u'The given password does not match the existing one '
338        html_error = u'<ul class="error"><li>' + error + '</li></ul>'
339        assert response['form'].errorlist() == html_error
340        assert response['form'].errors_for('old_password') == [error]
341        # password was not changed
342        assert user.check_password('s3cr3t')
343        assert not user.check_password('h1dd3n s3cr3t')
344
345    def test_change_password_post_password_mismatch(
346            self, passwd_post_request, john):
347        request = passwd_post_request
348        user = john
349        request.POST['old_password'] = 's3cr3t'
350        request.POST['password'] = 'h1dd3n s3cr3ts'
351        request.POST['password_confirm'] = 'h1dd3n s3cr3t'
352        response = user_views.change_password(user, request)
353        assert isinstance(response['form'], OWFormRenderer)
354        error = u'Fields do not match'
355        html_error = u'<ul class="error"><li>' + error + '</li></ul>'
356        assert response['form'].errorlist() == html_error
357        assert response['form'].errors_for('password_confirm') == [error]
358        # password was not changed
359        assert user.check_password('s3cr3t')
360        assert not user.check_password('h1dd3n s3cr3t')
361
362    def test_signup_get(self, dummy_request):
363        request = dummy_request
364        response = user_views.signup(request.root, request)
365        assert isinstance(response['form'], OWFormRenderer)
366        # no errors in the form (first load)
367        assert response['form'].errorlist() == ''
368
369    def test_signup_post_ok(self, signup_post_request):
370        request = signup_post_request
371        assert 'jack.black@example.net' not in request.root.emails
372        assert 'JackBlack' not in request.root.all_nicknames
373        response = user_views.signup(request.root, request)
374        assert isinstance(response, HTTPFound)
375        assert response.location == request.resource_url(request.root)
376        assert 'jack.black@example.net' in request.root.emails
377        assert 'JackBlack' in request.root.all_nicknames
378
379    def test_signup_missing_required(self, signup_post_request):
380        request = signup_post_request
381        request.POST['email'] = ''
382        assert 'jack.black@example.net' not in request.root.emails
383        assert 'JackBlack' not in request.root.all_nicknames
384        response = user_views.signup(request.root, request)
385        assert isinstance(response['form'], OWFormRenderer)
386        error = u'Please enter an email address'
387        html_error = '<ul class="error">'
388        html_error += '<li>' + error + '</li>'
389        html_error += '</ul>'
390        errorlist = response['form'].errorlist().replace('\n', '')
391        assert errorlist == html_error
392        assert response['form'].errors_for('email') == [error]
393        assert 'jack.black@example.net' not in request.root.emails
394        assert 'JackBlack' not in request.root.all_nicknames
395
396    def test_signup_existing_nickname(self, signup_post_request, john):
397        request = signup_post_request
398        # assign john a nickname first
399        john.nickname = 'john'
400        # now set it for the POST request
401        request.POST['nickname'] = 'john'
402        # check jack is not there yet
403        assert 'jack.black@example.net' not in request.root.emails
404        assert 'JackBlack' not in request.root.all_nicknames
405        # now signup as jack, but trying to set the nickname 'john'
406        response = user_views.signup(request.root, request)
407        assert isinstance(response['form'], OWFormRenderer)
408        error = u'Another user is already using the nickname john'
409        html_error = '<ul class="error">'
410        html_error += '<li>' + error + '</li>'
411        html_error += '</ul>'
412        errorlist = response['form'].errorlist().replace('\n', '')
413        assert errorlist == html_error
414        assert response['form'].errors_for('nickname') == [error]
415        # all the errors, and jack is not there
416        assert 'jack.black@example.net' not in request.root.emails
417        assert 'JackBlack' not in request.root.all_nicknames
418
419    def test_signup_existing_email(self, signup_post_request):
420        request = signup_post_request
421        request.POST['email'] = 'john.doe@example.net'
422        assert 'jack.black@example.net' not in request.root.emails
423        assert 'JackBlack' not in request.root.all_nicknames
424        response = user_views.signup(request.root, request)
425        assert isinstance(response['form'], OWFormRenderer)
426        error = u'Another user is already registered with the email '
427        error += u'john.doe@example.net'
428        html_error = '<ul class="error">'
429        html_error += '<li>' + error + '</li>'
430        html_error += '</ul>'
431        errorlist = response['form'].errorlist().replace('\n', '')
432        assert errorlist == html_error
433        assert response['form'].errors_for('email') == [error]
434        assert 'jack.black@example.net' not in request.root.emails
435        assert 'JackBlack' not in request.root.all_nicknames
Note: See TracBrowser for help on using the repository browser.