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
RevLine 
[5ec3a0b]1import os
[cef474f]2import json
[5ec3a0b]3from io import BytesIO
4from datetime import datetime, timedelta, timezone
5from cgi import FieldStorage
[cef474f]6from unittest.mock import Mock, patch, PropertyMock
[5ec3a0b]7
8import pytest
9
10from pyramid.testing import DummyRequest
11from pyramid.httpexceptions import HTTPFound, HTTPNotFound
[cef474f]12from pyramid.response import Response
[5ec3a0b]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
[d517001]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):
[5ec3a0b]146        """
147        POST request to add a workout manually, providing the needed data
148        """
149        request = valid_post_request
[d517001]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']
[5ec3a0b]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
[d517001]160        assert user['2'].title == expected
[5ec3a0b]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
[fe6089a]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
[5ec3a0b]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
[fe6089a]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
[5ec3a0b]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
[ceae158]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                }
[cef474f]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.