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

currentfeature/docs
Last change on this file since 24596da was 24596da, checked in by Borja Lopez <borja@…>, 5 years ago

Added missing tests, raised overall coverage

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