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

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

(#7) Added week_stats view, that returns a json-encoded stream of data

containing information about the current week status for the given user.

  • Property mode set to 100644
File size: 25.5 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) == 4
156        assert 'month_name' in response.keys()
157        # this user has a single workout, in 2015
158        assert response['viewing_year'] == 2015
159        assert response['viewing_month'] == 6
160        assert response['workouts'] == [w for w in john.workouts()]
161
162    def test_dashboard_year(self, dummy_request, john):
163        """
164        Renders the user dashboard for a chosen year.
165        """
166        request = dummy_request
167        # first test the year for which we know there is a workout
168        request.GET['year'] = 2015
169        response = user_views.dashboard(john, request)
170        assert len(response) == 4
171        assert 'month_name' in response.keys()
172        # this user has a single workout, in 2015
173        assert response['viewing_year'] == 2015
174        assert response['viewing_month'] == 6
175        assert response['workouts'] == [w for w in john.workouts()]
176        # now, a year we know there is no workout info
177        request.GET['year'] = 2000
178        response = user_views.dashboard(john, request)
179        assert len(response) == 4
180        assert 'month_name' in response.keys()
181        # this user has a single workout, in 2015
182        assert response['viewing_year'] == 2000
183        # we have no data for that year and we didn't ask for a certain month,
184        # so the passing value for that is None
185        assert response['viewing_month'] is None
186        assert response['workouts'] == []
187
188    def test_dashboard_year_month(self, dummy_request, john):
189        """
190        Renders the user dashboard for a chosen year and month.
191        """
192        request = dummy_request
193        # first test the year/month for which we know there is a workout
194        request.GET['year'] = 2015
195        request.GET['month'] = 6
196        response = user_views.dashboard(john, request)
197        assert len(response) == 4
198        assert 'month_name' in response.keys()
199        # this user has a single workout, in 2015
200        assert response['viewing_year'] == 2015
201        assert response['viewing_month'] == 6
202        assert response['workouts'] == [w for w in john.workouts()]
203        # now, change month to one without values
204        request.GET['month'] = 2
205        response = user_views.dashboard(john, request)
206        assert len(response) == 4
207        assert 'month_name' in response.keys()
208        # this user has a single workout, in 2015
209        assert response['viewing_year'] == 2015
210        assert response['viewing_month'] == 2
211        assert response['workouts'] == []
212        # now the month with data, but in a different year
213        request.GET['year'] = 2010
214        request.GET['month'] = 6
215        response = user_views.dashboard(john, request)
216        assert len(response) == 4
217        assert 'month_name' in response.keys()
218        # this user has a single workout, in 2015
219        assert response['viewing_year'] == 2010
220        assert response['viewing_month'] == 6
221        assert response['workouts'] == []
222
223    def test_dashboard_month(self, dummy_request, john):
224        """
225        Passing a month without a year when rendering the dashboard. The last
226        year for which workout data is available is assumed
227        """
228        request = dummy_request
229        # Set a month without workout data
230        request.GET['month'] = 5
231        response = user_views.dashboard(john, request)
232        assert len(response) == 4
233        assert 'month_name' in response.keys()
234        # this user has a single workout, in 2015
235        assert response['viewing_year'] == 2015
236        assert response['viewing_month'] == 5
237        assert response['workouts'] == []
238        # now a month with data
239        request.GET['month'] = 6
240        response = user_views.dashboard(john, request)
241        assert len(response) == 4
242        assert 'month_name' in response.keys()
243        # this user has a single workout, in 2015
244        assert response['viewing_year'] == 2015
245        assert response['viewing_month'] == 6
246        assert response['workouts'] == [w for w in john.workouts()]
247
248    def test_profile(self, dummy_request, john):
249        """
250        Renders the user profile page
251        """
252        request = dummy_request
253        response = user_views.profile(john, request)
254        assert response == {}
255
256    def test_login_get(self, dummy_request):
257        """
258        GET request to access the login page
259        """
260        request = dummy_request
261        response = user_views.login(request.root, request)
262        assert response['message'] == ''
263        assert response['email'] == ''
264        assert response['password'] == ''
265        assert response['redirect_url'] == request.resource_url(request.root)
266
267    def test_login_get_return_to(self, dummy_request, john):
268        """
269        GET request to access the login page, if there is a page set to where
270        the user should be sent to, the response "redirect_url" key will have
271        such url
272        """
273        request = dummy_request
274        workout = john.workouts()[0]
275        workout_url = request.resource_url(workout)
276        request.params['return_to'] = workout_url
277        response = user_views.login(request.root, request)
278        assert response['redirect_url'] == workout_url
279
280    def test_login_post_wrong_email(self, dummy_request):
281        request = dummy_request
282        request.method = 'POST'
283        request.POST['submit'] = True
284        request.POST['email'] = 'jack@example.net'
285        response = user_views.login(request.root, request)
286        assert response['message'] == u'Wrong email address'
287
288    def test_login_post_wrong_password(self, dummy_request):
289        request = dummy_request
290        request.method = 'POST'
291        request.POST['submit'] = True
292        request.POST['email'] = 'john.doe@example.net'
293        request.POST['password'] = 'badpassword'
294        response = user_views.login(request.root, request)
295        assert response['message'] == u'Wrong password'
296
297    @patch('ow.views.user.remember')
298    def test_login_post_ok(self, rem, dummy_request, john):
299        request = dummy_request
300        request.method = 'POST'
301        request.POST['submit'] = True
302        request.POST['email'] = 'john.doe@example.net'
303        request.POST['password'] = 's3cr3t'
304        response = user_views.login(request.root, request)
305        assert isinstance(response, HTTPFound)
306        assert rem.called
307        assert response.location == request.resource_url(john)
308
309    @patch('ow.views.user.forget')
310    def test_logout(self, forg, dummy_request):
311        request = dummy_request
312        response = user_views.logout(request.root, request)
313        assert isinstance(response, HTTPFound)
314        assert forg.called
315        assert response.location == request.resource_url(request.root)
316
317    extensions = ('png', 'jpg', 'jpeg', 'gif')
318
319    @pytest.mark.parametrize('extension', extensions)
320    def test_profile_picture(self, extension, dummy_request, john):
321        """
322        GET request to get the profile picture of an user.
323        """
324        request = dummy_request
325        # Get the user
326        user = john
327        # Get the path to the image, then open it and copy it to a new Blob
328        # object
329        path = 'fixtures/image.' + extension
330        image_path = os.path.join(
331            os.path.dirname(os.path.dirname(__file__)), path)
332        blob = Blob()
333        with open(image_path, 'rb') as infile, blob.open('w') as out:
334            infile.seek(0)
335            copyfileobj(infile, out)
336
337        # Set the blob with the picture
338        user.picture = blob
339
340        # Call the profile_picture view
341        response = user_views.profile_picture(user, request)
342        assert isinstance(response, Response)
343        assert response.status_int == 200
344        assert response.content_type == 'image'
345
346    def test_edit_profile_get(self, dummy_request, john):
347        """
348        GET request to the edit profile page, returns the form ready to
349        be rendered
350        """
351        request = dummy_request
352        user = john
353        response = user_views.edit_profile(user, request)
354        assert isinstance(response['form'], OWFormRenderer)
355        # no errors in the form (first load)
356        assert response['form'].errorlist() == ''
357        # the form carries along the proper data keys, taken from the
358        # loaded user profile
359        data = ['firstname', 'lastname', 'email', 'nickname', 'bio',
360                'birth_date', 'height', 'weight', 'gender', 'timezone']
361        assert list(response['form'].data.keys()) == data
362        # and check the email to see data is properly loaded
363        assert response['form'].data['email'] == 'john.doe@example.net'
364
365    def test_edit_profile_post_ok(self, profile_post_request, john):
366        request = profile_post_request
367        user = john
368        # Update the bio field
369        bio = 'Some text about this user'
370        request.POST['bio'] = bio
371        response = user_views.edit_profile(user, request)
372        assert isinstance(response, HTTPFound)
373        assert response.location == request.resource_url(user, 'profile')
374        assert user.bio == bio
375
376    def test_edit_profile_post_missing_required(
377            self, profile_post_request, john):
378        request = profile_post_request
379        request.POST['email'] = ''
380        user = john
381        response = user_views.edit_profile(user, request)
382        assert isinstance(response['form'], OWFormRenderer)
383        # error on the missing email field
384        error = u'Please enter an email address'
385        html_error = u'<ul class="error"><li>' + error + '</li></ul>'
386        assert response['form'].errorlist() == html_error
387        assert response['form'].errors_for('email') == [error]
388
389    def test_edit_profile_post_ok_picture_empty_bytes(
390            self, profile_post_request, john):
391        """
392        POST request with an empty picture, the content of
393        request['POST'].picture is a empty bytes string (b'') which triggers
394        a bug in formencode, we put a fix in place, test that
395        (more in ow.user.views.edit_profile)
396        """
397        # for the purposes of this test, we can mock the picture
398        picture = Mock()
399        john.picture = picture
400        request = profile_post_request
401        user = john
402        # Mimic what happens when a picture is not provided by the user
403        request.POST['picture'] = b''
404        response = user_views.edit_profile(user, request)
405        assert isinstance(response, HTTPFound)
406        assert response.location == request.resource_url(user, 'profile')
407        assert user.picture == picture
408
409    def test_edit_profile_post_ok_missing_picture(
410            self, profile_post_request, john):
411        """
412        POST request without picture
413        """
414        # for the purposes of this test, we can mock the picture
415        picture = Mock()
416        john.picture = picture
417        request = profile_post_request
418        user = john
419        # No pic is provided in the request POST values
420        del request.POST['picture']
421        response = user_views.edit_profile(user, request)
422        assert isinstance(response, HTTPFound)
423        assert response.location == request.resource_url(user, 'profile')
424        assert user.picture == picture
425
426    def test_edit_profile_post_ok_nickname(self, profile_post_request, john):
427        """
428        User with a nickname set saves profile without changing the profile,
429        we have to be sure there are no "nickname already in use" errors
430        """
431        request = profile_post_request
432        user = john
433        user.nickname = 'mr_jones'
434        # add the nickname, the default post request has not a nickname set
435        request.POST['nickname'] = 'mr_jones'
436        response = user_views.edit_profile(user, request)
437        assert isinstance(response, HTTPFound)
438        assert response.location == request.resource_url(user, 'profile')
439
440    def test_change_password_get(self, dummy_request, john):
441        request = dummy_request
442        user = john
443        response = user_views.change_password(user, request)
444        assert isinstance(response['form'], OWFormRenderer)
445        # no errors in the form (first load)
446        assert response['form'].errorlist() == ''
447
448    def test_change_password_post_ok(self, passwd_post_request, john):
449        request = passwd_post_request
450        user = john
451        request.POST['old_password'] = 's3cr3t'
452        request.POST['password'] = 'h1dd3n s3cr3t'
453        request.POST['password_confirm'] = 'h1dd3n s3cr3t'
454        response = user_views.change_password(user, request)
455        assert isinstance(response, HTTPFound)
456        assert response.location == request.resource_url(user, 'profile')
457        # password was changed
458        assert not user.check_password('s3cr3t')
459        assert user.check_password('h1dd3n s3cr3t')
460
461    def test_change_password_post_no_values(self, passwd_post_request, john):
462        request = passwd_post_request
463        user = john
464        response = user_views.change_password(user, request)
465        assert isinstance(response['form'], OWFormRenderer)
466        error = u'Please enter a value'
467        html_error = u'<ul class="error">'
468        html_error += ('<li>' + error + '</li>') * 3  # 3 fields
469        html_error += '</ul>'
470        errorlist = response['form'].errorlist().replace('\n', '')
471        assert errorlist == html_error
472        assert response['form'].errors_for('old_password') == [error]
473        assert response['form'].errors_for('password') == [error]
474        assert response['form'].errors_for('password_confirm') == [error]
475        # password was not changed
476        assert user.check_password('s3cr3t')
477
478    def test_change_password_post_bad_old_password(
479            self, passwd_post_request, john):
480        request = passwd_post_request
481        user = john
482        request.POST['old_password'] = 'FAIL PASSWORD'
483        request.POST['password'] = 'h1dd3n s3cr3t'
484        request.POST['password_confirm'] = 'h1dd3n s3cr3t'
485        response = user_views.change_password(user, request)
486        assert isinstance(response['form'], OWFormRenderer)
487        error = u'The given password does not match the existing one '
488        html_error = u'<ul class="error"><li>' + error + '</li></ul>'
489        assert response['form'].errorlist() == html_error
490        assert response['form'].errors_for('old_password') == [error]
491        # password was not changed
492        assert user.check_password('s3cr3t')
493        assert not user.check_password('h1dd3n s3cr3t')
494
495    def test_change_password_post_password_mismatch(
496            self, passwd_post_request, john):
497        request = passwd_post_request
498        user = john
499        request.POST['old_password'] = 's3cr3t'
500        request.POST['password'] = 'h1dd3n s3cr3ts'
501        request.POST['password_confirm'] = 'h1dd3n s3cr3t'
502        response = user_views.change_password(user, request)
503        assert isinstance(response['form'], OWFormRenderer)
504        error = u'Fields do not match'
505        html_error = u'<ul class="error"><li>' + error + '</li></ul>'
506        assert response['form'].errorlist() == html_error
507        assert response['form'].errors_for('password_confirm') == [error]
508        # password was not changed
509        assert user.check_password('s3cr3t')
510        assert not user.check_password('h1dd3n s3cr3t')
511
512    def test_signup_get(self, dummy_request):
513        request = dummy_request
514        response = user_views.signup(request.root, request)
515        assert isinstance(response['form'], OWFormRenderer)
516        # no errors in the form (first load)
517        assert response['form'].errorlist() == ''
518
519    def test_signup_post_ok(self, signup_post_request):
520        request = signup_post_request
521        assert 'jack.black@example.net' not in request.root.emails
522        assert 'JackBlack' not in request.root.all_nicknames
523        response = user_views.signup(request.root, request)
524        assert isinstance(response, HTTPFound)
525        assert response.location == request.resource_url(request.root)
526        assert 'jack.black@example.net' in request.root.emails
527        assert 'JackBlack' in request.root.all_nicknames
528
529    def test_signup_missing_required(self, signup_post_request):
530        request = signup_post_request
531        request.POST['email'] = ''
532        assert 'jack.black@example.net' not in request.root.emails
533        assert 'JackBlack' not in request.root.all_nicknames
534        response = user_views.signup(request.root, request)
535        assert isinstance(response['form'], OWFormRenderer)
536        error = u'Please enter an email address'
537        html_error = '<ul class="error">'
538        html_error += '<li>' + error + '</li>'
539        html_error += '</ul>'
540        errorlist = response['form'].errorlist().replace('\n', '')
541        assert errorlist == html_error
542        assert response['form'].errors_for('email') == [error]
543        assert 'jack.black@example.net' not in request.root.emails
544        assert 'JackBlack' not in request.root.all_nicknames
545
546    def test_signup_existing_nickname(self, signup_post_request, john):
547        request = signup_post_request
548        # assign john a nickname first
549        john.nickname = 'john'
550        # now set it for the POST request
551        request.POST['nickname'] = 'john'
552        # check jack is not there yet
553        assert 'jack.black@example.net' not in request.root.emails
554        assert 'JackBlack' not in request.root.all_nicknames
555        # now signup as jack, but trying to set the nickname 'john'
556        response = user_views.signup(request.root, request)
557        assert isinstance(response['form'], OWFormRenderer)
558        error = u'Another user is already using the nickname john'
559        html_error = '<ul class="error">'
560        html_error += '<li>' + error + '</li>'
561        html_error += '</ul>'
562        errorlist = response['form'].errorlist().replace('\n', '')
563        assert errorlist == html_error
564        assert response['form'].errors_for('nickname') == [error]
565        # all the errors, and jack is not there
566        assert 'jack.black@example.net' not in request.root.emails
567        assert 'JackBlack' not in request.root.all_nicknames
568
569    def test_signup_existing_email(self, signup_post_request):
570        request = signup_post_request
571        request.POST['email'] = 'john.doe@example.net'
572        assert 'jack.black@example.net' not in request.root.emails
573        assert 'JackBlack' not in request.root.all_nicknames
574        response = user_views.signup(request.root, request)
575        assert isinstance(response['form'], OWFormRenderer)
576        error = u'Another user is already registered with the email '
577        error += u'john.doe@example.net'
578        html_error = '<ul class="error">'
579        html_error += '<li>' + error + '</li>'
580        html_error += '</ul>'
581        errorlist = response['form'].errorlist().replace('\n', '')
582        assert errorlist == html_error
583        assert response['form'].errors_for('email') == [error]
584        assert 'jack.black@example.net' not in request.root.emails
585        assert 'JackBlack' not in request.root.all_nicknames
586
587    def test_week_stats_no_stats(self, dummy_request, john):
588        response = user_views.week_stats(john, dummy_request)
589        assert isinstance(response, Response)
590        assert response.content_type == 'application/json'
591        # the body is a valid json-encoded stream
592        obj = json.loads(response.body)
593        assert obj == [
594            {'distance': 0, 'elevation': 0, 'name': 'Mon',
595             'time': '00', 'workouts': 0},
596            {'distance': 0, 'elevation': 0, 'name': 'Tue',
597             'time': '00', 'workouts': 0},
598            {'distance': 0, 'elevation': 0, 'name': 'Wed',
599             'time': '00', 'workouts': 0},
600            {'distance': 0, 'elevation': 0, 'name': 'Thu',
601             'time': '00', 'workouts': 0},
602            {'distance': 0, 'elevation': 0, 'name': 'Fri',
603             'time': '00', 'workouts': 0},
604            {'distance': 0, 'elevation': 0, 'name': 'Sat',
605             'time': '00', 'workouts': 0},
606            {'distance': 0, 'elevation': 0, 'name': 'Sun',
607             'time': '00', 'workouts': 0}
608        ]
609
610    def test_week_stats(self, dummy_request, john):
611        workout = Workout(
612            start=datetime.now(timezone.utc),
613            duration=timedelta(minutes=60),
614            distance=30,
615            elevation=540
616        )
617        john.add_workout(workout)
618        response = user_views.week_stats(john, dummy_request)
619        assert isinstance(response, Response)
620        assert response.content_type == 'application/json'
621        # the body is a valid json-encoded stream
622        obj = json.loads(response.body)
623        assert len(obj) == 7
624        for day in obj:
625            if datetime.now(timezone.utc).strftime('%a') == day['name']:
626                day['distance'] == 30
627                day['elevation'] == 540
628                day['time'] == '01'
629                day['workouts'] == 1
630            else:
631                day['distance'] == 0
632                day['elevation'] == 0
633                day['time'] == '00'
634                day['workouts'] == 0
Note: See TracBrowser for help on using the repository browser.