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

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

(#7) Added method to gather monthly stats + view to export them

  • Property mode set to 100644
File size: 26.7 KB
Line 
1import os
2import json
3from datetime import datetime, timedelta, timezone
4from shutil import copyfileobj
5from unittest.mock import Mock, patch
6
7import pytest
8
9from ZODB.blob import Blob
10
11from pyramid.testing import DummyRequest
12from pyramid.httpexceptions import HTTPFound
13from pyramid.response import Response
14
15from webob.multidict import MultiDict
16
17from ow.models.root import OpenWorkouts
18from ow.models.user import User
19from ow.models.workout import Workout
20from ow.views.renderers import OWFormRenderer
21import ow.views.user as user_views
22
23
24class TestUserViews(object):
25
26    @pytest.fixture
27    def john(self):
28        user = User(firstname='John', lastname='Doe',
29                    email='john.doe@example.net')
30        user.password = 's3cr3t'
31        return user
32
33    @pytest.fixture
34    def root(self, john):
35        root = OpenWorkouts()
36        root.add_user(john)
37        workout = Workout(
38            start=datetime(2015, 6, 28, 12, 55, tzinfo=timezone.utc),
39            duration=timedelta(minutes=60),
40            distance=30
41        )
42        john.add_workout(workout)
43        return root
44
45    @pytest.fixture
46    def dummy_request(self, root):
47        request = DummyRequest()
48        request.root = root
49        return request
50
51    @pytest.fixture
52    def profile_post_request(self, root, john):
53        """
54        This is a valid POST request to update an user profile.
55        Form will validate, but nothing will be really updated/changed.
56        """
57        user = john
58        request = DummyRequest()
59        request.root = root
60        request.method = 'POST'
61        request.POST = MultiDict({
62            'submit': True,
63            'firstname': user.firstname,
64            'lastname': user.lastname,
65            'email': user.email,
66            'bio': user.bio,
67            'weight': user.weight,
68            'height': user.height,
69            'gender': user.gender,
70            'birth_date': user.birth_date,
71            'picture': user.picture,
72            })
73        return request
74
75    @pytest.fixture
76    def passwd_post_request(self, root):
77        """
78        This is a valid POST request to change the user password, but
79        the form will not validate (empty fields)
80        """
81        request = DummyRequest()
82        request.root = root
83        request.method = 'POST'
84        request.POST = MultiDict({
85            'submit': True,
86            'old_password': '',
87            'password': '',
88            'password_confirm': ''
89            })
90        return request
91
92    @pytest.fixture
93    def signup_post_request(self, root):
94        """
95        This is a valid POST request to signup a new user.
96        """
97        request = DummyRequest()
98        request.root = root
99        request.method = 'POST'
100        request.POST = MultiDict({
101            'submit': True,
102            'nickname': 'JackBlack',
103            'email': 'jack.black@example.net',
104            'firstname': 'Jack',
105            'lastname': 'Black',
106            'password': 'j4ck s3cr3t',
107            'password_confirm': 'j4ck s3cr3t'
108            })
109        return request
110
111    def test_dashboard_redirect_unauthenticated(self, root):
112        """
113        Anoymous access to the root object, send the user to the login page.
114
115        Instead of reusing the DummyRequest from the request fixture, we do
116        Mock fully the request here, because we need to use
117        authenticated_userid, which cannot be easily set in the DummyRequest
118        """
119        request = DummyRequest()
120        request.root = root
121        response = user_views.dashboard_redirect(root, request)
122        assert isinstance(response, HTTPFound)
123        assert response.location == request.resource_url(root, 'login')
124
125    def test_dashboard_redirect_authenticated(self, root, john):
126        """
127        Authenticated user accesing the root object, send the user to her
128        dashboard
129
130        Instead of reusing the DummyRequest from the request fixture, we do
131        Mock fully the request here, because we need to use
132        authenticated_userid, which cannot be easily set in the DummyRequest
133        """
134        alt_request = DummyRequest()
135        request = Mock()
136        request.root = root
137        request.authenticated_userid = str(john.uid)
138        request.resource_url = alt_request.resource_url
139        response = user_views.dashboard_redirect(root, request)
140        assert isinstance(response, HTTPFound)
141        assert response.location == request.resource_url(john)
142        # if authenticated_userid is the id of an user that does not exist
143        # anymore, we send the user to the logout page
144        request.authenticated_userid = 'faked-uid'
145        response = user_views.dashboard_redirect(root, request)
146        assert isinstance(response, HTTPFound)
147        assert response.location == request.resource_url(root, 'logout')
148
149    def test_dashboard(self, dummy_request, john):
150        """
151        Renders the user dashboard
152        """
153        request = dummy_request
154        response = user_views.dashboard(john, request)
155        assert len(response) == 6
156        assert 'month_name' in response.keys()
157        assert response['current_year'] == datetime.now().year
158        assert response['current_day_name'] == datetime.now().strftime('%a')
159        # this user has a single workout, in 2015
160        assert response['viewing_year'] == 2015
161        assert response['viewing_month'] == 6
162        assert response['workouts'] == [w for w in john.workouts()]
163
164    def test_dashboard_year(self, dummy_request, john):
165        """
166        Renders the user dashboard for a chosen year.
167        """
168        request = dummy_request
169        # first test the year for which we know there is a workout
170        request.GET['year'] = 2015
171        response = user_views.dashboard(john, request)
172        assert len(response) == 6
173        assert 'month_name' in response.keys()
174        assert response['current_year'] == datetime.now().year
175        assert response['current_day_name'] == datetime.now().strftime('%a')
176        # this user has a single workout, in 2015
177        assert response['viewing_year'] == 2015
178        assert response['viewing_month'] == 6
179        assert response['workouts'] == [w for w in john.workouts()]
180        # now, a year we know there is no workout info
181        request.GET['year'] = 2000
182        response = user_views.dashboard(john, request)
183        assert len(response) == 6
184        assert 'month_name' in response.keys()
185        assert response['current_year'] == datetime.now().year
186        assert response['current_day_name'] == datetime.now().strftime('%a')
187        # this user has a single workout, in 2015
188        assert response['viewing_year'] == 2000
189        # we have no data for that year and we didn't ask for a certain month,
190        # so the passing value for that is None
191        assert response['viewing_month'] is None
192        assert response['workouts'] == []
193
194    def test_dashboard_year_month(self, dummy_request, john):
195        """
196        Renders the user dashboard for a chosen year and month.
197        """
198        request = dummy_request
199        # first test the year/month for which we know there is a workout
200        request.GET['year'] = 2015
201        request.GET['month'] = 6
202        response = user_views.dashboard(john, request)
203        assert len(response) == 6
204        assert 'month_name' in response.keys()
205        assert response['current_year'] == datetime.now().year
206        assert response['current_day_name'] == datetime.now().strftime('%a')
207        # this user has a single workout, in 2015
208        assert response['viewing_year'] == 2015
209        assert response['viewing_month'] == 6
210        assert response['workouts'] == [w for w in john.workouts()]
211        # now, change month to one without values
212        request.GET['month'] = 2
213        response = user_views.dashboard(john, request)
214        assert len(response) == 6
215        assert 'month_name' in response.keys()
216        assert response['current_year'] == datetime.now().year
217        assert response['current_day_name'] == datetime.now().strftime('%a')
218        # this user has a single workout, in 2015
219        assert response['viewing_year'] == 2015
220        assert response['viewing_month'] == 2
221        assert response['workouts'] == []
222        # now the month with data, but in a different year
223        request.GET['year'] = 2010
224        request.GET['month'] = 6
225        response = user_views.dashboard(john, request)
226        assert len(response) == 6
227        assert 'month_name' in response.keys()
228        assert response['current_year'] == datetime.now().year
229        assert response['current_day_name'] == datetime.now().strftime('%a')
230        # this user has a single workout, in 2015
231        assert response['viewing_year'] == 2010
232        assert response['viewing_month'] == 6
233        assert response['workouts'] == []
234
235    def test_dashboard_month(self, dummy_request, john):
236        """
237        Passing a month without a year when rendering the dashboard. The last
238        year for which workout data is available is assumed
239        """
240        request = dummy_request
241        # Set a month without workout data
242        request.GET['month'] = 5
243        response = user_views.dashboard(john, request)
244        assert len(response) == 6
245        assert 'month_name' in response.keys()
246        assert response['current_year'] == datetime.now().year
247        assert response['current_day_name'] == datetime.now().strftime('%a')
248        # this user has a single workout, in 2015
249        assert response['viewing_year'] == 2015
250        assert response['viewing_month'] == 5
251        assert response['workouts'] == []
252        # now a month with data
253        request.GET['month'] = 6
254        response = user_views.dashboard(john, request)
255        assert len(response) == 6
256        assert 'month_name' in response.keys()
257        assert response['current_year'] == datetime.now().year
258        assert response['current_day_name'] == datetime.now().strftime('%a')
259        # this user has a single workout, in 2015
260        assert response['viewing_year'] == 2015
261        assert response['viewing_month'] == 6
262        assert response['workouts'] == [w for w in john.workouts()]
263
264    def test_profile(self, dummy_request, john):
265        """
266        Renders the user profile page
267        """
268        request = dummy_request
269        response = user_views.profile(john, request)
270        assert len(response.keys()) == 1
271        current_month = datetime.now(timezone.utc).strftime('%Y-%m')
272        assert response['current_month'] == current_month
273
274    def test_login_get(self, dummy_request):
275        """
276        GET request to access the login page
277        """
278        request = dummy_request
279        response = user_views.login(request.root, request)
280        assert response['message'] == ''
281        assert response['email'] == ''
282        assert response['password'] == ''
283        assert response['redirect_url'] == request.resource_url(request.root)
284
285    def test_login_get_return_to(self, dummy_request, john):
286        """
287        GET request to access the login page, if there is a page set to where
288        the user should be sent to, the response "redirect_url" key will have
289        such url
290        """
291        request = dummy_request
292        workout = john.workouts()[0]
293        workout_url = request.resource_url(workout)
294        request.params['return_to'] = workout_url
295        response = user_views.login(request.root, request)
296        assert response['redirect_url'] == workout_url
297
298    def test_login_post_wrong_email(self, dummy_request):
299        request = dummy_request
300        request.method = 'POST'
301        request.POST['submit'] = True
302        request.POST['email'] = 'jack@example.net'
303        response = user_views.login(request.root, request)
304        assert response['message'] == u'Wrong email address'
305
306    def test_login_post_wrong_password(self, dummy_request):
307        request = dummy_request
308        request.method = 'POST'
309        request.POST['submit'] = True
310        request.POST['email'] = 'john.doe@example.net'
311        request.POST['password'] = 'badpassword'
312        response = user_views.login(request.root, request)
313        assert response['message'] == u'Wrong password'
314
315    @patch('ow.views.user.remember')
316    def test_login_post_ok(self, rem, dummy_request, john):
317        request = dummy_request
318        request.method = 'POST'
319        request.POST['submit'] = True
320        request.POST['email'] = 'john.doe@example.net'
321        request.POST['password'] = 's3cr3t'
322        response = user_views.login(request.root, request)
323        assert isinstance(response, HTTPFound)
324        assert rem.called
325        assert response.location == request.resource_url(john)
326
327    @patch('ow.views.user.forget')
328    def test_logout(self, forg, dummy_request):
329        request = dummy_request
330        response = user_views.logout(request.root, request)
331        assert isinstance(response, HTTPFound)
332        assert forg.called
333        assert response.location == request.resource_url(request.root)
334
335    extensions = ('png', 'jpg', 'jpeg', 'gif')
336
337    @pytest.mark.parametrize('extension', extensions)
338    def test_profile_picture(self, extension, dummy_request, john):
339        """
340        GET request to get the profile picture of an user.
341        """
342        request = dummy_request
343        # Get the user
344        user = john
345        # Get the path to the image, then open it and copy it to a new Blob
346        # object
347        path = 'fixtures/image.' + extension
348        image_path = os.path.join(
349            os.path.dirname(os.path.dirname(__file__)), path)
350        blob = Blob()
351        with open(image_path, 'rb') as infile, blob.open('w') as out:
352            infile.seek(0)
353            copyfileobj(infile, out)
354
355        # Set the blob with the picture
356        user.picture = blob
357
358        # Call the profile_picture view
359        response = user_views.profile_picture(user, request)
360        assert isinstance(response, Response)
361        assert response.status_int == 200
362        assert response.content_type == 'image'
363
364    def test_edit_profile_get(self, dummy_request, john):
365        """
366        GET request to the edit profile page, returns the form ready to
367        be rendered
368        """
369        request = dummy_request
370        user = john
371        response = user_views.edit_profile(user, request)
372        assert isinstance(response['form'], OWFormRenderer)
373        # no errors in the form (first load)
374        assert response['form'].errorlist() == ''
375        # the form carries along the proper data keys, taken from the
376        # loaded user profile
377        data = ['firstname', 'lastname', 'email', 'nickname', 'bio',
378                'birth_date', 'height', 'weight', 'gender', 'timezone']
379        assert list(response['form'].data.keys()) == data
380        # and check the email to see data is properly loaded
381        assert response['form'].data['email'] == 'john.doe@example.net'
382
383    def test_edit_profile_post_ok(self, profile_post_request, john):
384        request = profile_post_request
385        user = john
386        # Update the bio field
387        bio = 'Some text about this user'
388        request.POST['bio'] = bio
389        response = user_views.edit_profile(user, request)
390        assert isinstance(response, HTTPFound)
391        assert response.location == request.resource_url(user, 'profile')
392        assert user.bio == bio
393
394    def test_edit_profile_post_missing_required(
395            self, profile_post_request, john):
396        request = profile_post_request
397        request.POST['email'] = ''
398        user = john
399        response = user_views.edit_profile(user, request)
400        assert isinstance(response['form'], OWFormRenderer)
401        # error on the missing email field
402        error = u'Please enter an email address'
403        html_error = u'<ul class="error"><li>' + error + '</li></ul>'
404        assert response['form'].errorlist() == html_error
405        assert response['form'].errors_for('email') == [error]
406
407    def test_edit_profile_post_ok_picture_empty_bytes(
408            self, profile_post_request, john):
409        """
410        POST request with an empty picture, the content of
411        request['POST'].picture is a empty bytes string (b'') which triggers
412        a bug in formencode, we put a fix in place, test that
413        (more in ow.user.views.edit_profile)
414        """
415        # for the purposes of this test, we can mock the picture
416        picture = Mock()
417        john.picture = picture
418        request = profile_post_request
419        user = john
420        # Mimic what happens when a picture is not provided by the user
421        request.POST['picture'] = b''
422        response = user_views.edit_profile(user, request)
423        assert isinstance(response, HTTPFound)
424        assert response.location == request.resource_url(user, 'profile')
425        assert user.picture == picture
426
427    def test_edit_profile_post_ok_missing_picture(
428            self, profile_post_request, john):
429        """
430        POST request without picture
431        """
432        # for the purposes of this test, we can mock the picture
433        picture = Mock()
434        john.picture = picture
435        request = profile_post_request
436        user = john
437        # No pic is provided in the request POST values
438        del request.POST['picture']
439        response = user_views.edit_profile(user, request)
440        assert isinstance(response, HTTPFound)
441        assert response.location == request.resource_url(user, 'profile')
442        assert user.picture == picture
443
444    def test_edit_profile_post_ok_nickname(self, profile_post_request, john):
445        """
446        User with a nickname set saves profile without changing the profile,
447        we have to be sure there are no "nickname already in use" errors
448        """
449        request = profile_post_request
450        user = john
451        user.nickname = 'mr_jones'
452        # add the nickname, the default post request has not a nickname set
453        request.POST['nickname'] = 'mr_jones'
454        response = user_views.edit_profile(user, request)
455        assert isinstance(response, HTTPFound)
456        assert response.location == request.resource_url(user, 'profile')
457
458    def test_change_password_get(self, dummy_request, john):
459        request = dummy_request
460        user = john
461        response = user_views.change_password(user, request)
462        assert isinstance(response['form'], OWFormRenderer)
463        # no errors in the form (first load)
464        assert response['form'].errorlist() == ''
465
466    def test_change_password_post_ok(self, passwd_post_request, john):
467        request = passwd_post_request
468        user = john
469        request.POST['old_password'] = 's3cr3t'
470        request.POST['password'] = 'h1dd3n s3cr3t'
471        request.POST['password_confirm'] = 'h1dd3n s3cr3t'
472        response = user_views.change_password(user, request)
473        assert isinstance(response, HTTPFound)
474        assert response.location == request.resource_url(user, 'profile')
475        # password was changed
476        assert not user.check_password('s3cr3t')
477        assert user.check_password('h1dd3n s3cr3t')
478
479    def test_change_password_post_no_values(self, passwd_post_request, john):
480        request = passwd_post_request
481        user = john
482        response = user_views.change_password(user, request)
483        assert isinstance(response['form'], OWFormRenderer)
484        error = u'Please enter a value'
485        html_error = u'<ul class="error">'
486        html_error += ('<li>' + error + '</li>') * 3  # 3 fields
487        html_error += '</ul>'
488        errorlist = response['form'].errorlist().replace('\n', '')
489        assert errorlist == html_error
490        assert response['form'].errors_for('old_password') == [error]
491        assert response['form'].errors_for('password') == [error]
492        assert response['form'].errors_for('password_confirm') == [error]
493        # password was not changed
494        assert user.check_password('s3cr3t')
495
496    def test_change_password_post_bad_old_password(
497            self, passwd_post_request, john):
498        request = passwd_post_request
499        user = john
500        request.POST['old_password'] = 'FAIL PASSWORD'
501        request.POST['password'] = 'h1dd3n s3cr3t'
502        request.POST['password_confirm'] = 'h1dd3n s3cr3t'
503        response = user_views.change_password(user, request)
504        assert isinstance(response['form'], OWFormRenderer)
505        error = u'The given password does not match the existing one '
506        html_error = u'<ul class="error"><li>' + error + '</li></ul>'
507        assert response['form'].errorlist() == html_error
508        assert response['form'].errors_for('old_password') == [error]
509        # password was not changed
510        assert user.check_password('s3cr3t')
511        assert not user.check_password('h1dd3n s3cr3t')
512
513    def test_change_password_post_password_mismatch(
514            self, passwd_post_request, john):
515        request = passwd_post_request
516        user = john
517        request.POST['old_password'] = 's3cr3t'
518        request.POST['password'] = 'h1dd3n s3cr3ts'
519        request.POST['password_confirm'] = 'h1dd3n s3cr3t'
520        response = user_views.change_password(user, request)
521        assert isinstance(response['form'], OWFormRenderer)
522        error = u'Fields do not match'
523        html_error = u'<ul class="error"><li>' + error + '</li></ul>'
524        assert response['form'].errorlist() == html_error
525        assert response['form'].errors_for('password_confirm') == [error]
526        # password was not changed
527        assert user.check_password('s3cr3t')
528        assert not user.check_password('h1dd3n s3cr3t')
529
530    def test_signup_get(self, dummy_request):
531        request = dummy_request
532        response = user_views.signup(request.root, request)
533        assert isinstance(response['form'], OWFormRenderer)
534        # no errors in the form (first load)
535        assert response['form'].errorlist() == ''
536
537    def test_signup_post_ok(self, signup_post_request):
538        request = signup_post_request
539        assert 'jack.black@example.net' not in request.root.emails
540        assert 'JackBlack' not in request.root.all_nicknames
541        response = user_views.signup(request.root, request)
542        assert isinstance(response, HTTPFound)
543        assert response.location == request.resource_url(request.root)
544        assert 'jack.black@example.net' in request.root.emails
545        assert 'JackBlack' in request.root.all_nicknames
546
547    def test_signup_missing_required(self, signup_post_request):
548        request = signup_post_request
549        request.POST['email'] = ''
550        assert 'jack.black@example.net' not in request.root.emails
551        assert 'JackBlack' not in request.root.all_nicknames
552        response = user_views.signup(request.root, request)
553        assert isinstance(response['form'], OWFormRenderer)
554        error = u'Please enter an email address'
555        html_error = '<ul class="error">'
556        html_error += '<li>' + error + '</li>'
557        html_error += '</ul>'
558        errorlist = response['form'].errorlist().replace('\n', '')
559        assert errorlist == html_error
560        assert response['form'].errors_for('email') == [error]
561        assert 'jack.black@example.net' not in request.root.emails
562        assert 'JackBlack' not in request.root.all_nicknames
563
564    def test_signup_existing_nickname(self, signup_post_request, john):
565        request = signup_post_request
566        # assign john a nickname first
567        john.nickname = 'john'
568        # now set it for the POST request
569        request.POST['nickname'] = 'john'
570        # check jack is not there yet
571        assert 'jack.black@example.net' not in request.root.emails
572        assert 'JackBlack' not in request.root.all_nicknames
573        # now signup as jack, but trying to set the nickname 'john'
574        response = user_views.signup(request.root, request)
575        assert isinstance(response['form'], OWFormRenderer)
576        error = u'Another user is already using the nickname john'
577        html_error = '<ul class="error">'
578        html_error += '<li>' + error + '</li>'
579        html_error += '</ul>'
580        errorlist = response['form'].errorlist().replace('\n', '')
581        assert errorlist == html_error
582        assert response['form'].errors_for('nickname') == [error]
583        # all the errors, and jack is not there
584        assert 'jack.black@example.net' not in request.root.emails
585        assert 'JackBlack' not in request.root.all_nicknames
586
587    def test_signup_existing_email(self, signup_post_request):
588        request = signup_post_request
589        request.POST['email'] = 'john.doe@example.net'
590        assert 'jack.black@example.net' not in request.root.emails
591        assert 'JackBlack' not in request.root.all_nicknames
592        response = user_views.signup(request.root, request)
593        assert isinstance(response['form'], OWFormRenderer)
594        error = u'Another user is already registered with the email '
595        error += u'john.doe@example.net'
596        html_error = '<ul class="error">'
597        html_error += '<li>' + error + '</li>'
598        html_error += '</ul>'
599        errorlist = response['form'].errorlist().replace('\n', '')
600        assert errorlist == html_error
601        assert response['form'].errors_for('email') == [error]
602        assert 'jack.black@example.net' not in request.root.emails
603        assert 'JackBlack' not in request.root.all_nicknames
604
605    def test_week_stats_no_stats(self, dummy_request, john):
606        response = user_views.week_stats(john, dummy_request)
607        assert isinstance(response, Response)
608        assert response.content_type == 'application/json'
609        # the body is a valid json-encoded stream
610        obj = json.loads(response.body)
611        assert obj == [
612            {'distance': 0, 'elevation': 0, 'name': 'Mon',
613             'time': '00', 'workouts': 0},
614            {'distance': 0, 'elevation': 0, 'name': 'Tue',
615             'time': '00', 'workouts': 0},
616            {'distance': 0, 'elevation': 0, 'name': 'Wed',
617             'time': '00', 'workouts': 0},
618            {'distance': 0, 'elevation': 0, 'name': 'Thu',
619             'time': '00', 'workouts': 0},
620            {'distance': 0, 'elevation': 0, 'name': 'Fri',
621             'time': '00', 'workouts': 0},
622            {'distance': 0, 'elevation': 0, 'name': 'Sat',
623             'time': '00', 'workouts': 0},
624            {'distance': 0, 'elevation': 0, 'name': 'Sun',
625             'time': '00', 'workouts': 0}
626        ]
627
628    def test_week_stats(self, dummy_request, john):
629        workout = Workout(
630            start=datetime.now(timezone.utc),
631            duration=timedelta(minutes=60),
632            distance=30,
633            elevation=540
634        )
635        john.add_workout(workout)
636        response = user_views.week_stats(john, dummy_request)
637        assert isinstance(response, Response)
638        assert response.content_type == 'application/json'
639        # the body is a valid json-encoded stream
640        obj = json.loads(response.body)
641        assert len(obj) == 7
642        for day in obj:
643            if datetime.now(timezone.utc).strftime('%a') == day['name']:
644                day['distance'] == 30
645                day['elevation'] == 540
646                day['time'] == '01'
647                day['workouts'] == 1
648            else:
649                day['distance'] == 0
650                day['elevation'] == 0
651                day['time'] == '00'
652                day['workouts'] == 0
Note: See TracBrowser for help on using the repository browser.