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

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

Added missing tests covering the map screenshots feature

  • Property mode set to 100644
File size: 17.5 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    def test_add_workout_manually_post_valid(self, valid_post_request):
129        """
130        POST request to add a workout manually, providing the needed data
131        """
132        request = valid_post_request
133        user = request.root['john']
134        assert len(user.workouts()) == 1
135        response = workout_views.add_workout_manually(user, request)
136        assert isinstance(response, HTTPFound)
137        assert response.location.endswith('/2/')
138        assert len(user.workouts()) == 2
139
140    def test_add_workout_get(self, dummy_request):
141        """
142        Test the view that renders the "add workout by upload tracking file"
143        form
144        """
145        request = dummy_request
146        user = request.root['john']
147        response = workout_views.add_workout(user, request)
148        assert 'form' in response
149        assert len(response['form'].form.errors) == 0
150        assert isinstance(response['form'].form.schema, UploadedWorkoutSchema)
151
152    def test_add_workout_post_invalid(self, dummy_request):
153        """
154        POST request to add a workout by uploading a tracking file, without
155        providing the required form data.
156        """
157        request = dummy_request
158        user = request.root['john']
159        request.method = 'POST'
160        request.POST = MultiDict({'submit': True})
161        response = workout_views.add_workout(user, request)
162        assert 'form' in response
163        # Only one required field in this case, the tracking file
164        assert len(response['form'].form.errors) == 1
165
166    def test_add_workout_post_invalid_bytes(self, dummy_request):
167        """
168        POST request to add a workout, without uploading a tracking file,
169        which sends an empty bytes object (b'')
170        """
171        request = dummy_request
172        user = request.root['john']
173        request.method = 'POST'
174        request.POST = MultiDict({
175            'tracking_file': b'',
176            'submit': True,
177            })
178        assert len(request.root['john'].workouts()) == 1
179        response = workout_views.add_workout(user, request)
180        assert 'form' in response
181        # Only one required field in this case, the tracking file
182        assert len(response['form'].form.errors) == 1
183
184    @pytest.mark.parametrize('filename', gpx_filenames)
185    def test_add_workout_post_valid(self, filename, dummy_request):
186        """
187        POST request to add a workout, uploading a tracking file
188        """
189        request = dummy_request
190        uploaded_file = self.open_uploaded_file(filename)
191        filestorage = self.create_filestorage(uploaded_file)
192        user = request.root['john']
193        request.method = 'POST'
194        request.POST = MultiDict({
195            'tracking_file': filestorage,
196            'submit': True,
197            })
198        assert len(request.root['john'].workouts()) == 1
199        response = workout_views.add_workout(user, request)
200        assert isinstance(response, HTTPFound)
201        assert response.location.endswith('/2/')
202        assert len(request.root['john'].workouts()) == 2
203        self.close_uploaded_file(uploaded_file)
204
205    def test_edit_workout_get(self, dummy_request):
206        """
207        Test the view that renders the "edit workout" form
208        """
209        request = dummy_request
210        user = request.root['john']
211        workout = user.workouts()[0]
212        response = workout_views.edit_workout(workout, request)
213        assert 'form' in response
214        assert len(response['form'].form.errors) == 0
215        assert isinstance(response['form'].form.schema, ManualWorkoutSchema)
216
217    def test_edit_workout_post_invalid(self, dummy_request):
218        """
219        POST request to edit a workout, without providing the required form
220        data (like removing data from required fields).
221        """
222        request = dummy_request
223        user = request.root['john']
224        workout = user.workouts()[0]
225        request.method = 'POST'
226        request.POST = MultiDict({'submit': True})
227        response = workout_views.edit_workout(workout, request)
228        assert 'form' in response
229        # All required fields (6) are marked in the form errors
230        assert len(response['form'].form.errors) == 6
231
232    def test_edit_workout_post_valid(self, valid_post_request):
233        """
234        POST request to edit a workout, providing the needed data
235        """
236        request = valid_post_request
237        user = request.root['john']
238        workout = user.workouts()[0]
239        assert len(user.workouts()) == 1
240        assert workout.start == datetime(
241            2015, 6, 28, 12, 55, tzinfo=timezone.utc)
242        response = workout_views.edit_workout(workout, request)
243        assert isinstance(response, HTTPFound)
244        assert response.location.endswith('/1/')
245        assert len(user.workouts()) == 1
246        assert user.workouts()[0].start == datetime(
247            2015, 12, 21, 8, 30, tzinfo=timezone.utc)
248
249    def test_update_workout_from_file_get(self, dummy_request):
250        """
251        Test the view that renders the "update workout from file" form
252        """
253        request = dummy_request
254        user = request.root['john']
255        workout = user.workouts()[0]
256        response = workout_views.update_workout_from_file(workout, request)
257        assert 'form' in response
258        assert len(response['form'].form.errors) == 0
259        assert isinstance(response['form'].form.schema, UpdateWorkoutSchema)
260
261    def test_update_workout_from_file_post_invalid(self, dummy_request):
262        """
263        POST request to update a workout by uploading a tracking file, without
264        providing the required form data.
265        """
266        request = dummy_request
267        user = request.root['john']
268        workout = user.workouts()[0]
269        request.method = 'POST'
270        request.POST = MultiDict({'submit': True})
271        response = workout_views.update_workout_from_file(workout, request)
272        assert 'form' in response
273        # Only one required field in this case, the tracking file
274        assert len(response['form'].form.errors) == 1
275
276    def test_update_workout_from_file_post_invalid_bytes(self, dummy_request):
277        """
278        POST request to update a workout, without uploading a tracking file,
279        which sends an empty bytes object (b'')
280        """
281        request = dummy_request
282        user = request.root['john']
283        workout = user.workouts()[0]
284        request.method = 'POST'
285        request.POST = MultiDict({
286            'tracking_file': b'',
287            'submit': True,
288            })
289        response = workout_views.update_workout_from_file(workout, request)
290        assert 'form' in response
291        # Only one required field in this case, the tracking file
292        assert len(response['form'].form.errors) == 1
293
294    @pytest.mark.parametrize('filen', gpx_filenames)
295    def test_update_workout_from_file_post_valid(self, filen, dummy_request):
296        """
297        POST request to update a workout, uploading a tracking file
298        """
299        filename = filen
300        request = dummy_request
301        uploaded_file = self.open_uploaded_file(filename)
302        filestorage = self.create_filestorage(uploaded_file)
303        user = request.root['john']
304        workout = user.workouts()[0]
305        request.method = 'POST'
306        request.POST = MultiDict({
307            'tracking_file': filestorage,
308            'submit': True,
309            })
310        assert len(user.workouts()) == 1
311        response = workout_views.update_workout_from_file(workout, request)
312        assert isinstance(response, HTTPFound)
313        assert response.location.endswith('/1/')
314        assert len(request.root['john'].workouts()) == 1
315        self.close_uploaded_file(uploaded_file)
316
317    def test_delete_workout_get(self, dummy_request):
318        request = dummy_request
319        user = request.root['john']
320        workout = user.workouts()[0]
321        response = workout_views.delete_workout(workout, request)
322        assert response == {}
323
324    def test_delete_workout_post_invalid(self, dummy_request):
325        request = dummy_request
326        user = request.root['john']
327        workout = user.workouts()[0]
328        request.method = 'POST'
329        # invalid, missing confirmation delete hidden value
330        request.POST = MultiDict({'submit': True})
331        response = workout_views.delete_workout(workout, request)
332        # we do reload the page asking for confirmation
333        assert response == {}
334
335    def test_delete_workout_post_valid(self, root):
336        """
337        Valid POST request to delete a workout.
338        Instead of reusing the DummyRequest from the request fixture, we do
339        Mock fully the request here, because we need to use
340        authenticated_userid, which cannot be easily set in the DummyRequest
341        """
342        request = Mock()
343        request.root = root
344        request.method = 'POST'
345        request.resource_url.return_value = '/dashboard/'
346        # invalid, missing confirmation delete hidden value
347        request.POST = MultiDict({'submit': True, 'delete': 'yes'})
348        user = request.root['john']
349        workout = user.workouts()[0]
350        # A real request will have the current logged in user id, which we need
351        # for deleting the workout
352        request.authenticated_userid = 'john'
353        response = workout_views.delete_workout(workout, request)
354        # after a successful delete, we send the user back to his dashboard
355        assert isinstance(response, HTTPFound)
356        assert response.location.endswith('/')
357        assert len(user.workouts()) == 0
358
359    def test_workout_without_gpx(self, dummy_request):
360        """
361        Test the view that renders the workout details page for a workout
362        without tracking data
363        """
364        request = dummy_request
365        user = request.root['john']
366        workout = user.workouts()[0]
367        response = workout_views.workout(workout, request)
368        assert response['start_point'] == {}
369
370    def test_workout_with_gpx(self, dummy_request):
371        """
372        Test the view that renders the workout details page for a workout
373        with a gpx tracking file. We use a gpx from the test fixtures
374        """
375        request = dummy_request
376        # expected values (from the gpx fixture file)
377        expected = {'latitude': 37.108735040304566,
378                    'longitude': 25.472489344630546,
379                    'elevation': None}
380
381        user = request.root['john']
382        workout = user.workouts()[0]
383        # to ensure has_gpx returns true
384        workout.tracking_filetype = 'gpx'
385
386        gpx_file_path = os.path.join(
387            os.path.dirname(os.path.dirname(__file__)),
388            'fixtures/20131013.gpx')
389        with patch.object(workout, 'tracking_file') as tf:
390            with open(gpx_file_path, 'r') as gpx_file:
391                tf.open.return_value = BytesIO(gpx_file.read().encode('utf-8'))
392                response = workout_views.workout(workout, request)
393                assert response['start_point'] == expected
394
395    def test_workout_gpx_no_gpx(self, dummy_request):
396        """
397        The view that renders the gpx contents attached to a workout return a
398        404 if the workout has no gpx
399        """
400        request = dummy_request
401        user = request.root['john']
402        workout = user.workouts()[0]
403        response = workout_views.workout_gpx(workout, request)
404        assert isinstance(response, HTTPNotFound)
405
406    def test_workout_gpx(self, dummy_request):
407        """
408        The view that renders the gpx contents attached to a workout returns a
409        response containing the gpx contents, as with the proper content_type
410        and all
411        """
412        request = dummy_request
413        user = request.root['john']
414        workout = user.workouts()[0]
415        # to ensure has_gpx returns true
416        workout.tracking_filetype = 'gpx'
417
418        # part of the expected body, so we can assert later
419        expected_body = b'<gpx version="1.1" creator="OpenWorkouts"'
420
421        gpx_file_path = os.path.join(
422            os.path.dirname(os.path.dirname(__file__)),
423            'fixtures/20131013.gpx')
424        with patch.object(workout, 'tracking_file') as tf:
425            with open(gpx_file_path, 'r') as gpx_file:
426                tf.open.return_value = BytesIO(gpx_file.read().encode('utf-8'))
427                response = workout_views.workout_gpx(workout, request)
428                assert response.status_code == 200
429                assert response.content_type == 'application/xml'
430                assert expected_body in response.body
431
432    def test_workout_map_no_gpx(self, dummy_request):
433        request = dummy_request
434        user = request.root['john']
435        workout = user.workouts()[0]
436        response = workout_views.workout_map(workout, request)
437        assert response == {'start_point': {}}
438
439    def test_workout_map(self, dummy_request):
440        request = dummy_request
441        user = request.root['john']
442        workout = user.workouts()[0]
443        # to ensure has_gpx returns true
444        workout.tracking_filetype = 'gpx'
445        gpx_file_path = os.path.join(
446            os.path.dirname(os.path.dirname(__file__)),
447            'fixtures/20131013.gpx')
448        with patch.object(workout, 'tracking_file') as tf:
449            with open(gpx_file_path, 'r') as gpx_file:
450                tf.open.return_value = BytesIO(gpx_file.read().encode('utf-8'))
451                response = workout_views.workout_map(workout, request)
452                assert response == {
453                    'start_point': {
454                        'elevation': None,
455                        'latitude': 37.108735040304566,
456                        'longitude': 25.472489344630546
457                    }
458                }
Note: See TracBrowser for help on using the repository browser.