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

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

(#71) Keep the "distance/time/elevation" filter when clicking on bars
in the profile yearly stats chart

  • Property mode set to 100644
File size: 48.4 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()) == 8
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        assert response['filter_by'] == 'distance'
435        # profile page for a previous date, that has workouts
436        request.GET['year'] = 2015
437        request.GET['month'] = 6
438        response = user_views.profile(john, request)
439        assert len(response.keys()) == 8
440        assert response['user'] == john
441        assert response['user_gender'] == 'Robot'
442        assert response['current_month'] == '2015-06'
443        assert response['current_week'] is None
444        workouts = john.workouts(2015, 6)
445        assert response['workouts'] == workouts
446        assert response['totals'] == {
447            'distance': workouts[0].distance,
448            'time': workouts[0].duration,
449            'elevation': Decimal(0)
450        }
451        assert response['filter_by'] == 'distance'
452        # same request, but passing a filter_by value
453        request.GET['filter_by'] = 'time'
454        response = user_views.profile(john, request)
455        assert len(response.keys()) == 8
456        assert response['filter_by'] == 'time'
457        # same, passing a week, first on a week without workouts,
458        # keeping the modified filter_by GET param
459        request.GET['year'] = 2015
460        request.GET['month'] = 6
461        request.GET['week'] = 25
462        response = user_views.profile(john, request)
463        assert len(response.keys()) == 8
464        assert response['user'] == john
465        assert response['user_gender'] == 'Robot'
466        assert response['current_month'] == '2015-06'
467        assert response['current_week'] == 25
468        assert response['workouts'] == []
469        assert response['totals'] == {
470            'distance': Decimal(0),
471            'time': timedelta(0),
472            'elevation': Decimal(0)
473        }
474        assert response['filter_by'] == 'time'
475        # now in a week with workouts, changing the filter_by again
476        request.GET['year'] = 2015
477        request.GET['month'] = 6
478        request.GET['week'] = 26
479        request.GET['filter_by'] = 'elevation'
480        response = user_views.profile(john, request)
481        assert len(response.keys()) == 8
482        assert response['user'] == john
483        assert response['user_gender'] == 'Robot'
484        assert response['current_month'] == '2015-06'
485        assert response['current_week'] == 26
486        workouts = john.workouts(2015, 6)
487        assert response['workouts'] == workouts
488        assert response['totals'] == {
489            'distance': workouts[0].distance,
490            'time': workouts[0].duration,
491            'elevation': Decimal(0)
492        }
493        assert response['filter_by'] == 'elevation'
494
495    def test_profile_with_nickname(self, dummy_request, john):
496        """
497        Loading the profile page using the user nickname
498        """
499        request = dummy_request
500        john.nickname = 'JohnDoe'
501        request.root.reindex(john)
502        # first load using the uuid
503        response_uuid = user_views.profile(john, request)
504        # now, do it with the nickname
505        request.subpath = ['JohnDoe']
506        response_nickname = user_views.profile(request.root, request)
507        assert response_uuid == response_nickname
508        # try an unknown nickname, HTTPNotFound is returned
509        request.subpath = ['Invalid']
510        response_nickname = user_views.profile(request.root, request)
511        assert isinstance(response_nickname, HTTPNotFound)
512
513    def test_login_get(self, dummy_request):
514        """
515        GET request to access the login page
516        """
517        request = dummy_request
518        response = user_views.login(request.root, request)
519        assert response['message'] == ''
520        assert response['email'] == ''
521        assert response['password'] == ''
522        assert response['redirect_url'] == request.resource_url(request.root)
523        assert response['resend_verify_link'] is None
524
525    def test_login_get_return_to(self, dummy_request, john):
526        """
527        GET request to access the login page, if there is a page set to where
528        the user should be sent to, the response "redirect_url" key will have
529        such url
530        """
531        request = dummy_request
532        workout = john.workouts()[0]
533        workout_url = request.resource_url(workout)
534        request.params['return_to'] = workout_url
535        response = user_views.login(request.root, request)
536        assert response['redirect_url'] == workout_url
537
538    def test_login_get_GET_messages(self, dummy_request):
539        """
540        GET request to access the login page, passing as a GET parameter
541        a "message key" so a given message is shown to the user
542        """
543        request = dummy_request
544        # first try with the keys we know there are messages associated to
545        request.GET['message'] = 'already-verified'
546        response = user_views.login(request.root, request)
547        assert response['message'] == 'User has been verified already'
548        request.GET['message'] = 'link-sent'
549        response = user_views.login(request.root, request)
550        assert response['message'] == (
551            'Verification link sent, please check your inbox')
552        request.GET['message'] = 'max-tokens-sent'
553        response = user_views.login(request.root, request)
554        assert response['message'] == (
555            'We already sent you the verification link more than three times')
556
557        # now try with a key that has no message assigned
558        request.GET['message'] = 'invalid-message-key'
559        response = user_views.login(request.root, request)
560        assert response['message'] == ''
561
562    def test_login_get_GET_email(self, dummy_request):
563        """
564        GET request to access the login page, passing as a GET parameter
565        an email address. That email is used to automatically fill in the
566        email input in the login form.
567        """
568        request = dummy_request
569        # first try with the keys we know there are messages associated to
570        request.GET['email'] = 'user@example.net'
571        response = user_views.login(request.root, request)
572        assert response['email'] == 'user@example.net'
573
574    def test_login_post_wrong_email(self, dummy_request):
575        request = dummy_request
576        request.method = 'POST'
577        request.POST['submit'] = True
578        request.POST['email'] = 'jack@example.net'
579        response = user_views.login(request.root, request)
580        assert response['message'] == u'Wrong email address'
581
582    def test_login_post_wrong_password(self, dummy_request):
583        request = dummy_request
584        request.method = 'POST'
585        request.POST['submit'] = True
586        request.POST['email'] = 'john.doe@example.net'
587        request.POST['password'] = 'badpassword'
588        # verify the user first
589        request.root.users[0].verified = True
590        response = user_views.login(request.root, request)
591        assert response['message'] == u'Wrong password'
592
593    @patch('ow.views.user.remember')
594    def test_login_post_unverified(self, rem, dummy_request, john):
595        request = dummy_request
596        request.method = 'POST'
597        request.POST['submit'] = True
598        request.POST['email'] = 'john.doe@example.net'
599        request.POST['password'] = 's3cr3t'
600        response = user_views.login(request.root, request)
601        assert response['message'] == u'You have to verify your account first'
602
603    @patch('ow.views.user.remember')
604    def test_login_post_ok(self, rem, dummy_request, john):
605        request = dummy_request
606        request.method = 'POST'
607        request.POST['submit'] = True
608        request.POST['email'] = 'john.doe@example.net'
609        request.POST['password'] = 's3cr3t'
610        # verify the user first
611        john.verified = True
612        response = user_views.login(request.root, request)
613        assert isinstance(response, HTTPFound)
614        assert rem.called
615        assert response.location == request.resource_url(john)
616        # the response headers contain the proper set_cookie for the default
617        # locale
618        default_locale_name = request.registry.settings[
619            'pyramid.default_locale_name']
620        expected_locale_header = '_LOCALE_=' + default_locale_name + '; Path=/'
621        assert response.headers['Set-Cookie'] == expected_locale_header
622
623    @patch('ow.views.user.remember')
624    def test_login_post_ok_set_locale(self, rem, dummy_request, john):
625        # same as the previous test, but this time the user has set a
626        # locale different than the default one
627        request = dummy_request
628        request.method = 'POST'
629        request.POST['submit'] = True
630        request.POST['email'] = 'john.doe@example.net'
631        request.POST['password'] = 's3cr3t'
632        # verify the user first
633        john.verified = True
634        # set the locale
635        john.locale = 'es'
636        response = user_views.login(request.root, request)
637        assert isinstance(response, HTTPFound)
638        assert rem.called
639        assert response.location == request.resource_url(john)
640        # the response headers contain the proper set_cookie for the user
641        # locale setting
642        expected_locale_header = '_LOCALE_=es; Path=/'
643        assert response.headers['Set-Cookie'] == expected_locale_header
644
645    @patch('ow.views.user.forget')
646    def test_logout(self, forg, dummy_request):
647        request = dummy_request
648        response = user_views.logout(request.root, request)
649        assert isinstance(response, HTTPFound)
650        assert forg.called
651        assert response.location == request.resource_url(request.root)
652        # the response headers contain the needed Set-Cookie header that
653        # invalidates the _LOCALE_ cookie, preventing problems with users
654        # sharing the same web browser (one locale setting being set for
655        # another user)
656        expected_locale_header = '_LOCALE_=; Max-Age=0; Path=/; expires='
657        assert expected_locale_header in response.headers['Set-Cookie']
658
659    extensions = ('png', 'jpg', 'jpeg', 'gif')
660
661    @pytest.mark.parametrize('extension', extensions)
662    def test_profile_picture(self, extension, dummy_request, john):
663        """
664        GET request to get the profile picture of an user.
665        """
666        request = dummy_request
667        # Get the user
668        user = john
669        # Get the path to the image, then open it and copy it to a new Blob
670        # object
671        path = 'fixtures/image.' + extension
672        image_path = os.path.join(
673            os.path.dirname(os.path.dirname(__file__)), path)
674        blob = Blob()
675        with open(image_path, 'rb') as infile, blob.open('w') as out:
676            infile.seek(0)
677            copyfileobj(infile, out)
678
679        # Set the blob with the picture
680        user.picture = blob
681
682        # Call the profile_picture view
683        response = user_views.profile_picture(user, request)
684        assert isinstance(response, Response)
685        assert response.status_int == 200
686        assert response.content_type == 'image'
687        # as we did not pass a specific size as a get parameter, the size is
688        # the same as the original image
689        original_image = Image.open(image_path)
690        returned_image = Image.open(BytesIO(response.body))
691        assert original_image.size == returned_image.size
692
693        # now, ask for a smaller image
694        request.GET['size'] = original_image.size[0] - 20
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        # now, ask for a size that is bigger than the original image,
704        # image will be the same size, as we do not "grow" its size
705        request.GET['size'] = original_image.size[0] + 1000
706        response = user_views.profile_picture(user, request)
707        assert isinstance(response, Response)
708        assert response.status_int == 200
709        assert response.content_type == 'image'
710        # now the size of the original image is bigger
711        returned_image = Image.open(BytesIO(response.body))
712        assert original_image.size == returned_image.size
713
714    def test_profile_picture_none(self, dummy_request, john):
715        """
716        GET request for an user without a profile picture
717        """
718        request = dummy_request
719        user = john
720        response = user_views.profile_picture(user, request)
721        assert isinstance(response, HTTPNotFound)
722
723    def test_edit_profile_get(self, dummy_request, john):
724        """
725        GET request to the edit profile page, returns the form ready to
726        be rendered
727        """
728        request = dummy_request
729        user = john
730        response = user_views.edit_profile(user, request)
731        assert isinstance(response['form'], OWFormRenderer)
732        # no errors in the form (first load)
733        assert response['form'].errorlist() == ''
734        # the form carries along the proper data keys, taken from the
735        # loaded user profile
736        data = ['firstname', 'lastname', 'email', 'nickname', 'bio',
737                'birth_date', 'height', 'weight', 'gender', 'timezone',
738                'locale']
739        assert list(response['form'].data.keys()) == data
740        # and check the email to see data is properly loaded
741        assert response['form'].data['email'] == 'john.doe@example.net'
742        assert response['timezones'] == common_timezones
743        assert response[
744            'available_locale_names'] == get_available_locale_names()
745        assert response['current_locale'] == request.registry.settings[
746            'pyramid.default_locale_name']
747
748    def test_edit_profile_post_ok(self, profile_post_request, john):
749        request = profile_post_request
750        user = john
751        # Update the bio field
752        bio = 'Some text about this user'
753        request.POST['bio'] = bio
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.bio == bio
758
759    def test_edit_profile_post_ok_change_locale(
760            self, profile_post_request, john):
761        request = profile_post_request
762        user = john
763        # Update the locale
764        request.POST['locale'] = 'es'
765        response = user_views.edit_profile(user, request)
766        assert isinstance(response, HTTPFound)
767        assert response.location == request.resource_url(user, 'profile')
768        assert user.locale == 'es'
769
770    def test_edit_profile_post_ok_invalid_locale(
771            self, profile_post_request, john):
772        request = profile_post_request
773        user = john
774        # Update the locale with an invalid option
775        request.POST['locale'] = 'XX'
776        response = user_views.edit_profile(user, request)
777        assert isinstance(response['form'], OWFormRenderer)
778        # as an error happened, the current_locale had not changed
779        assert response['form'].errors == {
780            'locale': "Value must be one of: en; es (not 'XX')"}
781        assert user.locale == 'en'
782
783    def test_edit_profile_post_missing_required(
784            self, profile_post_request, john):
785        request = profile_post_request
786        request.POST['email'] = ''
787        user = john
788        response = user_views.edit_profile(user, request)
789        assert isinstance(response['form'], OWFormRenderer)
790        # error on the missing email field
791        error = u'Please enter an email address'
792        html_error = u'<ul class="error"><li>' + error + '</li></ul>'
793        assert response['form'].errorlist() == html_error
794        assert response['form'].errors_for('email') == [error]
795
796    def test_edit_profile_post_ok_picture_empty_bytes(
797            self, profile_post_request, john):
798        """
799        POST request with an empty picture, the content of
800        request['POST'].picture is a empty bytes string (b'') which triggers
801        a bug in formencode, we put a fix in place, test that
802        (more in ow.user.views.edit_profile)
803        """
804        # for the purposes of this test, we can mock the picture
805        picture = Mock()
806        john.picture = picture
807        request = profile_post_request
808        user = john
809        # Mimic what happens when a picture is not provided by the user
810        request.POST['picture'] = b''
811        response = user_views.edit_profile(user, request)
812        assert isinstance(response, HTTPFound)
813        assert response.location == request.resource_url(user, 'profile')
814        assert user.picture == picture
815
816    def test_edit_profile_post_ok_missing_picture(
817            self, profile_post_request, john):
818        """
819        POST request without picture
820        """
821        # for the purposes of this test, we can mock the picture
822        picture = Mock()
823        john.picture = picture
824        request = profile_post_request
825        user = john
826        # No pic is provided in the request POST values
827        del request.POST['picture']
828        response = user_views.edit_profile(user, request)
829        assert isinstance(response, HTTPFound)
830        assert response.location == request.resource_url(user, 'profile')
831        assert user.picture == picture
832
833    def test_edit_profile_post_ok_nickname(self, profile_post_request, john):
834        """
835        User with a nickname set saves profile without changing the profile,
836        we have to be sure there are no "nickname already in use" errors
837        """
838        request = profile_post_request
839        user = john
840        user.nickname = 'mr_jones'
841        # add the nickname, the default post request has not a nickname set
842        request.POST['nickname'] = 'mr_jones'
843        response = user_views.edit_profile(user, request)
844        assert isinstance(response, HTTPFound)
845        assert response.location == request.resource_url(user, 'profile')
846
847    def test_change_password_get(self, dummy_request, john):
848        request = dummy_request
849        user = john
850        response = user_views.change_password(user, request)
851        assert isinstance(response['form'], OWFormRenderer)
852        # no errors in the form (first load)
853        assert response['form'].errorlist() == ''
854
855    def test_change_password_post_ok(self, passwd_post_request, john):
856        request = passwd_post_request
857        user = john
858        request.POST['old_password'] = 's3cr3t'
859        request.POST['password'] = 'h1dd3n s3cr3t'
860        request.POST['password_confirm'] = 'h1dd3n s3cr3t'
861        response = user_views.change_password(user, request)
862        assert isinstance(response, HTTPFound)
863        assert response.location == request.resource_url(user, 'profile')
864        # password was changed
865        assert not user.check_password('s3cr3t')
866        assert user.check_password('h1dd3n s3cr3t')
867
868    def test_change_password_post_no_values(self, passwd_post_request, john):
869        request = passwd_post_request
870        user = john
871        response = user_views.change_password(user, request)
872        assert isinstance(response['form'], OWFormRenderer)
873        error = u'Please enter a value'
874        html_error = u'<ul class="error">'
875        html_error += ('<li>' + error + '</li>') * 3  # 3 fields
876        html_error += '</ul>'
877        errorlist = response['form'].errorlist().replace('\n', '')
878        assert errorlist == html_error
879        assert response['form'].errors_for('old_password') == [error]
880        assert response['form'].errors_for('password') == [error]
881        assert response['form'].errors_for('password_confirm') == [error]
882        # password was not changed
883        assert user.check_password('s3cr3t')
884
885    def test_change_password_post_bad_old_password(
886            self, passwd_post_request, john):
887        request = passwd_post_request
888        user = john
889        request.POST['old_password'] = 'FAIL PASSWORD'
890        request.POST['password'] = 'h1dd3n s3cr3t'
891        request.POST['password_confirm'] = 'h1dd3n s3cr3t'
892        response = user_views.change_password(user, request)
893        assert isinstance(response['form'], OWFormRenderer)
894        error = u'The given password does not match the existing one '
895        html_error = u'<ul class="error"><li>' + error + '</li></ul>'
896        assert response['form'].errorlist() == html_error
897        assert response['form'].errors_for('old_password') == [error]
898        # password was not changed
899        assert user.check_password('s3cr3t')
900        assert not user.check_password('h1dd3n s3cr3t')
901
902    def test_change_password_post_password_mismatch(
903            self, passwd_post_request, john):
904        request = passwd_post_request
905        user = john
906        request.POST['old_password'] = 's3cr3t'
907        request.POST['password'] = 'h1dd3n s3cr3ts'
908        request.POST['password_confirm'] = 'h1dd3n s3cr3t'
909        response = user_views.change_password(user, request)
910        assert isinstance(response['form'], OWFormRenderer)
911        error = u'Fields do not match'
912        html_error = u'<ul class="error"><li>' + error + '</li></ul>'
913        assert response['form'].errorlist() == html_error
914        assert response['form'].errors_for('password_confirm') == [error]
915        # password was not changed
916        assert user.check_password('s3cr3t')
917        assert not user.check_password('h1dd3n s3cr3t')
918
919    def test_signup_get(self, dummy_request):
920        request = dummy_request
921        response = user_views.signup(request.root, request)
922        assert isinstance(response['form'], OWFormRenderer)
923        # no errors in the form (first load)
924        assert response['form'].errorlist() == ''
925
926    @patch('ow.views.user.send_verification_email')
927    def test_signup_post_ok(self, sve, signup_post_request):
928        request = signup_post_request
929        assert 'jack.black@example.net' not in request.root.emails
930        assert 'JackBlack' not in request.root.all_nicknames
931        response = user_views.signup(request.root, request)
932        assert isinstance(response, HTTPFound)
933        assert response.location == request.resource_url(request.root)
934        assert 'jack.black@example.net' in request.root.emails
935        assert 'JackBlack' in request.root.all_nicknames
936        # user is in "unverified" state
937        user = request.root.get_user_by_email('jack.black@example.net')
938        assert not user.verified
939        assert isinstance(user.verification_token, UUID)
940        # also, we sent an email to that user
941        sve.assert_called_once_with(request, user)
942
943    def test_signup_missing_required(self, signup_post_request):
944        request = signup_post_request
945        request.POST['email'] = ''
946        assert 'jack.black@example.net' not in request.root.emails
947        assert 'JackBlack' not in request.root.all_nicknames
948        response = user_views.signup(request.root, request)
949        assert isinstance(response['form'], OWFormRenderer)
950        error = u'Please enter an email address'
951        html_error = '<ul class="error">'
952        html_error += '<li>' + error + '</li>'
953        html_error += '</ul>'
954        errorlist = response['form'].errorlist().replace('\n', '')
955        assert errorlist == html_error
956        assert response['form'].errors_for('email') == [error]
957        assert 'jack.black@example.net' not in request.root.emails
958        assert 'JackBlack' not in request.root.all_nicknames
959
960    def test_signup_existing_nickname(self, signup_post_request, john):
961        request = signup_post_request
962        # assign john a nickname first
963        john.nickname = 'john'
964        # now set it for the POST request
965        request.POST['nickname'] = 'john'
966        # check jack is not there yet
967        assert 'jack.black@example.net' not in request.root.emails
968        assert 'JackBlack' not in request.root.all_nicknames
969        # now signup as jack, but trying to set the nickname 'john'
970        response = user_views.signup(request.root, request)
971        assert isinstance(response['form'], OWFormRenderer)
972        error = u'Another user is already using the nickname john'
973        html_error = '<ul class="error">'
974        html_error += '<li>' + error + '</li>'
975        html_error += '</ul>'
976        errorlist = response['form'].errorlist().replace('\n', '')
977        assert errorlist == html_error
978        assert response['form'].errors_for('nickname') == [error]
979        # all the errors, and jack is not there
980        assert 'jack.black@example.net' not in request.root.emails
981        assert 'JackBlack' not in request.root.all_nicknames
982
983    def test_signup_existing_email(self, signup_post_request):
984        request = signup_post_request
985        request.POST['email'] = 'john.doe@example.net'
986        assert 'jack.black@example.net' not in request.root.emails
987        assert 'JackBlack' not in request.root.all_nicknames
988        response = user_views.signup(request.root, request)
989        assert isinstance(response['form'], OWFormRenderer)
990        error = u'Another user is already registered with the email '
991        error += u'john.doe@example.net'
992        html_error = '<ul class="error">'
993        html_error += '<li>' + error + '</li>'
994        html_error += '</ul>'
995        errorlist = response['form'].errorlist().replace('\n', '')
996        assert errorlist == html_error
997        assert response['form'].errors_for('email') == [error]
998        assert 'jack.black@example.net' not in request.root.emails
999        assert 'JackBlack' not in request.root.all_nicknames
1000
1001    def test_week_stats_no_stats(self, dummy_request, john):
1002        response = user_views.week_stats(john, dummy_request)
1003        assert isinstance(response, Response)
1004        assert response.content_type == 'application/json'
1005        # the body is a valid json-encoded stream
1006        obj = json.loads(response.body)
1007        assert obj == [
1008            {'distance': 0, 'elevation': 0, 'name': 'Mon',
1009             'time': '00', 'workouts': 0},
1010            {'distance': 0, 'elevation': 0, 'name': 'Tue',
1011             'time': '00', 'workouts': 0},
1012            {'distance': 0, 'elevation': 0, 'name': 'Wed',
1013             'time': '00', 'workouts': 0},
1014            {'distance': 0, 'elevation': 0, 'name': 'Thu',
1015             'time': '00', 'workouts': 0},
1016            {'distance': 0, 'elevation': 0, 'name': 'Fri',
1017             'time': '00', 'workouts': 0},
1018            {'distance': 0, 'elevation': 0, 'name': 'Sat',
1019             'time': '00', 'workouts': 0},
1020            {'distance': 0, 'elevation': 0, 'name': 'Sun',
1021             'time': '00', 'workouts': 0}
1022        ]
1023
1024    def test_week_stats(self, dummy_request, john):
1025        workout = Workout(
1026            start=datetime.now(timezone.utc),
1027            duration=timedelta(minutes=60),
1028            distance=30,
1029            elevation=540
1030        )
1031        john.add_workout(workout)
1032        response = user_views.week_stats(john, dummy_request)
1033        assert isinstance(response, Response)
1034        assert response.content_type == 'application/json'
1035        # the body is a valid json-encoded stream
1036        obj = json.loads(response.body)
1037        assert len(obj) == 7
1038        for day in obj:
1039            if datetime.now(timezone.utc).strftime('%a') == day['name']:
1040                day['distance'] == 30
1041                day['elevation'] == 540
1042                day['time'] == '01'
1043                day['workouts'] == 1
1044            else:
1045                day['distance'] == 0
1046                day['elevation'] == 0
1047                day['time'] == '00'
1048                day['workouts'] == 0
1049
1050    def test_last_months_stats(self, dummy_request, john):
1051        request = dummy_request
1052        user = john
1053        response = user_views.last_months_stats(user, request)
1054        assert isinstance(response, Response)
1055        assert response.content_type == 'application/json'
1056        # the body is a valid json-encoded stream
1057        obj = json.loads(response.body)
1058        assert len(obj) == 13
1059        for month in obj:
1060            assert len(month.keys()) == 7
1061            assert 'id' in month.keys()
1062            assert 'name' in month.keys()
1063            assert month['distance'] == 0
1064            assert month['elevation'] == 0
1065            assert month['time'] == '00'
1066            assert month['workouts'] == 0
1067            assert str(user.uid) in month['url']
1068            assert 'profile' in month['url']
1069            assert 'year' in month['url']
1070            assert 'month' in month['url']
1071
1072        # try now with a workout
1073        workout_start = datetime.now(timezone.utc)
1074        workout_start_id = (
1075            str(workout_start.year) + '-' + str(workout_start.month).zfill(2))
1076        workout = Workout(
1077            sport='cycling',
1078            start=workout_start,
1079            duration=timedelta(minutes=60),
1080            distance=30,
1081            uphill=540
1082        )
1083        user.add_workout(workout)
1084        response = user_views.last_months_stats(user, request)
1085        assert isinstance(response, Response)
1086        assert response.content_type == 'application/json'
1087        # the body is a valid json-encoded stream
1088        obj = json.loads(response.body)
1089        assert len(obj) == 13
1090        for month in obj:
1091            assert len(month.keys()) == 7
1092            assert 'id' in month.keys()
1093            assert 'name' in month.keys()
1094            assert str(user.uid) in month['url']
1095            assert 'profile' in month['url']
1096            assert 'year' in month['url']
1097            assert 'month' in month['url']
1098            if month['id'] == workout_start_id:
1099                assert month['distance'] == 30
1100                assert month['elevation'] == 540
1101                assert month['time'] == '01'
1102                assert month['workouts'] == 1
1103            else:
1104                assert month['distance'] == 0
1105                assert month['elevation'] == 0
1106                assert month['time'] == '00'
1107                assert month['workouts'] == 0
1108
1109    def test_last_weeks_stats(self, dummy_request, john):
1110        request = dummy_request
1111        user = john
1112        response = user_views.last_weeks_stats(user, request)
1113        assert isinstance(response, Response)
1114        assert response.content_type == 'application/json'
1115        # the body is a valid json-encoded stream
1116        obj = json.loads(response.body)
1117        for week in obj:
1118            assert len(week.keys()) == 8
1119            assert 'id' in week.keys()
1120            assert 'name' in week.keys()
1121            assert 'week' in week.keys()
1122            assert week['distance'] == 0
1123            assert week['elevation'] == 0
1124            assert week['time'] == '00'
1125            assert week['workouts'] == 0
1126            assert str(user.uid) in week['url']
1127            assert 'profile' in week['url']
1128            assert 'year' in week['url']
1129            assert 'month' in week['url']
1130            assert 'week' in week['url']
1131
1132        # try now with a workout
1133        workout_start = datetime.now(timezone.utc)
1134        workout_start_id = str(workout_start.year)
1135        workout_start_id += '-' + str(workout_start.month).zfill(2)
1136        workout_start_id += '-' + str(workout_start.isocalendar()[1])
1137        workout = Workout(
1138            sport='cycling',
1139            start=workout_start,
1140            duration=timedelta(minutes=60),
1141            distance=30,
1142            uphill=540
1143        )
1144        user.add_workout(workout)
1145        response = user_views.last_weeks_stats(user, request)
1146        assert isinstance(response, Response)
1147        assert response.content_type == 'application/json'
1148        # the body is a valid json-encoded stream
1149        obj = json.loads(response.body)
1150        for week in obj:
1151            assert len(week.keys()) == 8
1152            assert 'id' in week.keys()
1153            assert 'name' in week.keys()
1154            assert 'week' in week.keys()
1155            assert str(user.uid) in week['url']
1156            assert 'profile' in week['url']
1157            assert 'year' in week['url']
1158            assert 'month' in week['url']
1159            assert 'week' in week['url']
1160            if week['id'] == workout_start_id:
1161                assert week['distance'] == 30
1162                assert week['elevation'] == 540
1163                assert week['time'] == '01'
1164                assert week['workouts'] == 1
1165            else:
1166                assert week['distance'] == 0
1167                assert week['elevation'] == 0
1168                assert week['time'] == '00'
1169                assert week['workouts'] == 0
Note: See TracBrowser for help on using the repository browser.