From d067ed38bfdaf4bff471dd394da4a821729a09eb Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Thu, 2 Nov 2023 12:30:12 +0100 Subject: [PATCH 01/16] Change socket to http in uwsgi.ini We are trying to resolve issues with uWSGI running in the CB docker containers, which fail with the error `bind(): Address already in use [core/socket.c line 769]` See this discussion or some background: https://chatlogs.metabrainz.org/libera/metabrainz/2023-11-02/?msg=5243055&page=2 --- docker/uwsgi/uwsgi.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/uwsgi/uwsgi.ini b/docker/uwsgi/uwsgi.ini index a7c93ef3a..924808df6 100644 --- a/docker/uwsgi/uwsgi.ini +++ b/docker/uwsgi/uwsgi.ini @@ -1,6 +1,6 @@ [uwsgi] master = true -socket = 0.0.0.0:13032 +http = 0.0.0.0:13032 module = manage callable = application chdir = /code/ From 5c9d5fb44d0a33e801dba8f647cc245cc0ca2ec8 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Fri, 3 Nov 2023 00:55:32 +0530 Subject: [PATCH 02/16] Revert "Change socket to http in uwsgi.ini" This reverts commit 23574afc1338beb754218db125913cac80f58f53. --- docker/uwsgi/uwsgi.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/uwsgi/uwsgi.ini b/docker/uwsgi/uwsgi.ini index 924808df6..a7c93ef3a 100644 --- a/docker/uwsgi/uwsgi.ini +++ b/docker/uwsgi/uwsgi.ini @@ -1,6 +1,6 @@ [uwsgi] master = true -http = 0.0.0.0:13032 +socket = 0.0.0.0:13032 module = manage callable = application chdir = /code/ From f8f70b24357642437c0fcb0564d39e4fc34951ec Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Wed, 22 Nov 2023 01:09:38 +0530 Subject: [PATCH 03/16] Update uwsgi settings for clean shutdown The default uwsgi config doesn't play well with consul-template. Add exit-on-reload for clean shutdown. The rest of changes help make startup more robust. --- docker/uwsgi/uwsgi.ini | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docker/uwsgi/uwsgi.ini b/docker/uwsgi/uwsgi.ini index a7c93ef3a..36ab3757b 100644 --- a/docker/uwsgi/uwsgi.ini +++ b/docker/uwsgi/uwsgi.ini @@ -1,4 +1,6 @@ [uwsgi] +uid = www-data +gid = www-data master = true socket = 0.0.0.0:13032 module = manage @@ -9,3 +11,7 @@ processes = 20 disable-logging = true ; increase buffer size for requests that send a lot of mbids in query params buffer-size = 8192 +; when uwsgi gets a sighup, quit completely and let runit restart us +exit-on-reload = true +need-app = true +log-x-forwarded-for=true From 71d724c881b362f351987dd44af9a6c37f2babb7 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Mon, 27 Nov 2023 18:42:07 +0530 Subject: [PATCH 04/16] Upgrade base python image To fix uWSGI and consul template errors, we need a newer version of consul template. So upgrade the base python image which comes with the newer version of consul template. Also, need to upgrade python to 3.11 and uWSGI to a compatible version as a result. --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index ea985663a..4a88d194c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM metabrainz/python:3.10-20220315 as critiquebrainz-base +FROM metabrainz/python:3.11-20231006 as critiquebrainz-base ENV PYTHONUNBUFFERED 1 @@ -39,7 +39,7 @@ RUN mkdir -p /etc/apt/keyrings \ RUN pip install --upgrade pip==21.0.1 -RUN pip install --no-cache-dir uWSGI==2.0.20 +RUN pip install --no-cache-dir uWSGI==2.0.23 RUN mkdir /code WORKDIR /code From 6f0a362829c13908039a0d621417002daec64372 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Mon, 27 Nov 2023 17:23:19 +0530 Subject: [PATCH 05/16] Improve uWSGI and consul-template interactions on reloading We fixed the `bind(): Address already in use [core/socket.c line 769]` error in LB using `exit-on-reload = true` in uWSGI configuration. However, this prevents graceful reloading. I did some further investigation as noted in metabrainz/artwork-redirect#46 and have a better fix in mind now. For the command field in exec block of consul template configuration, use array format. This executes the command directly without spawning a shell wrapper or setting a process group id. Both of which will interfere with forwarding signals to the uWSGI process. --- docker/uwsgi/consul-template-uwsgi.conf | 2 +- docker/uwsgi/uwsgi.ini | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docker/uwsgi/consul-template-uwsgi.conf b/docker/uwsgi/consul-template-uwsgi.conf index 412a0b3dd..b556ff551 100644 --- a/docker/uwsgi/consul-template-uwsgi.conf +++ b/docker/uwsgi/consul-template-uwsgi.conf @@ -3,7 +3,7 @@ template { destination = "/code/consul_config.py" } exec { - command = "uwsgi --die-on-term /etc/uwsgi/uwsgi.ini" + command = ["uwsgi", "/etc/uwsgi/uwsgi.ini"] splay = "60s" reload_signal = "SIGHUP" kill_signal = "SIGTERM" diff --git a/docker/uwsgi/uwsgi.ini b/docker/uwsgi/uwsgi.ini index 36ab3757b..d26176cc7 100644 --- a/docker/uwsgi/uwsgi.ini +++ b/docker/uwsgi/uwsgi.ini @@ -11,7 +11,6 @@ processes = 20 disable-logging = true ; increase buffer size for requests that send a lot of mbids in query params buffer-size = 8192 -; when uwsgi gets a sighup, quit completely and let runit restart us -exit-on-reload = true need-app = true -log-x-forwarded-for=true +log-x-forwarded-for = true +die-on-term = true From 4142620814a4bbfa7aa8c185ea20e2566266fae7 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Thu, 30 Nov 2023 02:34:01 +0530 Subject: [PATCH 06/16] Make develop.sh and test.sh compatible with compose v2 See https://github.com/metabrainz/listenbrainz-server/pull/2184. --- develop.sh | 11 +++++++-- test.sh | 66 ++++++++++++++++++++++++++++-------------------------- 2 files changed, 43 insertions(+), 34 deletions(-) diff --git a/develop.sh b/develop.sh index 0d17c8596..a2dad4ace 100755 --- a/develop.sh +++ b/develop.sh @@ -7,14 +7,21 @@ if [[ ! -d "docker" ]]; then exit -1 fi +echo "Checking docker compose version" +if docker compose version &> /dev/null; then + DOCKER_COMPOSE_CMD="docker compose" +else + DOCKER_COMPOSE_CMD="docker-compose" +fi + function invoke_docker_compose { - exec docker-compose -f docker/docker-compose.dev.yml \ + exec $DOCKER_COMPOSE_CMD -f docker/docker-compose.dev.yml \ -p critiquebrainz \ "$@" } function invoke_docker_compose_test { - exec docker-compose -f docker/docker-compose.test.yml \ + exec $DOCKER_COMPOSE_CMD -f docker/docker-compose.test.yml \ -p critiquebrainz_test \ "$@" } diff --git a/test.sh b/test.sh index 902ba53d5..fddc5acec 100755 --- a/test.sh +++ b/test.sh @@ -17,40 +17,47 @@ if [[ ! -d "docker" ]]; then exit -1 fi +echo "Checking docker compose version" +if docker compose version &> /dev/null; then + DOCKER_COMPOSE_CMD="docker compose" +else + DOCKER_COMPOSE_CMD="docker-compose" +fi + +function invoke_docker_compose_cmd { + $DOCKER_COMPOSE_CMD \ + -f ${COMPOSE_FILE_LOC} \ + -p ${COMPOSE_PROJECT_NAME} \ + "$@" +} + function build_containers { - docker-compose -f ${COMPOSE_FILE_LOC} \ - -p ${COMPOSE_PROJECT_NAME} \ - build critiquebrainz + invoke_docker_compose_cmd build critiquebrainz } function bring_up_db { - docker-compose -f ${COMPOSE_FILE_LOC} \ - -p ${COMPOSE_PROJECT_NAME} \ - up -d db musicbrainz_db critiquebrainz_redis + invoke_docker_compose_cmd up -d db musicbrainz_db critiquebrainz_redis } function setup { echo "Running setup" # PostgreSQL Database initialization - docker-compose -f ${COMPOSE_FILE_LOC} \ - -p ${COMPOSE_PROJECT_NAME} \ - run --rm critiquebrainz dockerize \ - -wait tcp://db:5432 -timeout 60s \ - bash -c "python3 manage.py init_db" + invoke_docker_compose_cmd \ + run --rm critiquebrainz dockerize \ + -wait tcp://db:5432 -timeout 60s \ + bash -c "python3 manage.py init_db" - docker-compose -f ${COMPOSE_FILE_LOC} \ - -p ${COMPOSE_PROJECT_NAME} \ - run --rm critiquebrainz bash scripts/download-import-bookbrainz-dump.sh + invoke_docker_compose_cmd \ + run --rm critiquebrainz bash scripts/download-import-bookbrainz-dump.sh - docker-compose -f ${COMPOSE_FILE_LOC} \ - -p ${COMPOSE_PROJECT_NAME} \ - run --rm critiquebrainz bash scripts/add-test-bookbrainz-data.sh + invoke_docker_compose_cmd \ + run --rm critiquebrainz bash scripts/add-test-bookbrainz-data.sh } function is_db_running { # Check if the database container is running - containername="${COMPOSE_PROJECT_NAME}_db_1" + containername="${COMPOSE_PROJECT_NAME}-db-1" res=`docker ps --filter "name=$containername" --filter "status=running" -q` if [[ -n "$res" ]]; then return 0 @@ -60,7 +67,7 @@ function is_db_running { } function is_db_exists { - containername="${COMPOSE_PROJECT_NAME}_db_1" + containername="${COMPOSE_PROJECT_NAME}-db-1" res=`docker ps --filter "name=$containername" --filter "status=exited" -q` if [[ -n "$res" ]]; then return 0 @@ -71,26 +78,21 @@ function is_db_exists { function dc_stop { # Stopping all unit test containers associated with this project - docker-compose -f ${COMPOSE_FILE_LOC} \ - -p ${COMPOSE_PROJECT_NAME} \ - stop + invoke_docker_compose_cmd stop } function dc_down { # Shutting down all unit test containers associated with this project - docker-compose -f ${COMPOSE_FILE_LOC} \ - -p ${COMPOSE_PROJECT_NAME} \ - down + invoke_docker_compose_cmd down } function run_tests { echo "Running tests" - docker-compose -f ${COMPOSE_FILE_LOC} \ - -p ${COMPOSE_PROJECT_NAME} \ - run --rm critiquebrainz \ - dockerize -wait tcp://db:5432 -timeout 60s \ - dockerize -wait tcp://musicbrainz_db:5432 -timeout 600s \ - pytest --junitxml=reports/tests.xml "$@" + invoke_docker_compose_cmd \ + run --rm critiquebrainz \ + dockerize -wait tcp://db:5432 -timeout 60s \ + dockerize -wait tcp://musicbrainz_db:5432 -timeout 600s \ + pytest --junitxml=reports/tests.xml "$@" } @@ -102,7 +104,7 @@ if [[ "$1" == "-s" ]]; then fi if [[ "$1" == "-d" ]]; then - echo "Running docker-compose down" + echo "Running $DOCKER_COMPOSE_CMD down" dc_down exit 0 fi From 158d64c8c09476ccb02025711ec378fd6010009f Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Fri, 1 Dec 2023 02:06:45 +0530 Subject: [PATCH 07/16] Test Improvements Remove unmaintained flask testing library and improve tests speed by clearing tables instead of dropping and recreating them between each test. These changes also help in upgrading the other flask dependencies. --- admin/sql/clear_tables.sql | 13 ++ critiquebrainz/data/testing.py | 6 +- critiquebrainz/data/utils.py | 4 + critiquebrainz/db/users.py | 3 +- critiquebrainz/frontend/external/__init__.py | 2 + critiquebrainz/frontend/testing.py | 35 ++-- critiquebrainz/frontend/views/review.py | 1 - .../frontend/views/test/test_artist.py | 3 +- .../frontend/views/test/test_oauth.py | 1 + .../frontend/views/test/test_review.py | 2 +- .../frontend/views/test/test_user.py | 4 +- critiquebrainz/testing.py | 153 ++++++++++++++++++ critiquebrainz/ws/__init__.py | 3 + critiquebrainz/ws/testing.py | 26 +-- requirements.txt | 1 - 15 files changed, 202 insertions(+), 55 deletions(-) create mode 100644 admin/sql/clear_tables.sql create mode 100644 critiquebrainz/testing.py diff --git a/admin/sql/clear_tables.sql b/admin/sql/clear_tables.sql new file mode 100644 index 000000000..582ed6d09 --- /dev/null +++ b/admin/sql/clear_tables.sql @@ -0,0 +1,13 @@ +DELETE FROM comment_revision; +DELETE FROM comment; +DELETE FROM vote; +DELETE FROM spam_report; +DELETE FROM revision; +DELETE FROM oauth_grant; +DELETE FROM oauth_token; +DELETE FROM oauth_client; +DELETE FROM moderation_log; +DELETE FROM review; +DELETE FROM "user"; +DELETE FROM license; +DELETE FROM avg_rating; diff --git a/critiquebrainz/data/testing.py b/critiquebrainz/data/testing.py index 027e57d3c..b4d7c742a 100644 --- a/critiquebrainz/data/testing.py +++ b/critiquebrainz/data/testing.py @@ -2,7 +2,7 @@ from flask_testing import TestCase -from critiquebrainz.data.utils import create_all, drop_tables, drop_types +from critiquebrainz.data.utils import create_all, drop_tables, drop_types, clear_tables from critiquebrainz.frontend import create_app @@ -21,6 +21,4 @@ def tearDown(self): @staticmethod def reset_db(): - drop_tables() - drop_types() - create_all() + clear_tables() diff --git a/critiquebrainz/data/utils.py b/critiquebrainz/data/utils.py index 4d36a8104..1924baad1 100644 --- a/critiquebrainz/data/utils.py +++ b/critiquebrainz/data/utils.py @@ -30,6 +30,10 @@ def drop_types(): db.run_sql_script(os.path.join(ADMIN_SQL_DIR, 'drop_types.sql')) +def clear_tables(): + db.run_sql_script(os.path.join(ADMIN_SQL_DIR, 'clear_tables.sql')) + + def explode_db_uri(uri): """Extracts database connection info from the URI. diff --git a/critiquebrainz/db/users.py b/critiquebrainz/db/users.py index f6e7a9996..e0e2dc315 100644 --- a/critiquebrainz/db/users.py +++ b/critiquebrainz/db/users.py @@ -4,7 +4,6 @@ import sqlalchemy from critiquebrainz import db -from critiquebrainz.db import revision as db_revision USER_GET_COLUMNS = [ @@ -305,6 +304,7 @@ def list_users(limit=None, offset=0): result = connection.execute(sqlalchemy.text(""" SELECT {columns} FROM "user" + ORDER BY musicbrainz_row_id LIMIT :limit OFFSET :offset """.format(columns=','.join(USER_GET_COLUMNS))), { @@ -357,6 +357,7 @@ def has_voted(user_id, review_id): Returns: (bool): True if has voted else False. """ + from critiquebrainz.db import revision as db_revision last_revision = db_revision.get(review_id, limit=1)[0] with db.engine.connect() as connection: result = connection.execute(sqlalchemy.text(""" diff --git a/critiquebrainz/frontend/external/__init__.py b/critiquebrainz/frontend/external/__init__.py index e401f7319..5ea2d1124 100644 --- a/critiquebrainz/frontend/external/__init__.py +++ b/critiquebrainz/frontend/external/__init__.py @@ -10,6 +10,7 @@ from critiquebrainz.frontend.external.entities import get_entity_by_id, get_multiple_entities + class MBDataAccess(object): """A data access object which switches between database get methods or development versions This is useful because we won't show a review if we cannot find its metadata in the @@ -51,6 +52,7 @@ def get_multiple_entities(self): ctx.get_multiple_entities = self.get_multiple_entities_method return ctx.get_multiple_entities + mbstore = MBDataAccess() diff --git a/critiquebrainz/frontend/testing.py b/critiquebrainz/frontend/testing.py index a91a9688c..e0f482880 100644 --- a/critiquebrainz/frontend/testing.py +++ b/critiquebrainz/frontend/testing.py @@ -1,34 +1,17 @@ import os -from critiquebrainz.ws.oauth import oauth - -from flask_testing import TestCase -from critiquebrainz.data.utils import create_all, drop_tables, drop_types from critiquebrainz.frontend import create_app +from critiquebrainz.testing import ServerTestCase +from critiquebrainz.ws.oauth import oauth -class FrontendTestCase(TestCase): +class FrontendTestCase(ServerTestCase): - def create_app(self): - app = create_app(config_path=os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', '..', 'test_config.py')) + @classmethod + def create_app(cls): + app = create_app( + config_path=os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', '..', 'test_config.py') + ) oauth.init_app(app) + app.config['TESTING'] = True return app - - def setUp(self): - self.reset_db() - # TODO(roman): Add stuff form fixtures. - - def tearDown(self): - pass - - @staticmethod - def reset_db(): - drop_tables() - drop_types() - create_all() - - def temporary_login(self, user): - """Based on: http://stackoverflow.com/a/16238537.""" - with self.client.session_transaction() as session: - session['_user_id'] = user.id - session['_fresh'] = True diff --git a/critiquebrainz/frontend/views/review.py b/critiquebrainz/frontend/views/review.py index 6440376b4..60fa0aa70 100644 --- a/critiquebrainz/frontend/views/review.py +++ b/critiquebrainz/frontend/views/review.py @@ -1,6 +1,5 @@ from math import ceil -from brainzutils.musicbrainz_db.exceptions import NoDataFoundException from flask import Blueprint, render_template, request, redirect, url_for, jsonify from flask_babel import gettext, get_locale, lazy_gettext from flask_login import login_required, current_user diff --git a/critiquebrainz/frontend/views/test/test_artist.py b/critiquebrainz/frontend/views/test/test_artist.py index 794192baf..c293d96b7 100644 --- a/critiquebrainz/frontend/views/test/test_artist.py +++ b/critiquebrainz/frontend/views/test/test_artist.py @@ -1,9 +1,9 @@ from unittest import mock -from critiquebrainz.db.user import User from critiquebrainz.frontend.testing import FrontendTestCase from critiquebrainz.db import users as db_users + def return_release_groups(*, artist_id, release_types=None, limit=None, offset=None): # pylint: disable=unused-argument if release_types == ['ep']: @@ -30,6 +30,7 @@ def return_release_groups(*, artist_id, release_types=None, limit=None, offset=N class ArtistViewsTestCase(FrontendTestCase): def setUp(self): + from critiquebrainz.db.user import User super(ArtistViewsTestCase, self).setUp() self.reviewer = User(db_users.get_or_create( 1, "Reviewer", new_user_data={"display_name": u"Reviewer"} diff --git a/critiquebrainz/frontend/views/test/test_oauth.py b/critiquebrainz/frontend/views/test/test_oauth.py index 998d47d0f..e12e91be8 100644 --- a/critiquebrainz/frontend/views/test/test_oauth.py +++ b/critiquebrainz/frontend/views/test/test_oauth.py @@ -9,6 +9,7 @@ class OauthTestCase(FrontendTestCase): def setUp(self): + super().setUp() from critiquebrainz.db.user import User self.user = User(db_users.get_or_create(2, "9371e5c7-5995-4471-a5a9-33481f897f9c", new_user_data={ "display_name": u"User", diff --git a/critiquebrainz/frontend/views/test/test_review.py b/critiquebrainz/frontend/views/test/test_review.py index 3c793b6bd..c7ab6abc0 100644 --- a/critiquebrainz/frontend/views/test/test_review.py +++ b/critiquebrainz/frontend/views/test/test_review.py @@ -2,7 +2,7 @@ from unittest.mock import patch from urllib.parse import urlparse -from flask import current_app, url_for +from flask import url_for import critiquebrainz.db.license as db_license import critiquebrainz.db.review as db_review diff --git a/critiquebrainz/frontend/views/test/test_user.py b/critiquebrainz/frontend/views/test/test_user.py index 901eb6310..91452b9df 100644 --- a/critiquebrainz/frontend/views/test/test_user.py +++ b/critiquebrainz/frontend/views/test/test_user.py @@ -12,7 +12,9 @@ def setUp(self): self.user = User(db_users.get_or_create(1, "Tester", new_user_data={ "display_name": u"Tester", })) - self.hacker = User(db_users.create(musicbrainz_row_id = 2, display_name = u"Hacker")) + self.hacker = User(db_users.get_or_create(2, "Hacker", new_user_data={ + "display_name": u"Hacker", + })) self.admin = User(db_users.get_or_create(3, "Admin", new_user_data={ "display_name": u"Admin", })) diff --git a/critiquebrainz/testing.py b/critiquebrainz/testing.py new file mode 100644 index 000000000..d1b6a5225 --- /dev/null +++ b/critiquebrainz/testing.py @@ -0,0 +1,153 @@ +import unittest +from urllib import parse + +from flask import template_rendered, message_flashed, g + +from critiquebrainz.data.utils import create_all, drop_tables, drop_types, clear_tables + + +class ServerTestCase(unittest.TestCase): + """ TestCase for Flask App tests, most of the code here has been borrowed from flask_testing.TestCase + (https://github.com/jarus/flask-testing/blob/5107691011fa891835c01547e73e991c484fa07f/flask_testing/utils.py#L118). + A key difference is that flask_testing's TestCase creates the flask app for each test method whereas + our ServerTestCase creates it once per class. flask_testing's test case allows also for custom Response mixins + and other stuff to be compatible across flask versions. Since, we don't need that the implementation can be + simplified. + + Notably, the implementation of the setUpClass, setUp, tearDown, tearDownClass, assertMessageFlashed, + assertTemplateUsed has been adapted to use class level variables. The implementation of assertRedirects has been + modified to fix concerning absolute and relative urls in Location header. + """ + + def reset_db(self): + clear_tables() + + def temporary_login(self, user): + # flask-login stores the logged in user in the global g which lasts for the entire duration of a test + # (a test request context is pushed in setUp and popped in teardown). therefore, we need to remove + # the field manually for if multiple logins are needed in a test. + if hasattr(g, "_login_user"): + del g._login_user + with self.client.session_transaction() as session: + session['_user_id'] = user.id + session['_fresh'] = True + + @classmethod + def setUpClass(cls): + cls.app = cls.create_app() + cls.client = cls.app.test_client() + + template_rendered.connect(cls._set_template) + message_flashed.connect(cls._add_flash_message) + + def setUp(self) -> None: + self._ctx = self.app.test_request_context() + self._ctx.push() + + ServerTestCase.template = None + ServerTestCase.flashed_messages = [] + + self.reset_db() + + @classmethod + def _add_flash_message(cls, app, message, category): + ServerTestCase.flashed_messages.append((message, category)) + + @classmethod + def _set_template(cls, app, template, context): + ServerTestCase.template = (template, context) + + def tearDown(self): + self._ctx.pop() + del self._ctx + + @classmethod + def tearDownClass(cls): + template_rendered.disconnect(cls._set_template) + message_flashed.disconnect(cls._add_flash_message) + del cls.client + del cls.app + + def assertMessageFlashed(self, message, category='message'): + """ + Checks if a given message was flashed. + Only works if your version of Flask has message_flashed + signal support (0.10+) and blinker is installed. + :param message: expected message + :param category: expected message category + """ + for _message, _category in ServerTestCase.flashed_messages: + if _message == message and _category == category: + return True + + raise AssertionError("Message '%s' in category '%s' wasn't flashed" % (message, category)) + + def assertTemplateUsed(self, name): + """ + Checks if a given template is used in the request. + Only works if your version of Flask has signals + support (0.6+) and blinker is installed. + If the template engine used is not Jinja2, provide + ``tmpl_name_attribute`` with a value of its `Template` + class attribute name which contains the provided ``name`` value. + :versionadded: 0.2 + :param name: template name + """ + if ServerTestCase.template is None: + self.fail("No template used") + used_template = ServerTestCase.template[0].name + self.assertEqual(used_template, name, f"Template {name} not used. Template used: {used_template}") + + def get_context_variable(self, name): + if ServerTestCase.template is None: + self.fail("No template used") + context = ServerTestCase.template[1] + if name in context: + return context[name] + raise ValueError() + + def assertContext(self, name, value, message=None): + try: + self.assertEqual(self.get_context_variable(name), value, message) + except ValueError: + self.fail(message or "Context variable does not exist: %s" % name) + + assert_context = assertContext + + def assertRedirects(self, response, location, message=None, permanent=False): + if permanent: + valid_status_codes = (301, 308) + else: + valid_status_codes = (301, 302, 303, 305, 307, 308) + + valid_status_code_str = ', '.join(str(code) for code in valid_status_codes) + not_redirect = f"HTTP Status {valid_status_code_str} expected but got {response.status_code}" + + self.assertIn(response.status_code, valid_status_codes, message or not_redirect) + + response_location = parse.unquote(response.location) + location_mismatch = f"Expected redirect location {location} but got {response_location}" + self.assertTrue(response_location.endswith(location), message or location_mismatch) + + def assertStatus(self, response, status_code, message=None): + message = message or 'HTTP Status %s expected but got %s' \ + % (status_code, response.status_code) + self.assertEqual(response.status_code, status_code, message) + + def assert200(self, response, message=None): + self.assertStatus(response, 200, message) + + def assert400(self, response, message=None): + self.assertStatus(response, 400, message) + + def assert401(self, response, message=None): + self.assertStatus(response, 401, message) + + def assert403(self, response, message=None): + self.assertStatus(response, 403, message) + + def assert404(self, response, message=None): + self.assertStatus(response, 404, message) + + def assert500(self, response, message=None): + self.assertStatus(response, 500, message) \ No newline at end of file diff --git a/critiquebrainz/ws/__init__.py b/critiquebrainz/ws/__init__.py index a91ca10eb..1d1843543 100644 --- a/critiquebrainz/ws/__init__.py +++ b/critiquebrainz/ws/__init__.py @@ -86,6 +86,9 @@ def create_app(debug=None, config_path=None): from critiquebrainz.frontend import babel babel.init_app(app, domain='cb_webservice') + from critiquebrainz.frontend.external import mbstore + mbstore.init_app(app) + # OAuth from critiquebrainz.ws.oauth import oauth oauth.init_app(app) diff --git a/critiquebrainz/ws/testing.py b/critiquebrainz/ws/testing.py index 96b7045f6..adfff932e 100644 --- a/critiquebrainz/ws/testing.py +++ b/critiquebrainz/ws/testing.py @@ -1,34 +1,22 @@ import os -from flask_testing import TestCase - import critiquebrainz.db.oauth_client as db_oauth_client import critiquebrainz.db.users as db_users -from critiquebrainz.data.utils import create_all, drop_tables, drop_types +from critiquebrainz.testing import ServerTestCase from critiquebrainz.ws import create_app from critiquebrainz.ws.oauth import oauth -class WebServiceTestCase(TestCase): +class WebServiceTestCase(ServerTestCase): - def create_app(self): - app = create_app(config_path=os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', '..', 'test_config.py')) + @classmethod + def create_app(cls): + app = create_app( + config_path=os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', '..', 'test_config.py') + ) oauth.init_app(app) return app - def setUp(self): - self.reset_db() - # TODO(roman): Add stuff form fixtures. - - def tearDown(self): - pass - - @staticmethod - def reset_db(): - drop_tables() - drop_types() - create_all() - @staticmethod def create_dummy_client(user): db_oauth_client.create( diff --git a/requirements.txt b/requirements.txt index 968aa2a50..622058f3c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,6 @@ click==8.1.3 Flask-Babel==2.0.0 Flask-Login==0.6.0 Flask-SQLAlchemy==2.5.1 -Flask-Testing==0.8.1 Flask-WTF==1.0.1 Markdown==3.3.6 bleach==5.0.1 From 75e193dda027568ef7bf178da4ff2565e0b0dea1 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Fri, 1 Dec 2023 02:28:37 +0530 Subject: [PATCH 08/16] Test Improvements - 2 Remove unmaintained flask testing library and improve tests speed by clearing tables instead of dropping and recreating them between each test. These changes also help in upgrading the other flask dependencies. --- critiquebrainz/data/testing.py | 25 +++---------------- .../frontend/views/test/test_bb_author.py | 1 + 2 files changed, 4 insertions(+), 22 deletions(-) diff --git a/critiquebrainz/data/testing.py b/critiquebrainz/data/testing.py index b4d7c742a..633eb4d6b 100644 --- a/critiquebrainz/data/testing.py +++ b/critiquebrainz/data/testing.py @@ -1,24 +1,5 @@ -import os +from critiquebrainz.frontend.testing import FrontendTestCase -from flask_testing import TestCase -from critiquebrainz.data.utils import create_all, drop_tables, drop_types, clear_tables -from critiquebrainz.frontend import create_app - - -class DataTestCase(TestCase): - - def create_app(self): - app = create_app(config_path=os.path.join(os.path.dirname(os.path.realpath(__file__)), '..', '..', 'test_config.py')) - return app - - def setUp(self): - self.reset_db() - # TODO(roman): Add stuff form fixtures. - - def tearDown(self): - pass - - @staticmethod - def reset_db(): - clear_tables() +class DataTestCase(FrontendTestCase): + pass diff --git a/critiquebrainz/frontend/views/test/test_bb_author.py b/critiquebrainz/frontend/views/test/test_bb_author.py index c8e61a351..7dd487ceb 100644 --- a/critiquebrainz/frontend/views/test/test_bb_author.py +++ b/critiquebrainz/frontend/views/test/test_bb_author.py @@ -6,6 +6,7 @@ from critiquebrainz.db.user import User from critiquebrainz.frontend.testing import FrontendTestCase + class AuthorViewsTestCase(FrontendTestCase): def setUp(self): From 6963e8dbde1b4bb243ade7957b5a0a1b7a89ac31 Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Wed, 29 Nov 2023 03:05:22 +0530 Subject: [PATCH 09/16] Upgrade Flask related dependencies regular maintenance and resolving security alerts. --- critiquebrainz/data/dump_manager.py | 22 +++++++------------ critiquebrainz/frontend/babel.py | 6 ++--- critiquebrainz/frontend/external/__init__.py | 21 ++++++++---------- critiquebrainz/frontend/forms/review.py | 4 ++-- critiquebrainz/frontend/views/profile.py | 2 +- .../frontend/views/test/test_comment.py | 7 ++++++ .../frontend/views/test/test_profile.py | 8 +++++++ requirements.txt | 19 ++++++++-------- 8 files changed, 48 insertions(+), 41 deletions(-) diff --git a/critiquebrainz/data/dump_manager.py b/critiquebrainz/data/dump_manager.py index 974bbd7d2..6c54412c1 100644 --- a/critiquebrainz/data/dump_manager.py +++ b/critiquebrainz/data/dump_manager.py @@ -9,9 +9,10 @@ from time import gmtime, strftime import click +import orjson import sqlalchemy from flask import current_app, jsonify -from flask.json import JSONEncoder +from flask.json.provider import JSONProvider from psycopg2.sql import SQL, Identifier from critiquebrainz import db @@ -147,7 +148,7 @@ def json(location, rotate=False): """ create_path(location) - current_app.json_encoder = DumpJSONEncoder + current_app.json_encoder = OrJSONProvider print("Creating new archives...") with db.engine.begin() as connection: @@ -541,16 +542,9 @@ def reset_sequence(table_names): connection.close() -class DumpJSONEncoder(JSONEncoder): - """Custom JSON encoder for database dumps.""" +class OrJSONProvider(JSONProvider): + def dumps(self, obj, **kwargs): + return orjson.dumps(obj).decode() - def default(self, o): # pylint: disable=method-hidden - try: - if isinstance(o, datetime): - return o.isoformat() - iterable = iter(o) - except TypeError: - pass - else: - return list(iterable) - return JSONEncoder.default(self, o) + def loads(self, s, **kwargs): + return orjson.loads(s) diff --git a/critiquebrainz/frontend/babel.py b/critiquebrainz/frontend/babel.py index d1d36604a..79e7b8d9d 100644 --- a/critiquebrainz/frontend/babel.py +++ b/critiquebrainz/frontend/babel.py @@ -3,8 +3,6 @@ def init_app(app, domain='messages'): - babel = Babel(app, default_domain=domain) - app.config['LANGUAGES'] = {} for language in app.config['SUPPORTED_LANGUAGES']: app.config['LANGUAGES'][language] = Locale.parse(language).language_name @@ -21,7 +19,6 @@ def after_this_request(f): g.after_request_callbacks.append(f) return f - @babel.localeselector def get_locale(): # pylint: disable=unused-variable supported_languages = app.config['SUPPORTED_LANGUAGES'] language_arg = request.args.get('l') @@ -38,3 +35,6 @@ def remember_language(response): # pylint: disable=unused-variable return language_cookie return request.accept_languages.best_match(supported_languages) + + babel = Babel() + babel.init_app(app, default_domain=domain, locale_selector=get_locale) diff --git a/critiquebrainz/frontend/external/__init__.py b/critiquebrainz/frontend/external/__init__.py index 5ea2d1124..a46502b42 100644 --- a/critiquebrainz/frontend/external/__init__.py +++ b/critiquebrainz/frontend/external/__init__.py @@ -6,8 +6,7 @@ See documentation in each module for information about usage. """ -from flask import current_app, _app_ctx_stack - +from flask import current_app, g from critiquebrainz.frontend.external.entities import get_entity_by_id, get_multiple_entities @@ -38,19 +37,16 @@ def init_app(self, app): @property def get_entity_by_id(self): - ctx = _app_ctx_stack.top - if ctx is not None: - if not hasattr(ctx, 'get_entity_by_id'): - ctx.get_entity_by_id = self.get_entity_by_id_method - return ctx.get_entity_by_id + if not hasattr(g, 'get_entity_by_id'): + g.get_entity_by_id = self.get_entity_by_id_method + return g.get_entity_by_id @property def get_multiple_entities(self): - ctx = _app_ctx_stack.top - if ctx is not None: - if not hasattr(ctx, 'get_multiple_entities'): - ctx.get_multiple_entities = self.get_multiple_entities_method - return ctx.get_multiple_entities + if not hasattr(g, 'get_multiple_entities'): + g.get_multiple_entities = self.get_multiple_entities_method + return g.get_multiple_entities + mbstore = MBDataAccess() @@ -67,6 +63,7 @@ def development_get_multiple_entities(entities): data[mbid] = get_dummy_item(mbid, entity_type) return data + def development_get_entity_by_id(entity_id, entity_type): """Same as get_entity_by_id, but always returns a dummy item if the requested entity isn't in the MusicBrainz database. Used in development with a sample database.""" diff --git a/critiquebrainz/frontend/forms/review.py b/critiquebrainz/frontend/forms/review.py index d84c621f9..64592ff28 100644 --- a/critiquebrainz/frontend/forms/review.py +++ b/critiquebrainz/frontend/forms/review.py @@ -47,8 +47,8 @@ def __init__(self, default_license_id='CC BY-SA 3.0', default_language='en', **k kwargs.setdefault('language', default_language) FlaskForm.__init__(self, **kwargs) - def validate(self): - if not super(ReviewEditForm, self).validate(): + def validate(self, extra_validators=None): + if not super(ReviewEditForm, self).validate(extra_validators): return False if not self.text.data and not self.rating.data: self.text.errors.append("You must provide some text or a rating to complete this review.") diff --git a/critiquebrainz/frontend/views/profile.py b/critiquebrainz/frontend/views/profile.py index 7b0bcd06b..5437455a1 100644 --- a/critiquebrainz/frontend/views/profile.py +++ b/critiquebrainz/frontend/views/profile.py @@ -20,7 +20,7 @@ def edit(): "license_choice": form.license_choice.data, }) flash.success(gettext("Profile updated.")) - return redirect(url_for('user.reviews', user_ref= current_user.user_ref)) + return redirect(url_for('user.reviews', user_ref=current_user.user_ref)) form.display_name.data = current_user.display_name form.email.data = current_user.email diff --git a/critiquebrainz/frontend/views/test/test_comment.py b/critiquebrainz/frontend/views/test/test_comment.py index fb28b2eca..ead1d469a 100644 --- a/critiquebrainz/frontend/views/test/test_comment.py +++ b/critiquebrainz/frontend/views/test/test_comment.py @@ -96,13 +96,20 @@ def test_create(self): # blocked user should not be allowed to comment db_users.block(self.commenter.id) + # because the g global persists for entire duration of the test, we need to manually logout/login + # the user again for the current_user to be refreshed. in production, this would happen automatically + # as the current_user is loaded at the start of each request/request context. + self.temporary_login(self.commenter) + response = self.client.post( url_for("comment.create"), data=payload, follow_redirects=True, ) self.assertIn("You are not allowed to write new comments", str(response.data)) + db_users.unblock(self.commenter.id) + self.temporary_login(self.commenter) # comment with some text and a valid review_id must be saved response = self.client.post( diff --git a/critiquebrainz/frontend/views/test/test_profile.py b/critiquebrainz/frontend/views/test/test_profile.py index 263902cbf..0a888b33d 100644 --- a/critiquebrainz/frontend/views/test/test_profile.py +++ b/critiquebrainz/frontend/views/test/test_profile.py @@ -31,6 +31,14 @@ def test_edit(self): response = self.client.post('/profile/edit', data=data, query_string=data, follow_redirects=True) self.assert200(response) + + # because the g global persists for entire duration of the test, we need to manually logout/login + # the user again for the current_user to be refreshed. in production, this would happen automatically + # as the current_user is loaded at the start of each request/request context. + self.temporary_login(self.user) + + response = self.client.post(f'/user/{self.user.id}') + self.assert200(response) self.assertIn(data['display_name'], str(response.data)) def test_delete(self): diff --git a/requirements.txt b/requirements.txt index 622058f3c..252ba8aa6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ brainzutils@git+https://github.com/metabrainz/brainzutils-python.git@v2.7.0 beautifulsoup4==4.8.0 click==8.1.3 -Flask-Babel==2.0.0 -Flask-Login==0.6.0 +Flask-Babel==4.0.0 +Flask-Login==0.6.3 Flask-SQLAlchemy==2.5.1 -Flask-WTF==1.0.1 +Flask-WTF==1.2.1 Markdown==3.3.6 bleach==5.0.1 musicbrainzngs==0.7.1 @@ -20,19 +20,20 @@ transifex-client==0.12.4 WTForms==3.0.1 email-validator==1.1.3 langdetect==1.0.7 -Flask==2.2.5 +Flask==3.0.0 Jinja2==3.1.2 -werkzeug==2.2.3 -Flask-DebugToolbar==0.13.1 +werkzeug==3.0.1 +Flask-DebugToolbar@git+https://github.com/amCap1712/flask-debugtoolbar.git@f42bb238cd3fbc79c51b93c341164c2be820025e Flask-UUID==0.2 -sentry-sdk[flask]==1.14.0 +sentry-sdk[flask]==1.37.1 redis==4.4.4 msgpack==0.5.6 requests==2.31.0 SQLAlchemy==1.4.41 mbdata@git+https://github.com/amCap1712/mbdata.git@fix-sqlalchemy-warnings sqlalchemy-dst==1.0.1 -markupsafe==2.1.1 +markupsafe==2.1.3 itsdangerous==2.1.2 flask-shell-ipython -requests-mock==1.9.3 \ No newline at end of file +requests-mock==1.9.3 +orjson==3.9.10 From 47e8a2218397484f319ba2390017a78b9abfc9ef Mon Sep 17 00:00:00 2001 From: Kartik Ohri Date: Fri, 1 Dec 2023 14:12:58 +0530 Subject: [PATCH 10/16] fix test --- critiquebrainz/frontend/views/test/test_profile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/critiquebrainz/frontend/views/test/test_profile.py b/critiquebrainz/frontend/views/test/test_profile.py index 0a888b33d..4cbc16d61 100644 --- a/critiquebrainz/frontend/views/test/test_profile.py +++ b/critiquebrainz/frontend/views/test/test_profile.py @@ -37,7 +37,7 @@ def test_edit(self): # as the current_user is loaded at the start of each request/request context. self.temporary_login(self.user) - response = self.client.post(f'/user/{self.user.id}') + response = self.client.get(f'/user/{self.user.id}') self.assert200(response) self.assertIn(data['display_name'], str(response.data)) From dfe696c942803c921332a232185ac471e33de56c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jan 2024 19:52:02 +0000 Subject: [PATCH 11/16] build(deps): Bump jinja2 from 3.1.2 to 3.1.3 Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.2 to 3.1.3. - [Release notes](https://github.com/pallets/jinja/releases) - [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/jinja/compare/3.1.2...3.1.3) --- updated-dependencies: - dependency-name: jinja2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 252ba8aa6..3b2a3506e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,7 +21,7 @@ WTForms==3.0.1 email-validator==1.1.3 langdetect==1.0.7 Flask==3.0.0 -Jinja2==3.1.2 +Jinja2==3.1.3 werkzeug==3.0.1 Flask-DebugToolbar@git+https://github.com/amCap1712/flask-debugtoolbar.git@f42bb238cd3fbc79c51b93c341164c2be820025e Flask-UUID==0.2 From 083cba54f78b024d3166071f3468d4fa46bb2400 Mon Sep 17 00:00:00 2001 From: Monkey Do Date: Tue, 27 Feb 2024 11:09:46 +0100 Subject: [PATCH 12/16] tests: Fix BookBrainz Work sort name This test was expecting a sort name that is incorrect, and has since been corrected in the main database. I'm not sure it's a good idea to run tests on entities that might change over time, as is the case here. --- .../frontend/external/bookbrainz_db/test/literary_work_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/critiquebrainz/frontend/external/bookbrainz_db/test/literary_work_test.py b/critiquebrainz/frontend/external/bookbrainz_db/test/literary_work_test.py index d842108e8..ccc478d1c 100644 --- a/critiquebrainz/frontend/external/bookbrainz_db/test/literary_work_test.py +++ b/critiquebrainz/frontend/external/bookbrainz_db/test/literary_work_test.py @@ -15,7 +15,7 @@ def test_get_literary_work_by_bbid(self): literary_work_info = literary_work.get_literary_work_by_bbid(self.bbid1) self.assertEqual(literary_work_info["bbid"], self.bbid1) self.assertEqual(literary_work_info["name"], "Assassin's Creed: Brotherhood") - self.assertEqual(literary_work_info["sort_name"], "Brotherhood, Assassin's Creed:") + self.assertEqual(literary_work_info["sort_name"], "Assassin's Creed: Brotherhood") self.assertEqual(literary_work_info["work_type"], "Novel") def test_fetch_multiple_literary_works(self): From 9df4caefcf46376ad439c6dff8d019c17ae49fcb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 10:10:54 +0000 Subject: [PATCH 13/16] build(deps): Bump orjson from 3.9.10 to 3.9.15 Bumps [orjson](https://github.com/ijl/orjson) from 3.9.10 to 3.9.15. - [Release notes](https://github.com/ijl/orjson/releases) - [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md) - [Commits](https://github.com/ijl/orjson/compare/3.9.10...3.9.15) --- updated-dependencies: - dependency-name: orjson dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3b2a3506e..8ac39fcd3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,4 +36,4 @@ markupsafe==2.1.3 itsdangerous==2.1.2 flask-shell-ipython requests-mock==1.9.3 -orjson==3.9.10 +orjson==3.9.15 From 493dd626cd7942435487a2004a6f32015c40f6de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 19:26:00 +0000 Subject: [PATCH 14/16] build(deps): Bump werkzeug from 3.0.1 to 3.0.3 Bumps [werkzeug](https://github.com/pallets/werkzeug) from 3.0.1 to 3.0.3. - [Release notes](https://github.com/pallets/werkzeug/releases) - [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/werkzeug/compare/3.0.1...3.0.3) --- updated-dependencies: - dependency-name: werkzeug dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8ac39fcd3..5b6c38578 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ email-validator==1.1.3 langdetect==1.0.7 Flask==3.0.0 Jinja2==3.1.3 -werkzeug==3.0.1 +werkzeug==3.0.3 Flask-DebugToolbar@git+https://github.com/amCap1712/flask-debugtoolbar.git@f42bb238cd3fbc79c51b93c341164c2be820025e Flask-UUID==0.2 sentry-sdk[flask]==1.37.1 From e964c6fc8592824b1450190136190b9c045f659d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 May 2024 06:44:08 +0000 Subject: [PATCH 15/16] --- updated-dependencies: - dependency-name: requests dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8ac39fcd3..ab0031f33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,7 @@ Flask-UUID==0.2 sentry-sdk[flask]==1.37.1 redis==4.4.4 msgpack==0.5.6 -requests==2.31.0 +requests==2.32.0 SQLAlchemy==1.4.41 mbdata@git+https://github.com/amCap1712/mbdata.git@fix-sqlalchemy-warnings sqlalchemy-dst==1.0.1 From 2c572467172ac32ddcf19c19352c5d370dc250fb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 May 2024 14:58:54 +0000 Subject: [PATCH 16/16] build(deps): Bump jinja2 from 3.1.3 to 3.1.4 Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.3 to 3.1.4. - [Release notes](https://github.com/pallets/jinja/releases) - [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/jinja/compare/3.1.3...3.1.4) --- updated-dependencies: - dependency-name: jinja2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5b6c38578..76f329c6e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,7 +21,7 @@ WTForms==3.0.1 email-validator==1.1.3 langdetect==1.0.7 Flask==3.0.0 -Jinja2==3.1.3 +Jinja2==3.1.4 werkzeug==3.0.3 Flask-DebugToolbar@git+https://github.com/amCap1712/flask-debugtoolbar.git@f42bb238cd3fbc79c51b93c341164c2be820025e Flask-UUID==0.2