API & Client - return relevant errors when gpx file is invalid

This commit is contained in:
Sam 2023-04-11 15:58:05 +02:00
parent ba890d90b9
commit c4cec056b4
7 changed files with 106 additions and 7 deletions

View File

@ -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>'
) )

View File

@ -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(

View File

@ -1,6 +1,10 @@
from fittrackee.exceptions import GenericException from fittrackee.exceptions import GenericException
class InvalidGPXException(GenericException):
...
class WorkoutException(GenericException): class WorkoutException(GenericException):
... ...

View File

@ -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
""" """
gpx = open_gpx_file(gpx_file) try:
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

View File

@ -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)

View File

@ -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.",

View File

@ -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.",