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

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

(#58) Set a title automatically when adding manually a workout without
providing one.

The title is generated based on the only required data we have (starting
date and time) + sport (if provided).

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