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

current
Last change on this file since aa6dcaf was aa6dcaf, checked in by Borja Lopez <borja@…>, 5 years ago

Fixed some cosmetic details (empty lines, whitespaces, indentation).
Fixed a test that was breaking depending on which days you ran it.

  • Property mode set to 100644
File size: 47.8 KB
Line 
1import os
2import json
3from decimal import Decimal
4from datetime import datetime, timedelta, timezone
5from shutil import copyfileobj
6from unittest.mock import Mock, patch
7from io import BytesIO
8from uuid import UUID
9
10import pytest
11
12from ZODB.blob import Blob
13
14from pyramid.testing import DummyRequest
15from pyramid.httpexceptions import HTTPFound, HTTPNotFound
16from pyramid.response import Response
17
18from webob.multidict import MultiDict
19
20from PIL import Image
21
22from pytz import common_timezones
23
24from ow.models.root import OpenWorkouts
25from ow.models.user import User
26from ow.models.workout import Workout
27from ow.views.renderers import OWFormRenderer
28from ow.utilities import get_available_locale_names
29import ow.views.user as user_views
30
31
32class TestUserViews(object):
33
34    @pytest.fixture
35    def john(self):
36        user = User(firstname='John', lastname='Doe',
37                    email='john.doe@example.net')
38        user.password = 's3cr3t'
39        return user
40
41    @pytest.fixture
42    def root(self, john):
43        root = OpenWorkouts()
44        root.add_user(john)
45        workout = Workout(
46            start=datetime(2015, 6, 28, 12, 55, tzinfo=timezone.utc),
47            duration=timedelta(minutes=60),
48            distance=30,
49            sport='cycling'
50        )
51        john.add_workout(workout)
52        return root
53
54    @pytest.fixture
55    def dummy_request(self, root):
56        request = DummyRequest()
57        request.registry.settings = {
58            'pyramid.default_locale_name': 'en'
59        }
60        request.root = root
61        return request
62
63    @pytest.fixture
64    def profile_post_request(self, root, john):
65        """
66        This is a valid POST request to update an user profile.
67        Form will validate, but nothing will be really updated/changed.
68        """
69        user = john
70        request = DummyRequest()
71        request.registry.settings = {
72            'pyramid.default_locale_name': 'en'
73        }
74        request.root = root
75        request.method = 'POST'
76        request.POST = MultiDict({
77            'submit': True,
78            'firstname': user.firstname,
79            'lastname': user.lastname,
80            'email': user.email,
81            'bio': user.bio,
82            'weight': user.weight,
83            'height': user.height,
84            'gender': user.gender,
85            'birth_date': user.birth_date,
86            'picture': user.picture,
87            })
88        return request
89
90    @pytest.fixture
91    def passwd_post_request(self, root):
92        """
93        This is a valid POST request to change the user password, but
94        the form will not validate (empty fields)
95        """
96        request = DummyRequest()
97        request.root = root
98        request.method = 'POST'
99        request.POST = MultiDict({
100            'submit': True,
101            'old_password': '',
102            'password': '',
103            'password_confirm': ''
104            })
105        return request
106
107    @pytest.fixture
108    def signup_post_request(self, root):
109        """
110        This is a valid POST request to signup a new user.
111        """
112        request = DummyRequest()
113        request.root = root
114        request.method = 'POST'
115        request.POST = MultiDict({
116            'submit': True,
117            'nickname': 'JackBlack',
118            'email': 'jack.black@example.net',
119            'firstname': 'Jack',
120            'lastname': 'Black',
121            'password': 'j4ck s3cr3t',
122            'password_confirm': 'j4ck s3cr3t'
123            })
124        return request
125
126    @patch('ow.views.user.remember')
127    def test_verify_already_verified(self, remember, dummy_request, john):
128        john.verified = True
129        response = user_views.verify(john, dummy_request)
130        assert isinstance(response, HTTPFound)
131        assert response.location == dummy_request.resource_url(john)
132        # user was not authenticated
133        assert not remember.called
134        # verified status did not change
135        assert john.verified
136
137    @patch('ow.views.user.remember')
138    def test_verify_no_subpath(self, remember, dummy_request, john):
139        response = user_views.verify(john, dummy_request)
140        # the verify info page is rendered, we don't pass anything to the
141        # rendering context
142        assert response == {}
143        # user was not authenticated
144        assert not remember.called
145        # verified status did not change
146        assert not john.verified
147
148    def test_verify_subpath_not_verified(self, dummy_request, john):
149        dummy_request.subpath = ['not_the_token']
150        response = user_views.verify(john, dummy_request)
151        # the verify info page is rendered, we don't pass anything to the
152        # rendering context
153        assert response == {}
154
155    @patch('ow.views.user.remember')
156    def test_verify_wrong_token(self, remember, dummy_request, john):
157        token = 'some-uuid4'
158        john.verification_token = 'some-other-uuid4'
159        dummy_request.subpath = [token]
160        response = user_views.verify(john, dummy_request)
161        # the verify info page is rendered, we don't pass anything to the
162        # rendering context
163        assert response == {}
164        # user was not authenticated
165        assert not remember.called
166        # verified status did not change, neither did the token
167        assert not john.verified
168        assert john.verification_token == 'some-other-uuid4'
169
170    @patch('ow.views.user.remember')
171    def test_verify_verifying(self, remember, dummy_request, john):
172        token = 'some-uuid4'
173        john.verification_token = token
174        dummy_request.subpath = [token]
175        response = user_views.verify(john, dummy_request)
176        # redirect to user page
177        assert isinstance(response, HTTPFound)
178        assert response.location == dummy_request.resource_url(john)
179        # user was authenticated after verified
180        remember.assert_called_with(dummy_request, str(john.uid))
181        # user has been verified
182        assert john.verified
183
184    @patch('ow.views.user.send_verification_email')
185    def test_resend_verification_already_verified(
186            self, sve, dummy_request, john):
187        john.verified = True
188        assert john.verification_token is None
189        response = user_views.resend_verification_link(john, dummy_request)
190        assert isinstance(response, HTTPFound)
191        assert response.location == dummy_request.resource_url(
192            dummy_request.root, 'login', query={'message': 'already-verified'}
193        )
194        # no emails have been sent
195        assert not sve.called
196        # the token has not been modified
197        assert john.verification_token is None
198
199    @patch('ow.views.user.send_verification_email')
200    def test_resend_verification(self, sve, dummy_request, john):
201        john.verified = False
202        john.verification_token = 'faked-uuid4-verification-token'
203        assert john.verification_tokens_sent == 0
204        response = user_views.resend_verification_link(john, dummy_request)
205        assert isinstance(response, HTTPFound)
206        assert response.location == dummy_request.resource_url(
207            dummy_request.root, 'login',
208            query={'message': 'link-sent', 'email': john.email}
209        )
210        # no emails have been sent
211        sve.assert_called_once_with(dummy_request, john)
212        # the token has not been modified
213        assert john.verification_token == 'faked-uuid4-verification-token'
214        # and we have registered that we sent a token
215        assert john.verification_tokens_sent == 1
216
217    @patch('ow.views.user.send_verification_email')
218    def test_resend_verification_new_token(self, sve, dummy_request, john):
219        john.verified = False
220        john.verification_token = None
221        assert john.verification_tokens_sent == 0
222        response = user_views.resend_verification_link(john, dummy_request)
223        assert isinstance(response, HTTPFound)
224        assert response.location == dummy_request.resource_url(
225            dummy_request.root, 'login',
226            query={'message': 'link-sent', 'email': john.email}
227        )
228        # no emails have been sent
229        sve.assert_called_once_with(dummy_request, john)
230        # the token has been modified
231        assert john.verification_token is not None
232        assert john.verification_tokens_sent == 1
233
234    @patch('ow.views.user.send_verification_email')
235    def test_resend_verification_limit(
236            self, sve, dummy_request, john):
237        john.verified = False
238        john.verification_token = 'faked-uuid4-verification-token'
239        john.verification_tokens_sent = 4
240        response = user_views.resend_verification_link(john, dummy_request)
241        assert isinstance(response, HTTPFound)
242        assert response.location == dummy_request.resource_url(
243            dummy_request.root, 'login',
244            query={'message': 'max-tokens-sent', 'email': john.email}
245        )
246        # no emails have been sent
247        assert not sve.called
248        # the token has not been modified
249        assert john.verification_token == 'faked-uuid4-verification-token'
250        assert john.verification_tokens_sent == 4
251
252    def test_dashboard_redirect_unauthenticated(self, root):
253        """
254        Anonymous access to the root object, send the user to the login page.
255
256        Instead of reusing the DummyRequest from the request fixture, we do
257        Mock fully the request here, because we need to use
258        authenticated_userid, which cannot be easily set in the DummyRequest
259        """
260        request = DummyRequest()
261        request.root = root
262        response = user_views.dashboard_redirect(root, request)
263        assert isinstance(response, HTTPFound)
264        assert response.location == request.resource_url(root, 'login')
265
266    def test_dashboard_redirect_authenticated(self, root, john):
267        """
268        Authenticated user accesing the root object, send the user to her
269        dashboard
270
271        Instead of reusing the DummyRequest from the request fixture, we do
272        Mock fully the request here, because we need to use
273        authenticated_userid, which cannot be easily set in the DummyRequest
274        """
275        alt_request = DummyRequest()
276        request = Mock()
277        request.root = root
278        request.authenticated_userid = str(john.uid)
279        request.resource_url = alt_request.resource_url
280        response = user_views.dashboard_redirect(root, request)
281        assert isinstance(response, HTTPFound)
282        assert response.location == request.resource_url(john)
283        # if authenticated_userid is the id of an user that does not exist
284        # anymore, we send the user to the logout page
285        request.authenticated_userid = 'faked-uid'
286        response = user_views.dashboard_redirect(root, request)
287        assert isinstance(response, HTTPFound)
288        assert response.location == request.resource_url(root, 'logout')
289
290    def test_dashboard(self, dummy_request, john):
291        """
292        Renders the user dashboard
293        """
294        now = datetime.now(timezone.utc)
295        request = dummy_request
296        response = user_views.dashboard(john, request)
297        assert len(response) == 6
298        assert 'month_name' in response.keys()
299        assert response['current_year'] == now.year
300        assert response['current_day_name'] == now.strftime('%a')
301        # this user has a single workout, in 2015
302        assert response['viewing_year'] == 2015
303        assert response['viewing_month'] == 6
304        assert response['workouts'] == [w for w in john.workouts()]
305
306    def test_dashboard_year(self, dummy_request, john):
307        """
308        Renders the user dashboard for a chosen year.
309        """
310        now = datetime.now(timezone.utc)
311        request = dummy_request
312        # first test the year for which we know there is a workout
313        request.GET['year'] = 2015
314        response = user_views.dashboard(john, request)
315        assert len(response) == 6
316        assert 'month_name' in response.keys()
317        assert response['current_year'] == now.year
318        assert response['current_day_name'] == now.strftime('%a')
319        # this user has a single workout, in 2015
320        assert response['viewing_year'] == 2015
321        assert response['viewing_month'] == 6
322        assert response['workouts'] == [w for w in john.workouts()]
323        # now, a year we know there is no workout info
324        request.GET['year'] = 2000
325        response = user_views.dashboard(john, request)
326        assert len(response) == 6
327        assert 'month_name' in response.keys()
328        assert response['current_year'] == now.year
329        assert response['current_day_name'] == now.strftime('%a')
330        # this user has a single workout, in 2015
331        assert response['viewing_year'] == 2000
332        # we have no data for that year and we didn't ask for a certain month,
333        # so the passing value for that is None
334        assert response['viewing_month'] is None
335        assert response['workouts'] == []
336
337    def test_dashboard_year_month(self, dummy_request, john):
338        """
339        Renders the user dashboard for a chosen year and month.
340        """
341        now = datetime.now(timezone.utc)
342        request = dummy_request
343        # first test the year/month for which we know there is a workout
344        request.GET['year'] = 2015
345        request.GET['month'] = 6
346        response = user_views.dashboard(john, request)
347        assert len(response) == 6
348        assert 'month_name' in response.keys()
349        assert response['current_year'] == now.year
350        assert response['current_day_name'] == now.strftime('%a')
351        # this user has a single workout, in 2015
352        assert response['viewing_year'] == 2015
353        assert response['viewing_month'] == 6
354        assert response['workouts'] == [w for w in john.workouts()]
355        # now, change month to one without values
356        request.GET['month'] = 2
357        response = user_views.dashboard(john, request)
358        assert len(response) == 6
359        assert 'month_name' in response.keys()
360        assert response['current_year'] == now.year
361        assert response['current_day_name'] == now.strftime('%a')
362        # this user has a single workout, in 2015
363        assert response['viewing_year'] == 2015
364        assert response['viewing_month'] == 2
365        assert response['workouts'] == []
366        # now the month with data, but in a different year
367        request.GET['year'] = 2010
368        request.GET['month'] = 6
369        response = user_views.dashboard(john, request)
370        assert len(response) == 6
371        assert 'month_name' in response.keys()
372        assert response['current_year'] == now.year
373        assert response['current_day_name'] == now.strftime('%a')
374        # this user has a single workout, in 2015
375        assert response['viewing_year'] == 2010
376        assert response['viewing_month'] == 6
377        assert response['workouts'] == []
378
379    def test_dashboard_month(self, dummy_request, john):
380        """
381        Passing a month without a year when rendering the dashboard. The last
382        year for which workout data is available is assumed
383        """
384        now = datetime.now(timezone.utc)
385        request = dummy_request
386        # Set a month without workout data
387        request.GET['month'] = 5
388        response = user_views.dashboard(john, request)
389        assert len(response) == 6
390        assert 'month_name' in response.keys()
391        assert response['current_year'] == now.year
392        assert response['current_day_name'] == now.strftime('%a')
393        # this user has a single workout, in 2015
394        assert response['viewing_year'] == 2015
395        assert response['viewing_month'] == 5
396        assert response['workouts'] == []
397        # now a month with data
398        request.GET['month'] = 6
399        response = user_views.dashboard(john, request)
400        assert len(response) == 6
401        assert 'month_name' in response.keys()
402        assert response['current_year'] == now.year
403        assert response['current_day_name'] == now.strftime('%a')
404        # this user has a single workout, in 2015
405        assert response['viewing_year'] == 2015
406        assert response['viewing_month'] == 6
407        assert response['workouts'] == [w for w in john.workouts()]
408
409    def test_profile(self, dummy_request, john):
410        """
411        Renders the user profile page
412        """
413        request = dummy_request
414        # profile page for the current day (no workouts avalable)
415        response = user_views.profile(john, request)
416        assert len(response.keys()) == 7
417        current_month = datetime.now(timezone.utc).strftime('%Y-%m')
418        assert response['user'] == john
419        assert response['user_gender'] == 'Robot'
420        assert response['current_month'] == current_month
421        assert response['current_week'] is None
422        assert response['workouts'] == []
423        assert response['totals'] == {
424            'distance': Decimal(0),
425            'time': timedelta(0),
426            'elevation': Decimal(0)
427        }
428        assert response['profile_stats'] == {
429            'sports': ['cycling'],
430            'years': [2015],
431            'current_year': datetime.now(timezone.utc).year,
432            'current_sport': 'cycling'
433        }
434        # profile page for a previous date, that has workouts
435        request.GET['year'] = 2015
436        request.GET['month'] = 6
437        response = user_views.profile(john, request)
438        assert len(response.keys()) == 7
439        assert response['user'] == john
440        assert response['user_gender'] == 'Robot'
441        assert response['current_month'] == '2015-06'
442        assert response['current_week'] is None
443        workouts = john.workouts(2015, 6)
444        assert response['workouts'] == workouts
445        assert response['totals'] == {
446            'distance': workouts[0].distance,
447            'time': workouts[0].duration,
448            'elevation': Decimal(0)
449        }
450        # same, passing a week, first on a week without workouts
451        request.GET['year'] = 2015
452        request.GET['month'] = 6
453        request.GET['week'] = 25
454        response = user_views.profile(john, request)
455        assert len(response.keys()) == 7
456        assert response['user'] == john
457        assert response['user_gender'] == 'Robot'
458        assert response['current_month'] == '2015-06'
459        assert response['current_week'] == 25
460        assert response['workouts'] == []
461        assert response['totals'] == {
462            'distance': Decimal(0),
463            'time': timedelta(0),
464            'elevation': Decimal(0)
465        }
466        # now in a week with workouts
467        request.GET['year'] = 2015
468        request.GET['month'] = 6
469        request.GET['week'] = 26
470        response = user_views.profile(john, request)
471        assert len(response.keys()) == 7
472        assert response['user'] == john
473        assert response['user_gender'] == 'Robot'
474        assert response['current_month'] == '2015-06'
475        assert response['current_week'] == 26
476        workouts = john.workouts(2015, 6)
477        assert response['workouts'] == workouts
478        assert response['totals'] == {
479            'distance': workouts[0].distance,
480            'time': workouts[0].duration,
481            'elevation': Decimal(0)
482        }
483
484    def test_profile_with_nickname(self, dummy_request, john):
485        """
486        Loading the profile page using the user nickname
487        """
488        request = dummy_request
489        john.nickname = 'JohnDoe'
490        request.root.reindex(john)
491        # first load using the uuid
492        response_uuid = user_views.profile(john, request)
493        # now, do it with the nickname
494        request.subpath = ['JohnDoe']
495        response_nickname = user_views.profile(request.root, request)
496        assert response_uuid == response_nickname
497        # try an unknown nickname, HTTPNotFound is returned
498        request.subpath = ['Invalid']
499        response_nickname = user_views.profile(request.root, request)
500        assert isinstance(response_nickname, HTTPNotFound)
501
502    def test_login_get(self, dummy_request):
503        """
504        GET request to access the login page
505        """
506        request = dummy_request
507        response = user_views.login(request.root, request)
508        assert response['message'] == ''
509        assert response['email'] == ''
510        assert response['password'] == ''
511        assert response['redirect_url'] == request.resource_url(request.root)
512        assert response['resend_verify_link'] is None
513
514    def test_login_get_return_to(self, dummy_request, john):
515        """
516        GET request to access the login page, if there is a page set to where
517        the user should be sent to, the response "redirect_url" key will have
518        such url
519        """
520        request = dummy_request
521        workout = john.workouts()[0]
522        workout_url = request.resource_url(workout)
523        request.params['return_to'] = workout_url
524        response = user_views.login(request.root, request)
525        assert response['redirect_url'] == workout_url
526
527    def test_login_get_GET_messages(self, dummy_request):
528        """
529        GET request to access the login page, passing as a GET parameter
530        a "message key" so a given message is shown to the user
531        """
532        request = dummy_request
533        # first try with the keys we know there are messages associated to
534        request.GET['message'] = 'already-verified'
535        response = user_views.login(request.root, request)
536        assert response['message'] == 'User has been verified already'
537        request.GET['message'] = 'link-sent'
538        response = user_views.login(request.root, request)
539        assert response['message'] == (
540            'Verification link sent, please check your inbox')
541        request.GET['message'] = 'max-tokens-sent'
542        response = user_views.login(request.root, request)
543        assert response['message'] == (
544            'We already sent you the verification link more than three times')
545
546        # now try with a key that has no message assigned
547        request.GET['message'] = 'invalid-message-key'
548        response = user_views.login(request.root, request)
549        assert response['message'] == ''
550
551    def test_login_get_GET_email(self, dummy_request):
552        """
553        GET request to access the login page, passing as a GET parameter
554        an email address. That email is used to automatically fill in the
555        email input in the login form.
556        """
557        request = dummy_request
558        # first try with the keys we know there are messages associated to
559        request.GET['email'] = 'user@example.net'
560        response = user_views.login(request.root, request)
561        assert response['email'] == 'user@example.net'
562
563    def test_login_post_wrong_email(self, dummy_request):
564        request = dummy_request
565        request.method = 'POST'
566        request.POST['submit'] = True
567        request.POST['email'] = 'jack@example.net'
568        response = user_views.login(request.root, request)
569        assert response['message'] == u'Wrong email address'
570
571    def test_login_post_wrong_password(self, dummy_request):
572        request = dummy_request
573        request.method = 'POST'
574        request.POST['submit'] = True
575        request.POST['email'] = 'john.doe@example.net'
576        request.POST['password'] = 'badpassword'
577        # verify the user first
578        request.root.users[0].verified = True
579        response = user_views.login(request.root, request)
580        assert response['message'] == u'Wrong password'
581
582    @patch('ow.views.user.remember')
583    def test_login_post_unverified(self, rem, dummy_request, john):
584        request = dummy_request
585        request.method = 'POST'
586        request.POST['submit'] = True
587        request.POST['email'] = 'john.doe@example.net'
588        request.POST['password'] = 's3cr3t'
589        response = user_views.login(request.root, request)
590        assert response['message'] == u'You have to verify your account first'
591
592    @patch('ow.views.user.remember')
593    def test_login_post_ok(self, rem, dummy_request, john):
594        request = dummy_request
595        request.method = 'POST'
596        request.POST['submit'] = True
597        request.POST['email'] = 'john.doe@example.net'
598        request.POST['password'] = 's3cr3t'
599        # verify the user first
600        john.verified = True
601        response = user_views.login(request.root, request)
602        assert isinstance(response, HTTPFound)
603        assert rem.called
604        assert response.location == request.resource_url(john)
605        # the response headers contain the proper set_cookie for the default
606        # locale
607        default_locale_name = request.registry.settings[
608            'pyramid.default_locale_name']
609        expected_locale_header = '_LOCALE_=' + default_locale_name + '; Path=/'
610        assert response.headers['Set-Cookie'] == expected_locale_header
611
612    @patch('ow.views.user.remember')
613    def test_login_post_ok_set_locale(self, rem, dummy_request, john):
614        # same as the previous test, but this time the user has set a
615        # locale different than the default one
616        request = dummy_request
617        request.method = 'POST'
618        request.POST['submit'] = True
619        request.POST['email'] = 'john.doe@example.net'
620        request.POST['password'] = 's3cr3t'
621        # verify the user first
622        john.verified = True
623        # set the locale
624        john.locale = 'es'
625        response = user_views.login(request.root, request)
626        assert isinstance(response, HTTPFound)
627        assert rem.called
628        assert response.location == request.resource_url(john)
629        # the response headers contain the proper set_cookie for the user
630        # locale setting
631        expected_locale_header = '_LOCALE_=es; Path=/'
632        assert response.headers['Set-Cookie'] == expected_locale_header
633
634    @patch('ow.views.user.forget')
635    def test_logout(self, forg, dummy_request):
636        request = dummy_request
637        response = user_views.logout(request.root, request)
638        assert isinstance(response, HTTPFound)
639        assert forg.called
640        assert response.location == request.resource_url(request.root)
641        # the response headers contain the needed Set-Cookie header that
642        # invalidates the _LOCALE_ cookie, preventing problems with users
643        # sharing the same web browser (one locale setting being set for
644        # another user)
645        expected_locale_header = '_LOCALE_=; Max-Age=0; Path=/; expires='
646        assert expected_locale_header in response.headers['Set-Cookie']
647
648    extensions = ('png', 'jpg', 'jpeg', 'gif')
649
650    @pytest.mark.parametrize('extension', extensions)
651    def test_profile_picture(self, extension, dummy_request, john):
652        """
653        GET request to get the profile picture of an user.
654        """
655        request = dummy_request
656        # Get the user
657        user = john
658        # Get the path to the image, then open it and copy it to a new Blob
659        # object
660        path = 'fixtures/image.' + extension
661        image_path = os.path.join(
662            os.path.dirname(os.path.dirname(__file__)), path)
663        blob = Blob()
664        with open(image_path, 'rb') as infile, blob.open('w') as out:
665            infile.seek(0)
666            copyfileobj(infile, out)
667
668        # Set the blob with the picture
669        user.picture = blob
670
671        # Call the profile_picture view
672        response = user_views.profile_picture(user, request)
673        assert isinstance(response, Response)
674        assert response.status_int == 200
675        assert response.content_type == 'image'
676        # as we did not pass a specific size as a get parameter, the size is
677        # the same as the original image
678        original_image = Image.open(image_path)
679        returned_image = Image.open(BytesIO(response.body))
680        assert original_image.size == returned_image.size
681
682        # now, ask for a smaller image
683        request.GET['size'] = original_image.size[0] - 20
684        response = user_views.profile_picture(user, request)
685        assert isinstance(response, Response)
686        assert response.status_int == 200
687        assert response.content_type == 'image'
688        # now the size of the original image is bigger
689        returned_image = Image.open(BytesIO(response.body))
690        assert original_image.size > returned_image.size
691
692        # now, ask for a size that is bigger than the original image,
693        # image will be the same size, as we do not "grow" its size
694        request.GET['size'] = original_image.size[0] + 1000
695        response = user_views.profile_picture(user, request)
696        assert isinstance(response, Response)
697        assert response.status_int == 200
698        assert response.content_type == 'image'
699        # now the size of the original image is bigger
700        returned_image = Image.open(BytesIO(response.body))
701        assert original_image.size == returned_image.size
702
703    def test_profile_picture_none(self, dummy_request, john):
704        """
705        GET request for an user without a profile picture
706        """
707        request = dummy_request
708        user = john
709        response = user_views.profile_picture(user, request)
710        assert isinstance(response, HTTPNotFound)
711
712    def test_edit_profile_get(self, dummy_request, john):
713        """
714        GET request to the edit profile page, returns the form ready to
715        be rendered
716        """
717        request = dummy_request
718        user = john
719        response = user_views.edit_profile(user, request)
720        assert isinstance(response['form'], OWFormRenderer)
721        # no errors in the form (first load)
722        assert response['form'].errorlist() == ''
723        # the form carries along the proper data keys, taken from the
724        # loaded user profile
725        data = ['firstname', 'lastname', 'email', 'nickname', 'bio',
726                'birth_date', 'height', 'weight', 'gender', 'timezone',
727                'locale']
728        assert list(response['form'].data.keys()) == data
729        # and check the email to see data is properly loaded
730        assert response['form'].data['email'] == 'john.doe@example.net'
731        assert response['timezones'] == common_timezones
732        assert response[
733            'available_locale_names'] == get_available_locale_names()
734        assert response['current_locale'] == request.registry.settings[
735            'pyramid.default_locale_name']
736
737    def test_edit_profile_post_ok(self, profile_post_request, john):
738        request = profile_post_request
739        user = john
740        # Update the bio field
741        bio = 'Some text about this user'
742        request.POST['bio'] = bio
743        response = user_views.edit_profile(user, request)
744        assert isinstance(response, HTTPFound)
745        assert response.location == request.resource_url(user, 'profile')
746        assert user.bio == bio
747
748    def test_edit_profile_post_ok_change_locale(
749            self, profile_post_request, john):
750        request = profile_post_request
751        user = john
752        # Update the locale
753        request.POST['locale'] = 'es'
754        response = user_views.edit_profile(user, request)
755        assert isinstance(response, HTTPFound)
756        assert response.location == request.resource_url(user, 'profile')
757        assert user.locale == 'es'
758
759    def test_edit_profile_post_ok_invalid_locale(
760            self, profile_post_request, john):
761        request = profile_post_request
762        user = john
763        # Update the locale with an invalid option
764        request.POST['locale'] = 'XX'
765        response = user_views.edit_profile(user, request)
766        assert isinstance(response['form'], OWFormRenderer)
767        # as an error happened, the current_locale had not changed
768        assert response['form'].errors == {
769            'locale': "Value must be one of: en; es (not 'XX')"}
770        assert user.locale == 'en'
771
772    def test_edit_profile_post_missing_required(
773            self, profile_post_request, john):
774        request = profile_post_request
775        request.POST['email'] = ''
776        user = john
777        response = user_views.edit_profile(user, request)
778        assert isinstance(response['form'], OWFormRenderer)
779        # error on the missing email field
780        error = u'Please enter an email address'
781        html_error = u'<ul class="error"><li>' + error + '</li></ul>'
782        assert response['form'].errorlist() == html_error
783        assert response['form'].errors_for('email') == [error]
784
785    def test_edit_profile_post_ok_picture_empty_bytes(
786            self, profile_post_request, john):
787        """
788        POST request with an empty picture, the content of
789        request['POST'].picture is a empty bytes string (b'') which triggers
790        a bug in formencode, we put a fix in place, test that
791        (more in ow.user.views.edit_profile)
792        """
793        # for the purposes of this test, we can mock the picture
794        picture = Mock()
795        john.picture = picture
796        request = profile_post_request
797        user = john
798        # Mimic what happens when a picture is not provided by the user
799        request.POST['picture'] = b''
800        response = user_views.edit_profile(user, request)
801        assert isinstance(response, HTTPFound)
802        assert response.location == request.resource_url(user, 'profile')
803        assert user.picture == picture
804
805    def test_edit_profile_post_ok_missing_picture(
806            self, profile_post_request, john):
807        """
808        POST request without picture
809        """
810        # for the purposes of this test, we can mock the picture
811        picture = Mock()
812        john.picture = picture
813        request = profile_post_request
814        user = john
815        # No pic is provided in the request POST values
816        del request.POST['picture']
817        response = user_views.edit_profile(user, request)
818        assert isinstance(response, HTTPFound)
819        assert response.location == request.resource_url(user, 'profile')
820        assert user.picture == picture
821
822    def test_edit_profile_post_ok_nickname(self, profile_post_request, john):
823        """
824        User with a nickname set saves profile without changing the profile,
825        we have to be sure there are no "nickname already in use" errors
826        """
827        request = profile_post_request
828        user = john
829        user.nickname = 'mr_jones'
830        # add the nickname, the default post request has not a nickname set
831        request.POST['nickname'] = 'mr_jones'
832        response = user_views.edit_profile(user, request)
833        assert isinstance(response, HTTPFound)
834        assert response.location == request.resource_url(user, 'profile')
835
836    def test_change_password_get(self, dummy_request, john):
837        request = dummy_request
838        user = john
839        response = user_views.change_password(user, request)
840        assert isinstance(response['form'], OWFormRenderer)
841        # no errors in the form (first load)
842        assert response['form'].errorlist() == ''
843
844    def test_change_password_post_ok(self, passwd_post_request, john):
845        request = passwd_post_request
846        user = john
847        request.POST['old_password'] = 's3cr3t'
848        request.POST['password'] = 'h1dd3n s3cr3t'
849        request.POST['password_confirm'] = 'h1dd3n s3cr3t'
850        response = user_views.change_password(user, request)
851        assert isinstance(response, HTTPFound)
852        assert response.location == request.resource_url(user, 'profile')
853        # password was changed
854        assert not user.check_password('s3cr3t')
855        assert user.check_password('h1dd3n s3cr3t')
856
857    def test_change_password_post_no_values(self, passwd_post_request, john):
858        request = passwd_post_request
859        user = john
860        response = user_views.change_password(user, request)
861        assert isinstance(response['form'], OWFormRenderer)
862        error = u'Please enter a value'
863        html_error = u'<ul class="error">'
864        html_error += ('<li>' + error + '</li>') * 3  # 3 fields
865        html_error += '</ul>'
866        errorlist = response['form'].errorlist().replace('\n', '')
867        assert errorlist == html_error
868        assert response['form'].errors_for('old_password') == [error]
869        assert response['form'].errors_for('password') == [error]
870        assert response['form'].errors_for('password_confirm') == [error]
871        # password was not changed
872        assert user.check_password('s3cr3t')
873
874    def test_change_password_post_bad_old_password(
875            self, passwd_post_request, john):
876        request = passwd_post_request
877        user = john
878        request.POST['old_password'] = 'FAIL PASSWORD'
879        request.POST['password'] = 'h1dd3n s3cr3t'
880        request.POST['password_confirm'] = 'h1dd3n s3cr3t'
881        response = user_views.change_password(user, request)
882        assert isinstance(response['form'], OWFormRenderer)
883        error = u'The given password does not match the existing one '
884        html_error = u'<ul class="error"><li>' + error + '</li></ul>'
885        assert response['form'].errorlist() == html_error
886        assert response['form'].errors_for('old_password') == [error]
887        # password was not changed
888        assert user.check_password('s3cr3t')
889        assert not user.check_password('h1dd3n s3cr3t')
890
891    def test_change_password_post_password_mismatch(
892            self, passwd_post_request, john):
893        request = passwd_post_request
894        user = john
895        request.POST['old_password'] = 's3cr3t'
896        request.POST['password'] = 'h1dd3n s3cr3ts'
897        request.POST['password_confirm'] = 'h1dd3n s3cr3t'
898        response = user_views.change_password(user, request)
899        assert isinstance(response['form'], OWFormRenderer)
900        error = u'Fields do not match'
901        html_error = u'<ul class="error"><li>' + error + '</li></ul>'
902        assert response['form'].errorlist() == html_error
903        assert response['form'].errors_for('password_confirm') == [error]
904        # password was not changed
905        assert user.check_password('s3cr3t')
906        assert not user.check_password('h1dd3n s3cr3t')
907
908    def test_signup_get(self, dummy_request):
909        request = dummy_request
910        response = user_views.signup(request.root, request)
911        assert isinstance(response['form'], OWFormRenderer)
912        # no errors in the form (first load)
913        assert response['form'].errorlist() == ''
914
915    @patch('ow.views.user.send_verification_email')
916    def test_signup_post_ok(self, sve, signup_post_request):
917        request = signup_post_request
918        assert 'jack.black@example.net' not in request.root.emails
919        assert 'JackBlack' not in request.root.all_nicknames
920        response = user_views.signup(request.root, request)
921        assert isinstance(response, HTTPFound)
922        assert response.location == request.resource_url(request.root)
923        assert 'jack.black@example.net' in request.root.emails
924        assert 'JackBlack' in request.root.all_nicknames
925        # user is in "unverified" state
926        user = request.root.get_user_by_email('jack.black@example.net')
927        assert not user.verified
928        assert isinstance(user.verification_token, UUID)
929        # also, we sent an email to that user
930        sve.assert_called_once_with(request, user)
931
932    def test_signup_missing_required(self, signup_post_request):
933        request = signup_post_request
934        request.POST['email'] = ''
935        assert 'jack.black@example.net' not in request.root.emails
936        assert 'JackBlack' not in request.root.all_nicknames
937        response = user_views.signup(request.root, request)
938        assert isinstance(response['form'], OWFormRenderer)
939        error = u'Please enter an email address'
940        html_error = '<ul class="error">'
941        html_error += '<li>' + error + '</li>'
942        html_error += '</ul>'
943        errorlist = response['form'].errorlist().replace('\n', '')
944        assert errorlist == html_error
945        assert response['form'].errors_for('email') == [error]
946        assert 'jack.black@example.net' not in request.root.emails
947        assert 'JackBlack' not in request.root.all_nicknames
948
949    def test_signup_existing_nickname(self, signup_post_request, john):
950        request = signup_post_request
951        # assign john a nickname first
952        john.nickname = 'john'
953        # now set it for the POST request
954        request.POST['nickname'] = 'john'
955        # check jack is not there yet
956        assert 'jack.black@example.net' not in request.root.emails
957        assert 'JackBlack' not in request.root.all_nicknames
958        # now signup as jack, but trying to set the nickname 'john'
959        response = user_views.signup(request.root, request)
960        assert isinstance(response['form'], OWFormRenderer)
961        error = u'Another user is already using the nickname john'
962        html_error = '<ul class="error">'
963        html_error += '<li>' + error + '</li>'
964        html_error += '</ul>'
965        errorlist = response['form'].errorlist().replace('\n', '')
966        assert errorlist == html_error
967        assert response['form'].errors_for('nickname') == [error]
968        # all the errors, and jack is not there
969        assert 'jack.black@example.net' not in request.root.emails
970        assert 'JackBlack' not in request.root.all_nicknames
971
972    def test_signup_existing_email(self, signup_post_request):
973        request = signup_post_request
974        request.POST['email'] = 'john.doe@example.net'
975        assert 'jack.black@example.net' not in request.root.emails
976        assert 'JackBlack' not in request.root.all_nicknames
977        response = user_views.signup(request.root, request)
978        assert isinstance(response['form'], OWFormRenderer)
979        error = u'Another user is already registered with the email '
980        error += u'john.doe@example.net'
981        html_error = '<ul class="error">'
982        html_error += '<li>' + error + '</li>'
983        html_error += '</ul>'
984        errorlist = response['form'].errorlist().replace('\n', '')
985        assert errorlist == html_error
986        assert response['form'].errors_for('email') == [error]
987        assert 'jack.black@example.net' not in request.root.emails
988        assert 'JackBlack' not in request.root.all_nicknames
989
990    def test_week_stats_no_stats(self, dummy_request, john):
991        response = user_views.week_stats(john, dummy_request)
992        assert isinstance(response, Response)
993        assert response.content_type == 'application/json'
994        # the body is a valid json-encoded stream
995        obj = json.loads(response.body)
996        assert obj == [
997            {'distance': 0, 'elevation': 0, 'name': 'Mon',
998             'time': '00', 'workouts': 0},
999            {'distance': 0, 'elevation': 0, 'name': 'Tue',
1000             'time': '00', 'workouts': 0},
1001            {'distance': 0, 'elevation': 0, 'name': 'Wed',
1002             'time': '00', 'workouts': 0},
1003            {'distance': 0, 'elevation': 0, 'name': 'Thu',
1004             'time': '00', 'workouts': 0},
1005            {'distance': 0, 'elevation': 0, 'name': 'Fri',
1006             'time': '00', 'workouts': 0},
1007            {'distance': 0, 'elevation': 0, 'name': 'Sat',
1008             'time': '00', 'workouts': 0},
1009            {'distance': 0, 'elevation': 0, 'name': 'Sun',
1010             'time': '00', 'workouts': 0}
1011        ]
1012
1013    def test_week_stats(self, dummy_request, john):
1014        workout = Workout(
1015            start=datetime.now(timezone.utc),
1016            duration=timedelta(minutes=60),
1017            distance=30,
1018            elevation=540
1019        )
1020        john.add_workout(workout)
1021        response = user_views.week_stats(john, dummy_request)
1022        assert isinstance(response, Response)
1023        assert response.content_type == 'application/json'
1024        # the body is a valid json-encoded stream
1025        obj = json.loads(response.body)
1026        assert len(obj) == 7
1027        for day in obj:
1028            if datetime.now(timezone.utc).strftime('%a') == day['name']:
1029                day['distance'] == 30
1030                day['elevation'] == 540
1031                day['time'] == '01'
1032                day['workouts'] == 1
1033            else:
1034                day['distance'] == 0
1035                day['elevation'] == 0
1036                day['time'] == '00'
1037                day['workouts'] == 0
1038
1039    def test_last_months_stats(self, dummy_request, john):
1040        request = dummy_request
1041        user = john
1042        response = user_views.last_months_stats(user, request)
1043        assert isinstance(response, Response)
1044        assert response.content_type == 'application/json'
1045        # the body is a valid json-encoded stream
1046        obj = json.loads(response.body)
1047        assert len(obj) == 13
1048        for month in obj:
1049            assert len(month.keys()) == 7
1050            assert 'id' in month.keys()
1051            assert 'name' in month.keys()
1052            assert month['distance'] == 0
1053            assert month['elevation'] == 0
1054            assert month['time'] == '00'
1055            assert month['workouts'] == 0
1056            assert str(user.uid) in month['url']
1057            assert 'profile' in month['url']
1058            assert 'year' in month['url']
1059            assert 'month' in month['url']
1060
1061        # try now with a workout
1062        workout_start = datetime.now(timezone.utc)
1063        workout_start_id = (
1064            str(workout_start.year) + '-' + str(workout_start.month).zfill(2))
1065        workout = Workout(
1066            sport='cycling',
1067            start=workout_start,
1068            duration=timedelta(minutes=60),
1069            distance=30,
1070            uphill=540
1071        )
1072        user.add_workout(workout)
1073        response = user_views.last_months_stats(user, request)
1074        assert isinstance(response, Response)
1075        assert response.content_type == 'application/json'
1076        # the body is a valid json-encoded stream
1077        obj = json.loads(response.body)
1078        assert len(obj) == 13
1079        for month in obj:
1080            assert len(month.keys()) == 7
1081            assert 'id' in month.keys()
1082            assert 'name' in month.keys()
1083            assert str(user.uid) in month['url']
1084            assert 'profile' in month['url']
1085            assert 'year' in month['url']
1086            assert 'month' in month['url']
1087            if month['id'] == workout_start_id:
1088                assert month['distance'] == 30
1089                assert month['elevation'] == 540
1090                assert month['time'] == '01'
1091                assert month['workouts'] == 1
1092            else:
1093                assert month['distance'] == 0
1094                assert month['elevation'] == 0
1095                assert month['time'] == '00'
1096                assert month['workouts'] == 0
1097
1098    def test_last_weeks_stats(self, dummy_request, john):
1099        request = dummy_request
1100        user = john
1101        response = user_views.last_weeks_stats(user, request)
1102        assert isinstance(response, Response)
1103        assert response.content_type == 'application/json'
1104        # the body is a valid json-encoded stream
1105        obj = json.loads(response.body)
1106        for week in obj:
1107            assert len(week.keys()) == 8
1108            assert 'id' in week.keys()
1109            assert 'name' in week.keys()
1110            assert 'week' in week.keys()
1111            assert week['distance'] == 0
1112            assert week['elevation'] == 0
1113            assert week['time'] == '00'
1114            assert week['workouts'] == 0
1115            assert str(user.uid) in week['url']
1116            assert 'profile' in week['url']
1117            assert 'year' in week['url']
1118            assert 'month' in week['url']
1119            assert 'week' in week['url']
1120
1121        # try now with a workout
1122        workout_start = datetime.now(timezone.utc)
1123        workout_start_id = str(workout_start.year)
1124        workout_start_id += '-' + str(workout_start.month).zfill(2)
1125        workout_start_id += '-' + str(workout_start.isocalendar()[1])
1126        workout = Workout(
1127            sport='cycling',
1128            start=workout_start,
1129            duration=timedelta(minutes=60),
1130            distance=30,
1131            uphill=540
1132        )
1133        user.add_workout(workout)
1134        response = user_views.last_weeks_stats(user, request)
1135        assert isinstance(response, Response)
1136        assert response.content_type == 'application/json'
1137        # the body is a valid json-encoded stream
1138        obj = json.loads(response.body)
1139        for week in obj:
1140            assert len(week.keys()) == 8
1141            assert 'id' in week.keys()
1142            assert 'name' in week.keys()
1143            assert 'week' in week.keys()
1144            assert str(user.uid) in week['url']
1145            assert 'profile' in week['url']
1146            assert 'year' in week['url']
1147            assert 'month' in week['url']
1148            assert 'week' in week['url']
1149            if week['id'] == workout_start_id:
1150                assert week['distance'] == 30
1151                assert week['elevation'] == 540
1152                assert week['time'] == '01'
1153                assert week['workouts'] == 1
1154            else:
1155                assert week['distance'] == 0
1156                assert week['elevation'] == 0
1157                assert week['time'] == '00'
1158                assert week['workouts'] == 0
Note: See TracBrowser for help on using the repository browser.