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

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

Imported sources from the old python2-only repository:

  • Modified the code so it is python 3.6 compatible
  • Fixed deprecation warnings, pyramid 1.10.x supported now
  • Fixed deprecation warnings about some libraries, like pyramid-simpleform
  • Added pytest-pycodestyle and pytest-flakes for automatic checks on the source code files when running tests.
  • Added default pytest.ini setup to enforce some default parameters when running tests.
  • Cleaned up the code a bit, catched up with tests coverage.
  • Property mode set to 100644
File size: 14.9 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    @pytest.mark.parametrize('filename', gpx_filenames)
167    def test_add_workout_post_valid(self, filename, dummy_request):
168        """
169        POST request to add a workout, uploading a tracking file
170        """
171        request = dummy_request
172        uploaded_file = self.open_uploaded_file(filename)
173        filestorage = self.create_filestorage(uploaded_file)
174        user = request.root['john']
175        request.method = 'POST'
176        request.POST = MultiDict({
177            'tracking_file': filestorage,
178            'submit': True,
179            })
180        assert len(request.root['john'].workouts()) == 1
181        response = workout_views.add_workout(user, request)
182        assert isinstance(response, HTTPFound)
183        assert response.location.endswith('/2/')
184        assert len(request.root['john'].workouts()) == 2
185        self.close_uploaded_file(uploaded_file)
186
187    def test_edit_workout_get(self, dummy_request):
188        """
189        Test the view that renders the "edit workout" form
190        """
191        request = dummy_request
192        user = request.root['john']
193        workout = user.workouts()[0]
194        response = workout_views.edit_workout(workout, request)
195        assert 'form' in response
196        assert len(response['form'].form.errors) == 0
197        assert isinstance(response['form'].form.schema, ManualWorkoutSchema)
198
199    def test_edit_workout_post_invalid(self, dummy_request):
200        """
201        POST request to edit a workout, without providing the required form
202        data (like removing data from required fields).
203        """
204        request = dummy_request
205        user = request.root['john']
206        workout = user.workouts()[0]
207        request.method = 'POST'
208        request.POST = MultiDict({'submit': True})
209        response = workout_views.edit_workout(workout, request)
210        assert 'form' in response
211        # All required fields (6) are marked in the form errors
212        assert len(response['form'].form.errors) == 6
213
214    def test_edit_workout_post_valid(self, valid_post_request):
215        """
216        POST request to edit a workout, providing the needed data
217        """
218        request = valid_post_request
219        user = request.root['john']
220        workout = user.workouts()[0]
221        assert len(user.workouts()) == 1
222        assert workout.start == datetime(
223            2015, 6, 28, 12, 55, tzinfo=timezone.utc)
224        response = workout_views.edit_workout(workout, request)
225        assert isinstance(response, HTTPFound)
226        assert response.location.endswith('/1/')
227        assert len(user.workouts()) == 1
228        assert user.workouts()[0].start == datetime(
229            2015, 12, 21, 8, 30, tzinfo=timezone.utc)
230
231    def test_update_workout_from_file_get(self, dummy_request):
232        """
233        Test the view that renders the "update workout from file" form
234        """
235        request = dummy_request
236        user = request.root['john']
237        workout = user.workouts()[0]
238        response = workout_views.update_workout_from_file(workout, request)
239        assert 'form' in response
240        assert len(response['form'].form.errors) == 0
241        assert isinstance(response['form'].form.schema, UpdateWorkoutSchema)
242
243    def test_update_workout_from_file_post_invalid(self, dummy_request):
244        """
245        POST request to update a workout by uploading a tracking file, without
246        providing the required form data.
247        """
248        request = dummy_request
249        user = request.root['john']
250        workout = user.workouts()[0]
251        request.method = 'POST'
252        request.POST = MultiDict({'submit': True})
253        response = workout_views.update_workout_from_file(workout, request)
254        assert 'form' in response
255        # Only one required field in this case, the tracking file
256        assert len(response['form'].form.errors) == 1
257
258    @pytest.mark.parametrize('filen', gpx_filenames)
259    def test_update_workout_from_file_post_valid(self, filen, dummy_request):
260        """
261        POST request to update a workout, uploading a tracking file
262        """
263        filename = filen
264        request = dummy_request
265        uploaded_file = self.open_uploaded_file(filename)
266        filestorage = self.create_filestorage(uploaded_file)
267        user = request.root['john']
268        workout = user.workouts()[0]
269        request.method = 'POST'
270        request.POST = MultiDict({
271            'tracking_file': filestorage,
272            'submit': True,
273            })
274        assert len(user.workouts()) == 1
275        response = workout_views.update_workout_from_file(workout, request)
276        assert isinstance(response, HTTPFound)
277        assert response.location.endswith('/1/')
278        assert len(request.root['john'].workouts()) == 1
279        self.close_uploaded_file(uploaded_file)
280
281    def test_delete_workout_get(self, dummy_request):
282        request = dummy_request
283        user = request.root['john']
284        workout = user.workouts()[0]
285        response = workout_views.delete_workout(workout, request)
286        assert response == {}
287
288    def test_delete_workout_post_invalid(self, dummy_request):
289        request = dummy_request
290        user = request.root['john']
291        workout = user.workouts()[0]
292        request.method = 'POST'
293        # invalid, missing confirmation delete hidden value
294        request.POST = MultiDict({'submit': True})
295        response = workout_views.delete_workout(workout, request)
296        # we do reload the page asking for confirmation
297        assert response == {}
298
299    def test_delete_workout_post_valid(self, root):
300        """
301        Valid POST request to delete a workout.
302        Instead of reusing the DummyRequest from the request fixture, we do
303        Mock fully the request here, because we need to use
304        authenticated_userid, which cannot be easily set in the DummyRequest
305        """
306        request = Mock()
307        request.root = root
308        request.method = 'POST'
309        request.resource_url.return_value = '/dashboard/'
310        # invalid, missing confirmation delete hidden value
311        request.POST = MultiDict({'submit': True, 'delete': 'yes'})
312        user = request.root['john']
313        workout = user.workouts()[0]
314        # A real request will have the current logged in user id, which we need
315        # for deleting the workout
316        request.authenticated_userid = 'john'
317        response = workout_views.delete_workout(workout, request)
318        # after a successful delete, we send the user back to his dashboard
319        assert isinstance(response, HTTPFound)
320        assert response.location.endswith('/')
321        assert len(user.workouts()) == 0
322
323    def test_workout_without_gpx(self, dummy_request):
324        """
325        Test the view that renders the workout details page for a workout
326        without tracking data
327        """
328        request = dummy_request
329        user = request.root['john']
330        workout = user.workouts()[0]
331        response = workout_views.workout(workout, request)
332        assert response['start_point'] == {}
333
334    def test_workout_with_gpx(self, dummy_request):
335        """
336        Test the view that renders the workout details page for a workout
337        with a gpx tracking file. We use a gpx from the test fixtures
338        """
339        request = dummy_request
340        # expected values (from the gpx fixture file)
341        expected = {'latitude': 37.108735040304566,
342                    'longitude': 25.472489344630546,
343                    'elevation': None}
344
345        user = request.root['john']
346        workout = user.workouts()[0]
347        # to ensure has_gpx returns true
348        workout.tracking_filetype = 'gpx'
349
350        gpx_file_path = os.path.join(
351            os.path.dirname(os.path.dirname(__file__)),
352            'fixtures/20131013.gpx')
353        with patch.object(workout, 'tracking_file') as tf:
354            with open(gpx_file_path, 'r') as gpx_file:
355                tf.open.return_value = BytesIO(gpx_file.read().encode('utf-8'))
356                response = workout_views.workout(workout, request)
357                assert response['start_point'] == expected
358
359    def test_workout_gpx_no_gpx(self, dummy_request):
360        """
361        The view that renders the gpx contents attached to a workout return a
362        404 if the workout has no gpx
363        """
364        request = dummy_request
365        user = request.root['john']
366        workout = user.workouts()[0]
367        response = workout_views.workout_gpx(workout, request)
368        assert isinstance(response, HTTPNotFound)
369
370    def test_workout_gpx(self, dummy_request):
371        """
372        The view that renders the gpx contents attached to a workout returns a
373        response containing the gpx contents, as with the proper content_type
374        and all
375        """
376        request = dummy_request
377        user = request.root['john']
378        workout = user.workouts()[0]
379        # to ensure has_gpx returns true
380        workout.tracking_filetype = 'gpx'
381
382        # part of the expected body, so we can assert later
383        expected_body = b'<gpx version="1.1" creator="OpenWorkouts"'
384
385        gpx_file_path = os.path.join(
386            os.path.dirname(os.path.dirname(__file__)),
387            'fixtures/20131013.gpx')
388        with patch.object(workout, 'tracking_file') as tf:
389            with open(gpx_file_path, 'r') as gpx_file:
390                tf.open.return_value = BytesIO(gpx_file.read().encode('utf-8'))
391                response = workout_views.workout_gpx(workout, request)
392                assert response.status_code == 200
393                assert response.content_type == 'application/xml'
394                assert expected_body in response.body
Note: See TracBrowser for help on using the repository browser.