From db8c9df33c27fc2e8e761029c3b72d9f929ffe9f Mon Sep 17 00:00:00 2001 From: Sam Date: Sun, 3 Apr 2022 11:42:03 +0200 Subject: [PATCH] API - fix user agent infos in emails after dependencies update (user-agent parsing has been removed in Werkzeug 2.1) --- fittrackee/__init__.py | 13 ++++++++++-- fittrackee/request.py | 24 +++++++++++++++++++++ fittrackee/tests/test_utils.py | 26 +++++++++++++++++++++++ fittrackee/tests/users/test_auth_api.py | 28 ++++++++++++------------- poetry.lock | 14 ++++++++++++- pyproject.toml | 1 + 6 files changed, 89 insertions(+), 17 deletions(-) create mode 100644 fittrackee/request.py diff --git a/fittrackee/__init__.py b/fittrackee/__init__.py index 5c0e722d..0928b89c 100644 --- a/fittrackee/__init__.py +++ b/fittrackee/__init__.py @@ -19,6 +19,7 @@ from flask_sqlalchemy import SQLAlchemy from sqlalchemy.exc import ProgrammingError from fittrackee.emails.email import EmailService +from fittrackee.request import CustomRequest VERSION = __version__ = '0.6.1' db = SQLAlchemy() @@ -35,9 +36,17 @@ logging.basicConfig( appLog = logging.getLogger('fittrackee') +class CustomFlask(Flask): + # add custom Request to handle user-agent parsing + # (removed in Werkzeug 2.1) + request_class = CustomRequest + + def create_app() -> Flask: # instantiate the app - app = Flask(__name__, static_folder='dist/static', template_folder='dist') + app = CustomFlask( + __name__, static_folder='dist/static', template_folder='dist' + ) # set config with app.app_context(): @@ -105,7 +114,7 @@ def create_app() -> Flask: appLog.setLevel(logging.DEBUG) # Enable CORS - @app.after_request + @app.after_request # type: ignore def after_request(response: Response) -> Response: response.headers.add('Access-Control-Allow-Origin', '*') response.headers.add( diff --git a/fittrackee/request.py b/fittrackee/request.py new file mode 100644 index 00000000..3b25d5bc --- /dev/null +++ b/fittrackee/request.py @@ -0,0 +1,24 @@ +from typing import Optional, Tuple + +from flask import Request +from ua_parser import user_agent_parser +from werkzeug.user_agent import UserAgent as IUserAgent + + +class UserAgent(IUserAgent): + def __init__(self, string: str): + super().__init__(string) + self.platform, self.browser = self._parse_user_agent(self.string) + + @staticmethod + def _parse_user_agent( + user_agent: str, + ) -> Tuple[Optional[str], Optional[str]]: + parsed_string = user_agent_parser.Parse(user_agent) + platform = parsed_string.get('os', {}).get('family') + browser = parsed_string.get('user_agent', {}).get('family') + return platform, browser + + +class CustomRequest(Request): + user_agent_class = UserAgent diff --git a/fittrackee/tests/test_utils.py b/fittrackee/tests/test_utils.py index b0a2069a..0b6f59fd 100644 --- a/fittrackee/tests/test_utils.py +++ b/fittrackee/tests/test_utils.py @@ -4,6 +4,7 @@ from uuid import uuid4 import pytest from fittrackee.files import display_readable_file_size +from fittrackee.request import UserAgent from fittrackee.utils import get_readable_duration @@ -42,3 +43,28 @@ class TestReadableDuration: readable_duration = get_readable_duration(30, locale) assert readable_duration == expected_duration + + +class TestParseUserAgent: + string = ( + 'Mozilla/5.0 (X11; Linux x86_64; rv:98.0) ' + 'Gecko/20100101 Firefox/98.0' + ) + + def test_it_returns_browser_name(self) -> None: + user_agent = UserAgent(self.string) + assert user_agent.browser == 'Firefox' + + def test_it_returns_other_as_brother_name_when_empty_string_provided( + self, + ) -> None: + user_agent = UserAgent('') + assert user_agent.browser == 'Other' + + def test_it_returns_operating_system(self) -> None: + user_agent = UserAgent(self.string) + assert user_agent.platform == 'Linux' + + def test_it_returns_other_as_os_when_empty_string_provided(self) -> None: + user_agent = UserAgent('') + assert user_agent.platform == 'Other' diff --git a/fittrackee/tests/users/test_auth_api.py b/fittrackee/tests/users/test_auth_api.py index fd4842ff..315dc875 100644 --- a/fittrackee/tests/users/test_auth_api.py +++ b/fittrackee/tests/users/test_auth_api.py @@ -285,8 +285,8 @@ class TestUserRegistration(ApiTestCaseMixin): { 'username': username, 'fittrackee_url': 'http://0.0.0.0:5000', - 'operating_system': 'linux', - 'browser_name': 'firefox', + 'operating_system': 'Linux', + 'browser_name': 'Firefox', 'account_confirmation_url': ( 'http://0.0.0.0:5000/account-confirmation' f'?token={expected_token}' @@ -807,8 +807,8 @@ class TestUserAccountUpdate(ApiTestCaseMixin): { 'username': user_1.username, 'fittrackee_url': 'http://0.0.0.0:5000', - 'operating_system': 'linux', - 'browser_name': 'firefox', + 'operating_system': 'Linux', + 'browser_name': 'Firefox', 'new_email_address': new_email, }, ) @@ -849,8 +849,8 @@ class TestUserAccountUpdate(ApiTestCaseMixin): { 'username': user_1.username, 'fittrackee_url': 'http://0.0.0.0:5000', - 'operating_system': 'linux', - 'browser_name': 'firefox', + 'operating_system': 'Linux', + 'browser_name': 'Firefox', 'email_confirmation_url': ( f'http://0.0.0.0:5000/email-update?token={expected_token}' ), @@ -1009,8 +1009,8 @@ class TestUserAccountUpdate(ApiTestCaseMixin): { 'username': user_1.username, 'fittrackee_url': 'http://0.0.0.0:5000', - 'operating_system': 'linux', - 'browser_name': 'firefox', + 'operating_system': 'Linux', + 'browser_name': 'Firefox', }, ) @@ -1690,8 +1690,8 @@ class TestPasswordResetRequest(ApiTestCaseMixin): f'http://0.0.0.0:5000/password-reset?token={token}' ), 'fittrackee_url': 'http://0.0.0.0:5000', - 'operating_system': 'linux', - 'browser_name': 'firefox', + 'operating_system': 'Linux', + 'browser_name': 'Firefox', }, ) @@ -1903,8 +1903,8 @@ class TestPasswordUpdate(ApiTestCaseMixin): { 'username': user_1.username, 'fittrackee_url': 'http://0.0.0.0:5000', - 'operating_system': 'linux', - 'browser_name': 'firefox', + 'operating_system': 'Linux', + 'browser_name': 'Firefox', }, ) @@ -2130,8 +2130,8 @@ class TestResendAccountConfirmationEmail(ApiTestCaseMixin): { 'username': inactive_user.username, 'fittrackee_url': 'http://0.0.0.0:5000', - 'operating_system': 'linux', - 'browser_name': 'firefox', + 'operating_system': 'Linux', + 'browser_name': 'Firefox', 'account_confirmation_url': ( 'http://0.0.0.0:5000/account-confirmation' f'?token={expected_token}' diff --git a/poetry.lock b/poetry.lock index 7adb6398..28d7079a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1315,6 +1315,14 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "ua-parser" +version = "0.10.0" +description = "Python port of Browserscope's user agent parser" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "urllib3" version = "1.26.9" @@ -1380,7 +1388,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "adc2a4d8457e207233c64b835fbfc1e38dbf49dc2d807657a80b53bd8a2c622d" +content-hash = "e130b306957d6577ecd21cc1a19daa81073d5bbe77ba9f6a60ff83d8af0a2f05" [metadata.files] alabaster = [ @@ -2193,6 +2201,10 @@ typing-extensions = [ {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, ] +ua-parser = [ + {file = "ua-parser-0.10.0.tar.gz", hash = "sha256:47b1782ed130d890018d983fac37c2a80799d9e0b9c532e734c67cf70f185033"}, + {file = "ua_parser-0.10.0-py2.py3-none-any.whl", hash = "sha256:46ab2e383c01dbd2ab284991b87d624a26a08f72da4d7d413f5bfab8b9036f8a"}, +] urllib3 = [ {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, diff --git a/pyproject.toml b/pyproject.toml index 1cdbe1d7..022a8bb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ shortuuid = "^1.0.8" staticmap = "^0.5.4" SQLAlchemy = "1.4.32" pyOpenSSL = "^22.0" +ua-parser = "^0.10.0" [tool.poetry.dev-dependencies] black = "^22.3"