API & Client - return relevant errors when gpx file is invalid
This commit is contained in:
parent
ba890d90b9
commit
c4cec056b4
50
fittrackee/tests/fixtures/fixtures_workouts.py
vendored
50
fittrackee/tests/fixtures/fixtures_workouts.py
vendored
@ -591,6 +591,56 @@ def gpx_file_invalid_xml() -> str:
|
|||||||
'<?xml version=\'1.0\' encoding=\'UTF-8\'?>'
|
'<?xml version=\'1.0\' encoding=\'UTF-8\'?>'
|
||||||
'<gpx xmlns:gpxdata="http://www.cluetrust.com/XML/GPXDATA/1/0" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxext="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns="http://www.topografix.com/GPX/1/1">' # noqa
|
'<gpx xmlns:gpxdata="http://www.cluetrust.com/XML/GPXDATA/1/0" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxext="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns="http://www.topografix.com/GPX/1/1">' # noqa
|
||||||
' <metadata/>'
|
' <metadata/>'
|
||||||
|
' <trk>'
|
||||||
|
' <name>just a workout</name>'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def gpx_file_without_time() -> str:
|
||||||
|
return (
|
||||||
|
'<?xml version=\'1.0\' encoding=\'UTF-8\'?>'
|
||||||
|
'<gpx xmlns:gpxdata="http://www.cluetrust.com/XML/GPXDATA/1/0" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxext="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns="http://www.topografix.com/GPX/1/1">' # noqa
|
||||||
|
' <metadata/>'
|
||||||
|
' <trk>'
|
||||||
|
' <name>just a workout</name>'
|
||||||
|
' <trkseg>'
|
||||||
|
' <trkpt lat="44.68095" lon="6.07367">'
|
||||||
|
' <ele>998</ele>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.68091" lon="6.07367">'
|
||||||
|
' <ele>998</ele>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.6808" lon="6.07364">'
|
||||||
|
' <ele>994</ele>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.68075" lon="6.07364">'
|
||||||
|
' <ele>994</ele>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.68071" lon="6.07364">'
|
||||||
|
' <ele>994</ele>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.68049" lon="6.07361">'
|
||||||
|
' <ele>993</ele>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.68019" lon="6.07356">'
|
||||||
|
' <ele>992</ele>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.68014" lon="6.07355">'
|
||||||
|
' <ele>992</ele>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.67995" lon="6.07358">'
|
||||||
|
' <ele>987</ele>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.67977" lon="6.07364">'
|
||||||
|
' <ele>987</ele>'
|
||||||
|
' </trkpt>'
|
||||||
|
' <trkpt lat="44.67972" lon="6.07367">'
|
||||||
|
' <ele>987</ele>'
|
||||||
|
' </trkpt>'
|
||||||
|
' </trkseg>'
|
||||||
|
' </trk>'
|
||||||
|
'</gpx>'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -674,7 +674,7 @@ class TestPostWorkoutWithGpx(ApiTestCaseMixin, CallArgsMixin):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
data = self.assert_500(response, 'error during gpx processing')
|
data = self.assert_500(response, 'no tracks in gpx file')
|
||||||
assert 'data' not in data
|
assert 'data' not in data
|
||||||
|
|
||||||
def test_it_returns_500_if_gpx_has_invalid_xml(
|
def test_it_returns_500_if_gpx_has_invalid_xml(
|
||||||
@ -703,7 +703,36 @@ class TestPostWorkoutWithGpx(ApiTestCaseMixin, CallArgsMixin):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
data = self.assert_500(response, 'error during gpx file parsing')
|
data = self.assert_500(response, 'gpx file is invalid')
|
||||||
|
assert 'data' not in data
|
||||||
|
|
||||||
|
def test_it_returns_500_if_gpx_has_no_time(
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
user_1: User,
|
||||||
|
sport_1_cycling: Sport,
|
||||||
|
gpx_file_without_time: str,
|
||||||
|
) -> None:
|
||||||
|
client, auth_token = self.get_test_client_and_auth_token(
|
||||||
|
app, user_1.email
|
||||||
|
)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
'/api/workouts',
|
||||||
|
data=dict(
|
||||||
|
file=(
|
||||||
|
BytesIO(str.encode(gpx_file_without_time)),
|
||||||
|
'example.gpx',
|
||||||
|
),
|
||||||
|
data='{"sport_id": 1}',
|
||||||
|
),
|
||||||
|
headers=dict(
|
||||||
|
content_type='multipart/form-data',
|
||||||
|
Authorization=f'Bearer {auth_token}',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
data = self.assert_500(response, '<time> is missing in gpx file')
|
||||||
assert 'data' not in data
|
assert 'data' not in data
|
||||||
|
|
||||||
def test_it_returns_400_if_workout_gpx_has_invalid_extension(
|
def test_it_returns_400_if_workout_gpx_has_invalid_extension(
|
||||||
@ -1405,7 +1434,7 @@ class TestPostWorkoutWithZipArchive(ApiTestCaseMixin):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
data = self.assert_500(response, 'error during gpx processing')
|
data = self.assert_500(response, 'no tracks in gpx file')
|
||||||
assert 'data' not in data
|
assert 'data' not in data
|
||||||
|
|
||||||
def test_it_returns_400_when_files_in_archive_exceed_limit(
|
def test_it_returns_400_when_files_in_archive_exceed_limit(
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
from fittrackee.exceptions import GenericException
|
from fittrackee.exceptions import GenericException
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidGPXException(GenericException):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
class WorkoutException(GenericException):
|
class WorkoutException(GenericException):
|
||||||
...
|
...
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union
|
|||||||
|
|
||||||
import gpxpy.gpx
|
import gpxpy.gpx
|
||||||
|
|
||||||
from ..exceptions import WorkoutGPXException
|
from ..exceptions import InvalidGPXException, WorkoutGPXException
|
||||||
from .weather import WeatherService
|
from .weather import WeatherService
|
||||||
|
|
||||||
weather_service = WeatherService()
|
weather_service = WeatherService()
|
||||||
@ -77,9 +77,12 @@ def get_gpx_info(
|
|||||||
"""
|
"""
|
||||||
Parse and return gpx, map and weather data from gpx file
|
Parse and return gpx, map and weather data from gpx file
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
gpx = open_gpx_file(gpx_file)
|
gpx = open_gpx_file(gpx_file)
|
||||||
|
except Exception:
|
||||||
|
raise InvalidGPXException('error', 'gpx file is invalid')
|
||||||
if gpx is None:
|
if gpx is None:
|
||||||
raise WorkoutGPXException('not found', 'No gpx file')
|
raise InvalidGPXException('error', 'no tracks in gpx file')
|
||||||
|
|
||||||
gpx_data: Dict = {'name': gpx.tracks[0].name, 'segments': []}
|
gpx_data: Dict = {'name': gpx.tracks[0].name, 'segments': []}
|
||||||
max_speed = 0.0
|
max_speed = 0.0
|
||||||
@ -95,6 +98,10 @@ def get_gpx_info(
|
|||||||
segment_start: Optional[datetime] = None
|
segment_start: Optional[datetime] = None
|
||||||
segment_points_nb = len(segment.points)
|
segment_points_nb = len(segment.points)
|
||||||
for point_idx, point in enumerate(segment.points):
|
for point_idx, point in enumerate(segment.points):
|
||||||
|
if point.time is None:
|
||||||
|
raise InvalidGPXException(
|
||||||
|
'error', '<time> is missing in gpx file'
|
||||||
|
)
|
||||||
if point_idx == 0:
|
if point_idx == 0:
|
||||||
segment_start = point.time
|
segment_start = point.time
|
||||||
# first gpx point => get weather
|
# first gpx point => get weather
|
||||||
|
@ -15,7 +15,7 @@ from fittrackee import appLog, db
|
|||||||
from fittrackee.files import get_absolute_file_path
|
from fittrackee.files import get_absolute_file_path
|
||||||
from fittrackee.users.models import User, UserSportPreference
|
from fittrackee.users.models import User, UserSportPreference
|
||||||
|
|
||||||
from ..exceptions import WorkoutException
|
from ..exceptions import InvalidGPXException, WorkoutException
|
||||||
from ..models import Sport, Workout, WorkoutSegment
|
from ..models import Sport, Workout, WorkoutSegment
|
||||||
from .gpx import get_gpx_info
|
from .gpx import get_gpx_info
|
||||||
from .maps import generate_map, get_map_hash
|
from .maps import generate_map, get_map_hash
|
||||||
@ -329,6 +329,9 @@ def process_one_gpx_file(
|
|||||||
except (gpxpy.gpx.GPXXMLSyntaxException, TypeError) as e:
|
except (gpxpy.gpx.GPXXMLSyntaxException, TypeError) as e:
|
||||||
delete_files(absolute_gpx_filepath, absolute_map_filepath)
|
delete_files(absolute_gpx_filepath, absolute_map_filepath)
|
||||||
raise WorkoutException('error', 'error during gpx file parsing', e)
|
raise WorkoutException('error', 'error during gpx file parsing', e)
|
||||||
|
except InvalidGPXException as e:
|
||||||
|
delete_files(absolute_gpx_filepath, absolute_map_filepath)
|
||||||
|
raise WorkoutException('error', str(e))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
delete_files(absolute_gpx_filepath, absolute_map_filepath)
|
delete_files(absolute_gpx_filepath, absolute_map_filepath)
|
||||||
raise WorkoutException('error', 'error during gpx processing', e)
|
raise WorkoutException('error', 'error during gpx processing', e)
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"ERROR": {
|
"ERROR": {
|
||||||
|
"<time> is missing in gpx file": "<time> element is missing in .gpx file.",
|
||||||
"Network Error": "Network Error.",
|
"Network Error": "Network Error.",
|
||||||
"UNKNOWN": "Error. Please try again or contact the administrator.",
|
"UNKNOWN": "Error. Please try again or contact the administrator.",
|
||||||
"at least one file in zip archive exceeds size limit, please check the archive": "At least one file in zip archive exceeds size limit, please check the archive.",
|
"at least one file in zip archive exceeds size limit, please check the archive": "At least one file in zip archive exceeds size limit, please check the archive.",
|
||||||
@ -14,6 +15,7 @@
|
|||||||
"error, registration is disabled": "Error, registration is disabled.",
|
"error, registration is disabled": "Error, registration is disabled.",
|
||||||
"file extension not allowed": "File extension not allowed.",
|
"file extension not allowed": "File extension not allowed.",
|
||||||
"file size is greater than the allowed size": "File size is greater than the allowed size.",
|
"file size is greater than the allowed size": "File size is greater than the allowed size.",
|
||||||
|
"gpx file is invalid": "The .gpx file is invalid.",
|
||||||
"invalid credentials": "Invalid credentials.",
|
"invalid credentials": "Invalid credentials.",
|
||||||
"invalid payload": "Provided data are invalid.",
|
"invalid payload": "Provided data are invalid.",
|
||||||
"invalid token, please log in again": "Invalid token, please log in again.",
|
"invalid token, please log in again": "Invalid token, please log in again.",
|
||||||
@ -21,6 +23,7 @@
|
|||||||
"new email must be different than curent email": "The new email must be different than curent email",
|
"new email must be different than curent email": "The new email must be different than curent email",
|
||||||
"no file part": "No file provided.",
|
"no file part": "No file provided.",
|
||||||
"no selected file": "No selected file.",
|
"no selected file": "No selected file.",
|
||||||
|
"no tracks in gpx file": "No track (<trk>) in .gpx file.",
|
||||||
"ongoing request exists": "A data export request already exists.",
|
"ongoing request exists": "A data export request already exists.",
|
||||||
"password: password and password confirmation do not match": "Password: password and password confirmation don't match.",
|
"password: password and password confirmation do not match": "Password: password and password confirmation don't match.",
|
||||||
"provide a valid auth token": "Provide a valid auth token.",
|
"provide a valid auth token": "Provide a valid auth token.",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"ERROR": {
|
"ERROR": {
|
||||||
|
"<time> is missing in gpx file": "Elément <time> manquant dans le fichier .gpx.",
|
||||||
"Network Error": "Erreur réseau.",
|
"Network Error": "Erreur réseau.",
|
||||||
"UNKNOWN": "Erreur. Veuillez réessayer ou contacter l'administrateur.",
|
"UNKNOWN": "Erreur. Veuillez réessayer ou contacter l'administrateur.",
|
||||||
"at least one file in zip archive exceeds size limit, please check the archive": "Au moins un fichier de l'archive zip dépasse la taille maximale, veuillez vérifier l'archive.",
|
"at least one file in zip archive exceeds size limit, please check the archive": "Au moins un fichier de l'archive zip dépasse la taille maximale, veuillez vérifier l'archive.",
|
||||||
@ -14,6 +15,7 @@
|
|||||||
"error, registration is disabled": "Erreur, les inscriptions sont désactivées.",
|
"error, registration is disabled": "Erreur, les inscriptions sont désactivées.",
|
||||||
"file extension not allowed": "Extension de fichier non autorisée.",
|
"file extension not allowed": "Extension de fichier non autorisée.",
|
||||||
"file size is greater than the allowed size": "La taille du fichier est supérieure à la limite autorisée.",
|
"file size is greater than the allowed size": "La taille du fichier est supérieure à la limite autorisée.",
|
||||||
|
"gpx file is invalid": "Le fichier .gpx est invalide.",
|
||||||
"invalid credentials": "Identifiants invalides.",
|
"invalid credentials": "Identifiants invalides.",
|
||||||
"invalid payload": "Données fournies incorrectes.",
|
"invalid payload": "Données fournies incorrectes.",
|
||||||
"invalid token, please log in again": "Jeton de connexion invalide, merci de vous reconnecter.",
|
"invalid token, please log in again": "Jeton de connexion invalide, merci de vous reconnecter.",
|
||||||
@ -21,6 +23,7 @@
|
|||||||
"new email must be different than curent email": "La nouvelle addresse électronique doit être differente de l'adresse actuelle",
|
"new email must be different than curent email": "La nouvelle addresse électronique doit être differente de l'adresse actuelle",
|
||||||
"no file part": "Pas de fichier fourni.",
|
"no file part": "Pas de fichier fourni.",
|
||||||
"no selected file": "Pas de fichier sélectionné.",
|
"no selected file": "Pas de fichier sélectionné.",
|
||||||
|
"no tracks in gpx file": "Pas de trace (<trk>) dans le fichier .gpx",
|
||||||
"ongoing request exists": "Une demande d'exportation de données existe déjà.",
|
"ongoing request exists": "Une demande d'exportation de données existe déjà.",
|
||||||
"password: password and password confirmation do not match": "Mot de passe : les mots de passe saisis sont différents.",
|
"password: password and password confirmation do not match": "Mot de passe : les mots de passe saisis sont différents.",
|
||||||
"provide a valid auth token": "Merci de fournir un jeton de connexion valide.",
|
"provide a valid auth token": "Merci de fournir un jeton de connexion valide.",
|
||||||
|
Loading…
Reference in New Issue
Block a user