source: OpenWorkouts-current/ow/tests/views/test_workout.py @ cef474f

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

Added more missing tests, raising coverage

  • Property mode set to 100644
File size: 20.8 KB
Line 
1import os
2import json
3from io import BytesIO
4from datetime import datetime, timedelta, timezone
5from cgi import FieldStorage
6from unittest.mock import Mock, patch, PropertyMock
7
8import pytest
9
10from pyramid.testing import DummyRequest
11from pyramid.httpexceptions import HTTPFound, HTTPNotFound
12from pyramid.response import Response
13
14from webob.multidict import MultiDict
15
16from ow.models.root import OpenWorkouts
17from ow.models.user import User
18from ow.models.workout import Workout
19from ow.schemas.workout import (
20    ManualWorkoutSchema,
21    UploadedWorkoutSchema,
22    UpdateWorkoutSchema,
23    )
24import ow.views.workout as workout_views
25
26
27class TestWorkoutViews(object):
28
29    # paths to gpx files we can use for testing, used in some of the tests
30    # as py.test fixtures
31    gpx_filenames = (
32        # GPX 1.0 file, no extensions
33        'fixtures/20131013.gpx',
34        # GPX 1.0 file, no extensions, missing elevation
35        'fixtures/20131013-without-elevation.gpx',
36        # GPX 1.1 file with extensions
37        'fixtures/20160129-with-extensions.gpx',
38        )
39
40    def open_uploaded_file(self, path):
41        """
42        Open the uploaded tracking file fixture from disk
43        """
44        uploaded_file_path = os.path.join(
45            os.path.dirname(os.path.dirname(__file__)), path)
46        uploaded_file = open(uploaded_file_path, 'r')
47        return uploaded_file
48
49    def close_uploaded_file(self, uploaded_file):
50        """
51        Close the opened uploaded tracking file
52        """
53        uploaded_file.close()
54
55    def create_filestorage(self, uploaded_file):
56        """
57        Create a FileStorage instance from an open uploaded tracking file,
58        suitable for testing file uploads later
59        """
60        storage = FieldStorage()
61        storage.filename = os.path.basename(uploaded_file.name)
62        storage.file = BytesIO(uploaded_file.read().encode('utf-8'))
63        storage.name = os.path.basename(uploaded_file.name)
64        # This prevents FormEncode validator from thinking we are providing
65        # more than one file for the upload, which crashes the tests
66        storage.list = None
67        return storage
68
69    @pytest.fixture
70    def root(self):
71        root = OpenWorkouts()
72        root['john'] = User(firstname='John', lastname='Doe',
73                            email='john.doe@example.net')
74        root['john'].password = 's3cr3t'
75        workout = Workout(
76            start=datetime(2015, 6, 28, 12, 55, tzinfo=timezone.utc),
77            duration=timedelta(minutes=60),
78            distance=30
79        )
80        root['john'].add_workout(workout)
81        return root
82
83    @pytest.fixture
84    def dummy_request(self, root):
85        request = DummyRequest()
86        request.root = root
87        return request
88
89    @pytest.fixture
90    def valid_post_request(self, root):
91        request = DummyRequest()
92        request.root = root
93        request.method = 'POST'
94        request.POST = MultiDict({
95            'start_date': '21/12/2015',
96            'start_time': '8:30',
97            'duration_hours': '3',
98            'duration_minutes': '30',
99            'duration_seconds': '20',
100            'distance': '10',
101            'submit': True,
102            })
103        return request
104
105    def test_add_workout_manually_get(self, dummy_request):
106        """
107        Test the view that renders the "add workout manually" form
108        """
109        request = dummy_request
110        user = request.root['john']
111        response = workout_views.add_workout_manually(user, request)
112        assert 'form' in response
113        assert len(response['form'].form.errors) == 0
114        assert isinstance(response['form'].form.schema, ManualWorkoutSchema)
115
116    def test_add_workout_manually_post_invalid(self, dummy_request):
117        """
118        POST request to add a workout manually, without providing the required
119        form data.
120        """
121        request = dummy_request
122        user = request.root['john']
123        request.method = 'POST'
124        request.POST = MultiDict({'submit': True})
125        response = workout_views.add_workout_manually(user, request)
126        assert 'form' in response
127        # All required fields (6) are marked in the form errors
128        assert len(response['form'].form.errors) == 6
129
130    add_workout_params = [
131        # no title, no sport, we generate a title based on when the
132        # workout started
133        ({'title': None, 'sport': None}, 'Morning workout'),
134        # no title, sport given, we use the sport too in the automatically
135        # generated title
136        ({'title': None, 'sport': 'cycling'}, 'Morning cycling workout'),
137        # title given, no sport, we use the provided title
138        ({'title': 'Example workout', 'sport': None}, 'Example workout'),
139        # title given, sport too, we use the provided title
140        ({'title': 'Example workout', 'sport': 'cycling'}, 'Example workout'),
141    ]
142
143    @pytest.mark.parametrize(('params', 'expected'), add_workout_params)
144    def test_add_workout_manually_post_valid(self, params, expected,
145                                             valid_post_request):
146        """
147        POST request to add a workout manually, providing the needed data
148        """
149        request = valid_post_request
150        if params['title'] is not None:
151            request.POST['title'] = params['title']
152        if params['sport'] is not None:
153            request.POST['sport'] = params['sport']
154        user = request.root['john']
155        assert len(user.workouts()) == 1
156        response = workout_views.add_workout_manually(user, request)
157        assert isinstance(response, HTTPFound)
158        assert response.location.endswith('/2/')
159        assert len(user.workouts()) == 2
160        assert user['2'].title == expected
161
162    def test_add_workout_get(self, dummy_request):
163        """
164        Test the view that renders the "add workout by upload tracking file"
165        form
166        """
167        request = dummy_request
168        user = request.root['john']
169        response = workout_views.add_workout(user, request)
170        assert 'form' in response
171        assert len(response['form'].form.errors) == 0
172        assert isinstance(response['form'].form.schema, UploadedWorkoutSchema)
173
174    def test_add_workout_post_invalid(self, dummy_request):
175        """
176        POST request to add a workout by uploading a tracking file, without
177        providing the required form data.
178        """
179        request = dummy_request
180        user = request.root['john']
181        request.method = 'POST'
182        request.POST = MultiDict({'submit': True})
183        response = workout_views.add_workout(user, request)
184        assert 'form' in response
185        # Only one required field in this case, the tracking file
186        assert len(response['form'].form.errors) == 1
187
188    def test_add_workout_post_invalid_bytes(self, dummy_request):
189        """
190        POST request to add a workout, without uploading a tracking file,
191        which sends an empty bytes object (b'')
192        """
193        request = dummy_request
194        user = request.root['john']
195        request.method = 'POST'
196        request.POST = MultiDict({
197            'tracking_file': b'',
198            'submit': True,
199            })
200        assert len(request.root['john'].workouts()) == 1
201        response = workout_views.add_workout(user, request)
202        assert 'form' in response
203        # Only one required field in this case, the tracking file
204        assert len(response['form'].form.errors) == 1
205
206    @pytest.mark.parametrize('filename', gpx_filenames)
207    def test_add_workout_post_valid(self, filename, dummy_request):
208        """
209        POST request to add a workout, uploading a tracking file
210        """
211        request = dummy_request
212        uploaded_file = self.open_uploaded_file(filename)
213        filestorage = self.create_filestorage(uploaded_file)
214        user = request.root['john']
215        request.method = 'POST'
216        request.POST = MultiDict({
217            'tracking_file': filestorage,
218            'submit': True,
219            })
220        assert len(request.root['john'].workouts()) == 1
221        response = workout_views.add_workout(user, request)
222        assert isinstance(response, HTTPFound)
223        assert response.location.endswith('/2/')
224        assert len(request.root['john'].workouts()) == 2
225        self.close_uploaded_file(uploaded_file)
226
227    def test_edit_workout_get(self, dummy_request):
228        """
229        Test the view that renders the "edit workout" form
230        """
231        request = dummy_request
232        user = request.root['john']
233        workout = user.workouts()[0]
234        response = workout_views.edit_workout(workout, request)
235        assert 'form' in response
236        assert len(response['form'].form.errors) == 0
237        assert isinstance(response['form'].form.schema, ManualWorkoutSchema)
238
239    def test_edit_workout_post_invalid(self, dummy_request):
240        """
241        POST request to edit a workout, without providing the required form
242        data (like removing data from required fields).
243        """
244        request = dummy_request
245        user = request.root['john']
246        workout = user.workouts()[0]
247        request.method = 'POST'
248        request.POST = MultiDict({'submit': True})
249        response = workout_views.edit_workout(workout, request)
250        assert 'form' in response
251        # All required fields (6) are marked in the form errors
252        assert len(response['form'].form.errors) == 6
253
254    def test_edit_workout_post_valid(self, valid_post_request):
255        """
256        POST request to edit a workout, providing the needed data
257        """
258        request = valid_post_request
259        user = request.root['john']
260        workout = user.workouts()[0]
261        assert len(user.workouts()) == 1
262        assert workout.start == datetime(
263            2015, 6, 28, 12, 55, tzinfo=timezone.utc)
264        response = workout_views.edit_workout(workout, request)
265        assert isinstance(response, HTTPFound)
266        assert response.location.endswith('/1/')
267        assert len(user.workouts()) == 1
268        assert user.workouts()[0].start == datetime(
269            2015, 12, 21, 8, 30, tzinfo=timezone.utc)
270
271    def test_update_workout_from_file_get(self, dummy_request):
272        """
273        Test the view that renders the "update workout from file" form
274        """
275        request = dummy_request
276        user = request.root['john']
277        workout = user.workouts()[0]
278        response = workout_views.update_workout_from_file(workout, request)
279        assert 'form' in response
280        assert len(response['form'].form.errors) == 0
281        assert isinstance(response['form'].form.schema, UpdateWorkoutSchema)
282
283    def test_update_workout_from_file_post_invalid(self, dummy_request):
284        """
285        POST request to update a workout by uploading a tracking file, without
286        providing the required form data.
287        """
288        request = dummy_request
289        user = request.root['john']
290        workout = user.workouts()[0]
291        request.method = 'POST'
292        request.POST = MultiDict({'submit': True})
293        response = workout_views.update_workout_from_file(workout, request)
294        assert 'form' in response
295        # Only one required field in this case, the tracking file
296        assert len(response['form'].form.errors) == 1
297
298    def test_update_workout_from_file_post_invalid_bytes(self, dummy_request):
299        """
300        POST request to update a workout, without uploading a tracking file,
301        which sends an empty bytes object (b'')
302        """
303        request = dummy_request
304        user = request.root['john']
305        workout = user.workouts()[0]
306        request.method = 'POST'
307        request.POST = MultiDict({
308            'tracking_file': b'',
309            'submit': True,
310            })
311        response = workout_views.update_workout_from_file(workout, request)
312        assert 'form' in response
313        # Only one required field in this case, the tracking file
314        assert len(response['form'].form.errors) == 1
315
316    @pytest.mark.parametrize('filen', gpx_filenames)
317    def test_update_workout_from_file_post_valid(self, filen, dummy_request):
318        """
319        POST request to update a workout, uploading a tracking file
320        """
321        filename = filen
322        request = dummy_request
323        uploaded_file = self.open_uploaded_file(filename)
324        filestorage = self.create_filestorage(uploaded_file)
325        user = request.root['john']
326        workout = user.workouts()[0]
327        request.method = 'POST'
328        request.POST = MultiDict({
329            'tracking_file': filestorage,
330            'submit': True,
331            })
332        assert len(user.workouts()) == 1
333        response = workout_views.update_workout_from_file(workout, request)
334        assert isinstance(response, HTTPFound)
335        assert response.location.endswith('/1/')
336        assert len(request.root['john'].workouts()) == 1
337        self.close_uploaded_file(uploaded_file)
338
339    def test_delete_workout_get(self, dummy_request):
340        request = dummy_request
341        user = request.root['john']
342        workout = user.workouts()[0]
343        response = workout_views.delete_workout(workout, request)
344        assert response == {}
345
346    def test_delete_workout_post_invalid(self, dummy_request):
347        request = dummy_request
348        user = request.root['john']
349        workout = user.workouts()[0]
350        request.method = 'POST'
351        # invalid, missing confirmation delete hidden value
352        request.POST = MultiDict({'submit': True})
353        response = workout_views.delete_workout(workout, request)
354        # we do reload the page asking for confirmation
355        assert response == {}
356
357    def test_delete_workout_post_valid(self, root):
358        """
359        Valid POST request to delete a workout.
360        Instead of reusing the DummyRequest from the request fixture, we do
361        Mock fully the request here, because we need to use
362        authenticated_userid, which cannot be easily set in the DummyRequest
363        """
364        request = Mock()
365        request.root = root
366        request.method = 'POST'
367        request.resource_url.return_value = '/dashboard/'
368        # invalid, missing confirmation delete hidden value
369        request.POST = MultiDict({'submit': True, 'delete': 'yes'})
370        user = request.root['john']
371        workout = user.workouts()[0]
372        # A real request will have the current logged in user id, which we need
373        # for deleting the workout
374        request.authenticated_userid = 'john'
375        response = workout_views.delete_workout(workout, request)
376        # after a successful delete, we send the user back to his dashboard
377        assert isinstance(response, HTTPFound)
378        assert response.location.endswith('/')
379        assert len(user.workouts()) == 0
380
381    def test_workout_without_gpx(self, dummy_request):
382        """
383        Test the view that renders the workout details page for a workout
384        without tracking data
385        """
386        request = dummy_request
387        user = request.root['john']
388        workout = user.workouts()[0]
389        response = workout_views.workout(workout, request)
390        assert response['start_point'] == {}
391
392    def test_workout_with_gpx(self, dummy_request):
393        """
394        Test the view that renders the workout details page for a workout
395        with a gpx tracking file. We use a gpx from the test fixtures
396        """
397        request = dummy_request
398        # expected values (from the gpx fixture file)
399        expected = {'latitude': 37.108735040304566,
400                    'longitude': 25.472489344630546,
401                    'elevation': None}
402
403        user = request.root['john']
404        workout = user.workouts()[0]
405        # to ensure has_gpx returns true
406        workout.tracking_filetype = 'gpx'
407
408        gpx_file_path = os.path.join(
409            os.path.dirname(os.path.dirname(__file__)),
410            'fixtures/20131013.gpx')
411        with patch.object(workout, 'tracking_file') as tf:
412            with open(gpx_file_path, 'r') as gpx_file:
413                tf.open.return_value = BytesIO(gpx_file.read().encode('utf-8'))
414                response = workout_views.workout(workout, request)
415                assert response['start_point'] == expected
416
417    def test_workout_gpx_no_gpx(self, dummy_request):
418        """
419        The view that renders the gpx contents attached to a workout return a
420        404 if the workout has no gpx
421        """
422        request = dummy_request
423        user = request.root['john']
424        workout = user.workouts()[0]
425        response = workout_views.workout_gpx(workout, request)
426        assert isinstance(response, HTTPNotFound)
427
428    def test_workout_gpx(self, dummy_request):
429        """
430        The view that renders the gpx contents attached to a workout returns a
431        response containing the gpx contents, as with the proper content_type
432        and all
433        """
434        request = dummy_request
435        user = request.root['john']
436        workout = user.workouts()[0]
437        # to ensure has_gpx returns true
438        workout.tracking_filetype = 'gpx'
439
440        # part of the expected body, so we can assert later
441        expected_body = b'<gpx version="1.1" creator="OpenWorkouts"'
442
443        gpx_file_path = os.path.join(
444            os.path.dirname(os.path.dirname(__file__)),
445            'fixtures/20131013.gpx')
446        with patch.object(workout, 'tracking_file') as tf:
447            with open(gpx_file_path, 'r') as gpx_file:
448                tf.open.return_value = BytesIO(gpx_file.read().encode('utf-8'))
449                response = workout_views.workout_gpx(workout, request)
450                assert response.status_code == 200
451                assert response.content_type == 'application/xml'
452                assert expected_body in response.body
453
454    def test_workout_map_no_gpx(self, dummy_request):
455        request = dummy_request
456        user = request.root['john']
457        workout = user.workouts()[0]
458        response = workout_views.workout_map(workout, request)
459        assert response == {'start_point': {}}
460
461    def test_workout_map(self, dummy_request):
462        request = dummy_request
463        user = request.root['john']
464        workout = user.workouts()[0]
465        # to ensure has_gpx returns true
466        workout.tracking_filetype = 'gpx'
467        gpx_file_path = os.path.join(
468            os.path.dirname(os.path.dirname(__file__)),
469            'fixtures/20131013.gpx')
470        with patch.object(workout, 'tracking_file') as tf:
471            with open(gpx_file_path, 'r') as gpx_file:
472                tf.open.return_value = BytesIO(gpx_file.read().encode('utf-8'))
473                response = workout_views.workout_map(workout, request)
474                assert response == {
475                    'start_point': {
476                        'elevation': None,
477                        'latitude': 37.108735040304566,
478                        'longitude': 25.472489344630546
479                    }
480                }
481
482    @patch('ow.views.workout.save_map_screenshot')
483    def test_workout_map_shot_generate_map(self, save_map, dummy_request):
484        """
485        Call the view that returns the url to the screenshot of a workout
486        tracking map, without a map being generated previously, the map is
487        generated using save_map_screenshot
488        """
489        def static_url(url):
490            return url
491        request = dummy_request
492        # mock static url to make testing this a bit easier
493        request.static_url = static_url
494        user = request.root['john']
495        workout = user.workouts()[0]
496        # we mock map_screenshot so the first access is None, triggering the
497        # save_map_screenshot call. The second access returns a string we can
498        # use with static_url for testing purposes
499        type(workout).map_screenshot = PropertyMock(
500            side_effect=[None, 'ow:static/maps/somemap.png'])
501        response = workout_views.workout_map_shot(workout, request)
502        save_map.assert_called_once_with(workout, request)
503        assert isinstance(response, Response)
504        assert response.content_type == 'application/json'
505        # the body is a valid json-encoded stream
506        obj = json.loads(response.body)
507        assert 'ow:static/maps/somemap.png' in obj['url']
508
509    @patch('ow.views.workout.save_map_screenshot')
510    def test_workout_map_shot_existing(self, save_map, dummy_request):
511        """
512        Call the view that returns the url to the screenshot of a workout
513        tracking map, with an existing map already there
514        """
515        def static_url(url):
516            return url
517        request = dummy_request
518        # mock static url to make testing this a bit easier
519        request.static_url = static_url
520        user = request.root['john']
521        workout = user.workouts()[0]
522        type(workout).map_screenshot = PropertyMock(
523            side_effect=['ow:static/maps/somemap.png',
524                         'ow:static/maps/somemap.png'])
525        response = workout_views.workout_map_shot(workout, request)
526        assert not save_map.called
527        assert isinstance(response, Response)
528        assert response.content_type == 'application/json'
529        # the body is a valid json-encoded stream
530        obj = json.loads(response.body)
531        assert 'ow:static/maps/somemap.png' in obj['url']
Note: See TracBrowser for help on using the repository browser.