From a2fc6da7f663d260b4fdb2862f7381fdee1fd21f Mon Sep 17 00:00:00 2001 From: dimmur-brw Date: Mon, 8 Dec 2025 06:15:24 +0100 Subject: [PATCH 1/3] Add ability to defer signals/tasks in tests (#4375) --- backend/pytest.ini | 2 + .../src/baserow/test_utils/pytest_conftest.py | 61 ++++++++++++++++++- .../data_sources/test_data_source_views.py | 4 ++ .../database/api/rows/test_row_views.py | 4 ++ .../views/gallery/test_gallery_view_views.py | 4 ++ .../api/views/grid/test_grid_view_views.py | 4 ++ .../database/api/views/test_view_views.py | 3 + .../field/test_field_notification_types.py | 5 ++ .../field/test_link_row_field_type.py | 4 ++ .../test_multiple_collaborators_field_type.py | 4 ++ .../contrib/database/rows/test_row_history.py | 1 + .../search/test_search_compatibility.py | 5 ++ .../database/search/test_search_handler.py | 5 ++ .../database/search/test_search_indexing.py | 5 ++ .../database/search/test_search_tasks.py | 5 ++ .../database/search/test_search_views.py | 5 ++ .../database/table/test_table_handler.py | 3 + .../database/table/test_table_usage_types.py | 4 ++ .../database/view/test_view_handler.py | 31 ++++++++++ .../view/test_view_notification_types.py | 4 ++ .../test_aggregate_rows_service_type.py | 4 ++ .../integrations/local_baserow/test_mixins.py | 4 ++ .../test_notifications_handler.py | 4 ++ ...by_deferring_heavy_signals_by_default.json | 9 +++ .../ws/test_database_rbac.py | 1 + .../ws/test_role_signals.py | 2 + .../ws/test_table_rbac.py | 2 + .../row_comments/test_row_comments_handler.py | 6 +- .../test_row_comments_notification_types.py | 3 + .../test_premium_view_notification_types.py | 1 + .../ws/test_ws_row_comments_signals.py | 1 + 31 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 changelog/entries/unreleased/refactor/4373_optimize_test_suite_by_deferring_heavy_signals_by_default.json diff --git a/backend/pytest.ini b/backend/pytest.ini index 807858ae40..92b5917710 100644 --- a/backend/pytest.ini +++ b/backend/pytest.ini @@ -52,3 +52,5 @@ markers = data_sync: All tests related to data sync functionality replica: All tests related to db replicas workspace_search: All tests related to workspace search functionality + enable_all_signals: Disables signal deferral for this test (all signals enabled) + enable_signals: Enables specific signals for this test (accepts dotted callable paths) diff --git a/backend/src/baserow/test_utils/pytest_conftest.py b/backend/src/baserow/test_utils/pytest_conftest.py index a3be16583c..f07dfdadc0 100755 --- a/backend/src/baserow/test_utils/pytest_conftest.py +++ b/backend/src/baserow/test_utils/pytest_conftest.py @@ -3,7 +3,7 @@ import os import sys import threading -from contextlib import contextmanager +from contextlib import ExitStack, contextmanager from datetime import date, datetime from decimal import Decimal from functools import partial @@ -48,6 +48,22 @@ SKIP_FLAGS = ["disabled-in-ci", "once-per-day-in-ci"] COMMAND_LINE_FLAG_PREFIX = "--run-" +# List of heavy callables to defer by default during tests +# Used by defer_heavy_signals fixture +DEFAULT_DEFERRED_CALLABLES: List[str] = [ + "baserow.ws.tasks.broadcast_to_channel_group.delay", + "baserow.ws.tasks.broadcast_to_users.delay", + "baserow.ws.tasks.broadcast_to_permitted_users.delay", + "baserow.ws.tasks.broadcast_to_group.delay", + "baserow.ws.tasks.broadcast_to_groups.delay", + "baserow.ws.tasks.broadcast_application_created.delay", + "baserow.contrib.database.search.tasks.schedule_update_search_data.delay", + "baserow.contrib.database.search.tasks.update_search_data.delay", + "baserow.contrib.database.table.tasks.update_table_usage.delay", + "baserow.core.notifications.tasks.send_queued_notifications_to_users.delay", + "baserow.contrib.database.views.tasks.update_view_index.delay", +] + # Provides a new fake instance for each class. Solve uniqueness problem sometimes. @pytest.fixture(scope="class", autouse=True) @@ -153,6 +169,41 @@ def clear_cache(): yield +@pytest.fixture(autouse=True) +def defer_heavy_signals(request): + """ + Defer heavy callables by default to speed up tests. + + Opt-out options: + @pytest.mark.enable_all_signals - Enable all deferred signals + @pytest.mark.enable_signals("path.to.callable") - Enable specific signal(s) + """ + + if request.node.get_closest_marker("enable_all_signals"): + yield + return + + signals_to_defer = set(DEFAULT_DEFERRED_CALLABLES) + + enable_signals_marker = request.node.get_closest_marker("enable_signals") + if enable_signals_marker: + signals_to_enable = set() + if enable_signals_marker.args: + signals_to_enable.update(enable_signals_marker.args) + if enable_signals_marker.kwargs.get("signals"): + signals_to_enable.update(enable_signals_marker.kwargs["signals"]) + signals_to_defer -= signals_to_enable + + if not signals_to_defer: + yield + return + + with ExitStack() as stack: + for name in signals_to_defer: + stack.enter_context(patch(name, lambda *args, **kwargs: None)) + yield + + @pytest.fixture def reset_schema(django_db_blocker): yield @@ -498,6 +549,14 @@ def pytest_configure(config): f"{flag}: mark test so it only runs when the " f"{COMMAND_LINE_FLAG_PREFIX}{flag} flag is provided to pytest", ) + config.addinivalue_line( + "markers", + "enable_all_signals: Disables signal deferral for this test", + ) + config.addinivalue_line( + "markers", + "enable_signals(callable_paths): Enables specific signals for this test", + ) pytest_configure.already_run = True diff --git a/backend/tests/baserow/contrib/builder/api/data_sources/test_data_source_views.py b/backend/tests/baserow/contrib/builder/api/data_sources/test_data_source_views.py index 0dd0369313..bcf09e7d67 100644 --- a/backend/tests/baserow/contrib/builder/api/data_sources/test_data_source_views.py +++ b/backend/tests/baserow/contrib/builder/api/data_sources/test_data_source_views.py @@ -1183,6 +1183,10 @@ def test_dispatch_data_source_with_adhoc_sortings(api_client, data_fixture): @pytest.mark.django_db(transaction=True) +@pytest.mark.enable_signals( + "baserow.contrib.database.search.tasks.schedule_update_search_data.delay", + "baserow.contrib.database.search.tasks.update_search_data.delay", +) def test_dispatch_data_source_with_adhoc_search(api_client, data_fixture): with transaction.atomic(): user, token = data_fixture.create_user_and_token() diff --git a/backend/tests/baserow/contrib/database/api/rows/test_row_views.py b/backend/tests/baserow/contrib/database/api/rows/test_row_views.py index b8072926d2..b87c5f865d 100644 --- a/backend/tests/baserow/contrib/database/api/rows/test_row_views.py +++ b/backend/tests/baserow/contrib/database/api/rows/test_row_views.py @@ -3678,6 +3678,10 @@ def test_get_row_adjacent_view_invalid_requests(api_client, data_fixture): @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize("search_mode", ALL_SEARCH_MODES) +@pytest.mark.enable_signals( + "baserow.contrib.database.search.tasks.schedule_update_search_data.delay", + "baserow.contrib.database.search.tasks.update_search_data.delay", +) def test_get_row_adjacent_search(api_client, data_fixture, search_mode): user, jwt_token = data_fixture.create_user_and_token( email="test@test.nl", password="password", first_name="Test1" diff --git a/backend/tests/baserow/contrib/database/api/views/gallery/test_gallery_view_views.py b/backend/tests/baserow/contrib/database/api/views/gallery/test_gallery_view_views.py index b890d7dc5b..a44dd8cad1 100644 --- a/backend/tests/baserow/contrib/database/api/views/gallery/test_gallery_view_views.py +++ b/backend/tests/baserow/contrib/database/api/views/gallery/test_gallery_view_views.py @@ -1249,6 +1249,10 @@ def test_list_rows_public_with_query_param_advanced_filters(api_client, data_fix @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize("search_mode", ALL_SEARCH_MODES) +@pytest.mark.enable_signals( + "baserow.contrib.database.search.tasks.schedule_update_search_data.delay", + "baserow.contrib.database.search.tasks.update_search_data.delay", +) def test_list_rows_public_only_searches_by_visible_columns( api_client, data_fixture, search_mode ): diff --git a/backend/tests/baserow/contrib/database/api/views/grid/test_grid_view_views.py b/backend/tests/baserow/contrib/database/api/views/grid/test_grid_view_views.py index 1d88a8fa97..338f633ccb 100644 --- a/backend/tests/baserow/contrib/database/api/views/grid/test_grid_view_views.py +++ b/backend/tests/baserow/contrib/database/api/views/grid/test_grid_view_views.py @@ -4024,6 +4024,10 @@ def test_list_rows_public_filters_by_visible_and_hidden_columns( @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize("search_mode", ALL_SEARCH_MODES) +@pytest.mark.enable_signals( + "baserow.contrib.database.search.tasks.schedule_update_search_data.delay", + "baserow.contrib.database.search.tasks.update_search_data.delay", +) def test_list_rows_public_only_searches_by_visible_columns( api_client, data_fixture, search_mode ): diff --git a/backend/tests/baserow/contrib/database/api/views/test_view_views.py b/backend/tests/baserow/contrib/database/api/views/test_view_views.py index 12c4d40431..d9d147f318 100644 --- a/backend/tests/baserow/contrib/database/api/views/test_view_views.py +++ b/backend/tests/baserow/contrib/database/api/views/test_view_views.py @@ -1172,6 +1172,9 @@ def test_view_cant_update_allow_public_export(data_fixture, api_client): @pytest.mark.django_db(transaction=True) +@pytest.mark.enable_signals( + "baserow.contrib.database.views.tasks.update_view_index.delay" +) def test_loading_a_sortable_view_will_create_an_index(api_client, data_fixture): user, token = data_fixture.create_user_and_token() table = data_fixture.create_database_table(user=user) diff --git a/backend/tests/baserow/contrib/database/field/test_field_notification_types.py b/backend/tests/baserow/contrib/database/field/test_field_notification_types.py index eabfe65b0e..ad58503e7a 100644 --- a/backend/tests/baserow/contrib/database/field/test_field_notification_types.py +++ b/backend/tests/baserow/contrib/database/field/test_field_notification_types.py @@ -31,6 +31,11 @@ assert_undo_redo_actions_are_valid, ) +pytestmark = pytest.mark.enable_signals( + "baserow.core.notifications.tasks.send_queued_notifications_to_users.delay", + "baserow.ws.tasks.broadcast_to_users.delay", +) + @pytest.mark.django_db(transaction=True) @patch("baserow.ws.tasks.broadcast_to_users.apply") diff --git a/backend/tests/baserow/contrib/database/field/test_link_row_field_type.py b/backend/tests/baserow/contrib/database/field/test_link_row_field_type.py index 8591b14ee6..9cd0a587d4 100644 --- a/backend/tests/baserow/contrib/database/field/test_link_row_field_type.py +++ b/backend/tests/baserow/contrib/database/field/test_link_row_field_type.py @@ -1185,6 +1185,10 @@ def test_link_row_field_type_api_row_views(api_client, data_fixture): @pytest.mark.django_db(transaction=True) @pytest.mark.field_link_row +@pytest.mark.enable_signals( + "baserow.contrib.database.search.tasks.schedule_update_search_data.delay", + "baserow.contrib.database.search.tasks.update_search_data.delay", +) def test_import_export_link_row_field(data_fixture): user = data_fixture.create_user() imported_workspace = data_fixture.create_workspace(user=user) diff --git a/backend/tests/baserow/contrib/database/field/test_multiple_collaborators_field_type.py b/backend/tests/baserow/contrib/database/field/test_multiple_collaborators_field_type.py index 658ce36b27..bc7ec7a75e 100644 --- a/backend/tests/baserow/contrib/database/field/test_multiple_collaborators_field_type.py +++ b/backend/tests/baserow/contrib/database/field/test_multiple_collaborators_field_type.py @@ -832,6 +832,10 @@ def test_multiple_collaborators_field_type_values_can_be_stringified(data_fixtur @pytest.mark.django_db(transaction=True) @pytest.mark.field_multiple_collaborators +@pytest.mark.enable_signals( + "baserow.contrib.database.search.tasks.schedule_update_search_data.delay", + "baserow.contrib.database.search.tasks.update_search_data.delay", +) def test_multiple_collaborators_field_type_values_can_be_searched(data_fixture): mario = data_fixture.create_user(first_name="Mario") luigi = data_fixture.create_user(first_name="Luigi") diff --git a/backend/tests/baserow/contrib/database/rows/test_row_history.py b/backend/tests/baserow/contrib/database/rows/test_row_history.py index fddf837fdb..7b67309548 100644 --- a/backend/tests/baserow/contrib/database/rows/test_row_history.py +++ b/backend/tests/baserow/contrib/database/rows/test_row_history.py @@ -1163,6 +1163,7 @@ def test_create_rows_action_row_history_with_undo_redo( ) @pytest.mark.django_db @pytest.mark.row_history +@pytest.mark.enable_signals("baserow.ws.tasks.broadcast_to_users.delay") def test_delete_rows_action_row_history_with_undo_redo( data_fixture, action_type: "ActionType", input_values: Callable ): diff --git a/backend/tests/baserow/contrib/database/search/test_search_compatibility.py b/backend/tests/baserow/contrib/database/search/test_search_compatibility.py index 17277656d2..6ebaf7355f 100644 --- a/backend/tests/baserow/contrib/database/search/test_search_compatibility.py +++ b/backend/tests/baserow/contrib/database/search/test_search_compatibility.py @@ -11,6 +11,11 @@ from baserow.contrib.database.table.handler import TableHandler from baserow.core.user_files.handler import UserFileHandler +pytestmark = pytest.mark.enable_signals( + "baserow.contrib.database.search.tasks.schedule_update_search_data.delay", + "baserow.contrib.database.search.tasks.update_search_data.delay", +) + @pytest.mark.django_db(transaction=True) def test_search_compatibility_between_current_and_postgres(data_fixture, tmpdir): diff --git a/backend/tests/baserow/contrib/database/search/test_search_handler.py b/backend/tests/baserow/contrib/database/search/test_search_handler.py index 5af959ddde..7b0a7afead 100644 --- a/backend/tests/baserow/contrib/database/search/test_search_handler.py +++ b/backend/tests/baserow/contrib/database/search/test_search_handler.py @@ -16,6 +16,11 @@ from baserow.core.trash.handler import TrashHandler from baserow.core.utils import Progress +pytestmark = pytest.mark.enable_signals( + "baserow.contrib.database.search.tasks.schedule_update_search_data.delay", + "baserow.contrib.database.search.tasks.update_search_data.delay", +) + def test_escape_query(): # Spacing is standardized. diff --git a/backend/tests/baserow/contrib/database/search/test_search_indexing.py b/backend/tests/baserow/contrib/database/search/test_search_indexing.py index 783d6ab23b..42f1673f66 100644 --- a/backend/tests/baserow/contrib/database/search/test_search_indexing.py +++ b/backend/tests/baserow/contrib/database/search/test_search_indexing.py @@ -14,6 +14,11 @@ from baserow.contrib.database.table.handler import TableHandler from baserow.core.user_files.handler import UserFileHandler +pytestmark = pytest.mark.enable_signals( + "baserow.contrib.database.search.tasks.schedule_update_search_data.delay", + "baserow.contrib.database.search.tasks.update_search_data.delay", +) + @pytest.mark.django_db(transaction=True) def test_textfield_get_search_expression(data_fixture): diff --git a/backend/tests/baserow/contrib/database/search/test_search_tasks.py b/backend/tests/baserow/contrib/database/search/test_search_tasks.py index 13e30f6217..3bdb07f754 100644 --- a/backend/tests/baserow/contrib/database/search/test_search_tasks.py +++ b/backend/tests/baserow/contrib/database/search/test_search_tasks.py @@ -6,6 +6,11 @@ from baserow.contrib.database.search.models import PendingSearchValueUpdate from baserow.contrib.database.search.tasks import periodic_check_pending_search_data +pytestmark = pytest.mark.enable_signals( + "baserow.contrib.database.search.tasks.schedule_update_search_data.delay", + "baserow.contrib.database.search.tasks.update_search_data.delay", +) + @pytest.mark.django_db(transaction=True) def test_periodic_check_pending_search_data(data_fixture): diff --git a/backend/tests/baserow/contrib/database/search/test_search_views.py b/backend/tests/baserow/contrib/database/search/test_search_views.py index 1c6abee260..0627680a9e 100644 --- a/backend/tests/baserow/contrib/database/search/test_search_views.py +++ b/backend/tests/baserow/contrib/database/search/test_search_views.py @@ -9,6 +9,11 @@ from baserow.contrib.database.search.handler import SearchMode from baserow.test_utils.helpers import setup_interesting_test_table +pytestmark = pytest.mark.enable_signals( + "baserow.contrib.database.search.tasks.schedule_update_search_data.delay", + "baserow.contrib.database.search.tasks.update_search_data.delay", +) + @pytest.mark.django_db def test_search_grid_with_invalid_mode(api_client, data_fixture): diff --git a/backend/tests/baserow/contrib/database/table/test_table_handler.py b/backend/tests/baserow/contrib/database/table/test_table_handler.py index b15c9decc0..8e7dc364d6 100644 --- a/backend/tests/baserow/contrib/database/table/test_table_handler.py +++ b/backend/tests/baserow/contrib/database/table/test_table_handler.py @@ -1115,6 +1115,9 @@ def test_usage_is_calculated_correctly_when_a_template_is_installed( @pytest.mark.django_db(transaction=True) +@pytest.mark.enable_signals( + "baserow.contrib.database.table.tasks.update_table_usage.delay" +) def test_usage_is_calculated_correctly_when_creating_a_new_table(data_fixture): user = data_fixture.create_user() database = data_fixture.create_database_application(user=user) diff --git a/backend/tests/baserow/contrib/database/table/test_table_usage_types.py b/backend/tests/baserow/contrib/database/table/test_table_usage_types.py index 878be00ede..5832551d22 100644 --- a/backend/tests/baserow/contrib/database/table/test_table_usage_types.py +++ b/backend/tests/baserow/contrib/database/table/test_table_usage_types.py @@ -9,6 +9,10 @@ from baserow.core.trash.handler import TrashHandler from baserow.core.usage.registries import USAGE_UNIT_MB +pytestmark = pytest.mark.enable_signals( + "baserow.contrib.database.table.tasks.update_table_usage.delay", +) + @pytest.mark.django_db(transaction=True) def test_table_workspace_storage_usage_item_type(data_fixture): diff --git a/backend/tests/baserow/contrib/database/view/test_view_handler.py b/backend/tests/baserow/contrib/database/view/test_view_handler.py index 875df488b9..97ea025676 100755 --- a/backend/tests/baserow/contrib/database/view/test_view_handler.py +++ b/backend/tests/baserow/contrib/database/view/test_view_handler.py @@ -2275,6 +2275,10 @@ def test_get_public_rows_queryset_and_field_ids_view_filters_applied(data_fixtur @pytest.mark.django_db(transaction=True) @pytest.mark.parametrize("search_mode", ALL_SEARCH_MODES) +@pytest.mark.enable_signals( + "baserow.contrib.database.search.tasks.schedule_update_search_data.delay", + "baserow.contrib.database.search.tasks.update_search_data.delay", +) def test_get_public_rows_queryset_and_field_ids_view_search(data_fixture, search_mode): grid_view = data_fixture.create_grid_view(public=True) table = grid_view.table @@ -3022,6 +3026,9 @@ def test_order_views_ownership_type(data_fixture): @override_settings(AUTO_INDEX_VIEW_ENABLED=True) @pytest.mark.django_db(transaction=True) +@pytest.mark.enable_signals( + "baserow.contrib.database.views.tasks.update_view_index.delay" +) def test_creating_view_sort_creates_a_new_index(data_fixture): user = data_fixture.create_user() table = data_fixture.create_database_table(user=user) @@ -3052,6 +3059,9 @@ def test_creating_view_sort_creates_a_new_index(data_fixture): AUTO_INDEX_VIEW_ENABLED=True, ) @pytest.mark.django_db(transaction=True) +@pytest.mark.enable_signals( + "baserow.contrib.database.views.tasks.update_view_index.delay" +) def test_updating_view_sorts_creates_a_new_index_and_delete_the_unused_one( data_fixture, ): @@ -3110,6 +3120,9 @@ def test_updating_view_sorts_creates_a_new_index_and_delete_the_unused_one( @override_settings(AUTO_INDEX_VIEW_ENABLED=True) @pytest.mark.django_db(transaction=True) +@pytest.mark.enable_signals( + "baserow.contrib.database.views.tasks.update_view_index.delay" +) def test_perm_deleting_view_remove_index_if_unused(data_fixture): user = data_fixture.create_user() table = data_fixture.create_database_table(user=user) @@ -3150,6 +3163,9 @@ def test_perm_deleting_view_remove_index_if_unused(data_fixture): @override_settings(AUTO_INDEX_VIEW_ENABLED=True) @pytest.mark.django_db(transaction=True) +@pytest.mark.enable_signals( + "baserow.contrib.database.views.tasks.update_view_index.delay" +) def test_duplicating_table_do_not_duplicate_indexes(data_fixture): user = data_fixture.create_user() table = data_fixture.create_database_table(user=user) @@ -3179,6 +3195,9 @@ def test_duplicating_table_do_not_duplicate_indexes(data_fixture): @override_settings(AUTO_INDEX_VIEW_ENABLED=True) @pytest.mark.django_db(transaction=True) +@pytest.mark.enable_signals( + "baserow.contrib.database.views.tasks.update_view_index.delay" +) def test_deleting_a_field_of_a_view_sort_update_view_indexes(data_fixture): user = data_fixture.create_user() table = data_fixture.create_database_table(user=user) @@ -3207,6 +3226,9 @@ def test_deleting_a_field_of_a_view_sort_update_view_indexes(data_fixture): @override_settings(AUTO_INDEX_VIEW_ENABLED=True) @pytest.mark.django_db(transaction=True) +@pytest.mark.enable_signals( + "baserow.contrib.database.views.tasks.update_view_index.delay" +) def test_changing_a_field_type_of_a_view_sort_to_non_orderable_one_delete_view_index( data_fixture, ): @@ -3231,6 +3253,9 @@ def test_changing_a_field_type_of_a_view_sort_to_non_orderable_one_delete_view_i @patch("baserow.contrib.database.views.tasks.update_view_index.delay") @pytest.mark.django_db(transaction=True) +@pytest.mark.enable_signals( + "baserow.contrib.database.views.tasks.update_view_index.delay" +) def test_loading_a_view_checks_for_db_index_without_additional_queries( mocked_view_index_update_task, data_fixture, @@ -3291,6 +3316,9 @@ def test_loading_a_view_checks_for_db_index_without_additional_queries( AUTO_INDEX_VIEW_ENABLED=True, ) @pytest.mark.django_db(transaction=True) +@pytest.mark.enable_signals( + "baserow.contrib.database.views.tasks.update_view_index.delay" +) def test_update_index_replaces_index_with_diff_collation(settings, data_fixture): with patch("baserow.core.db.get_collation_name", new=lambda: None): user = data_fixture.create_user() @@ -3328,6 +3356,9 @@ def test_update_index_replaces_index_with_diff_collation(settings, data_fixture) AUTO_INDEX_VIEW_ENABLED=True, ) @pytest.mark.django_db(transaction=True) +@pytest.mark.enable_signals( + "baserow.contrib.database.views.tasks.update_view_index.delay" +) def test_view_loaded_replaces_index_with_diff_collation(settings, data_fixture): with patch("baserow.core.db.get_collation_name", new=lambda: None): user = data_fixture.create_user() diff --git a/backend/tests/baserow/contrib/database/view/test_view_notification_types.py b/backend/tests/baserow/contrib/database/view/test_view_notification_types.py index b3077169a2..6ffbdeac4c 100644 --- a/backend/tests/baserow/contrib/database/view/test_view_notification_types.py +++ b/backend/tests/baserow/contrib/database/view/test_view_notification_types.py @@ -17,6 +17,7 @@ @pytest.mark.django_db(transaction=True) @patch("baserow.ws.tasks.broadcast_to_users.apply") +@pytest.mark.enable_signals("baserow.ws.tasks.broadcast_to_users.delay") def test_user_receive_notification_on_form_submit( mocked_broadcast_to_users, api_client, data_fixture ): @@ -130,6 +131,7 @@ def submit_form(): @pytest.mark.django_db(transaction=True) @patch("baserow.ws.tasks.broadcast_to_users.apply") +@pytest.mark.enable_signals("baserow.ws.tasks.broadcast_to_users.delay") def test_all_interested_users_receive_the_notification_on_form_submit( mocked_broadcast_to_users, api_client, data_fixture ): @@ -178,6 +180,7 @@ def submit_form(): @pytest.mark.django_db(transaction=True) @patch("baserow.ws.tasks.broadcast_to_users.apply") +@pytest.mark.enable_signals("baserow.ws.tasks.broadcast_to_users.delay") def test_only_users_with_access_to_the_table_receive_the_notification_on_form_submit( mocked_broadcast_to_users, api_client, data_fixture ): @@ -325,6 +328,7 @@ def test_form_submit_notification_can_be_render_as_email(api_client, data_fixtur @pytest.mark.django_db(transaction=True) @patch("baserow.ws.tasks.broadcast_to_users.apply") +@pytest.mark.enable_signals("baserow.ws.tasks.broadcast_to_users.delay") def test_can_user_receive_notification_for_all_interesting_field_values( mocked_broadcast_to_users, api_client, data_fixture ): diff --git a/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_aggregate_rows_service_type.py b/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_aggregate_rows_service_type.py index 52ed9beba4..ed480892be 100644 --- a/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_aggregate_rows_service_type.py +++ b/backend/tests/baserow/contrib/integrations/local_baserow/service_types/test_aggregate_rows_service_type.py @@ -420,6 +420,10 @@ def test_local_baserow_aggregate_rows_dispatch_data_with_service_filters(data_fi @pytest.mark.django_db(transaction=True) +@pytest.mark.enable_signals( + "baserow.contrib.database.search.tasks.schedule_update_search_data.delay", + "baserow.contrib.database.search.tasks.update_search_data.delay", +) def test_local_baserow_aggregate_rows_dispatch_data_with_search(data_fixture): with transaction.atomic(): user = data_fixture.create_user() diff --git a/backend/tests/baserow/contrib/integrations/local_baserow/test_mixins.py b/backend/tests/baserow/contrib/integrations/local_baserow/test_mixins.py index e2b8c3cd8f..0b74b20a8e 100644 --- a/backend/tests/baserow/contrib/integrations/local_baserow/test_mixins.py +++ b/backend/tests/baserow/contrib/integrations/local_baserow/test_mixins.py @@ -434,6 +434,10 @@ def test_local_baserow_table_service_sortable_mixin_get_dispatch_sorts_raises_ex @pytest.mark.django_db(transaction=True) +@pytest.mark.enable_signals( + "baserow.contrib.database.search.tasks.schedule_update_search_data.delay", + "baserow.contrib.database.search.tasks.update_search_data.delay", +) def test_local_baserow_table_service_searchable_mixin_get_table_queryset( data_fixture, ): diff --git a/backend/tests/baserow/core/notifications/test_notifications_handler.py b/backend/tests/baserow/core/notifications/test_notifications_handler.py index ad0655fd32..92ba81b565 100644 --- a/backend/tests/baserow/core/notifications/test_notifications_handler.py +++ b/backend/tests/baserow/core/notifications/test_notifications_handler.py @@ -299,6 +299,10 @@ def test_queued_notifications_are_not_visible_to_the_users( @pytest.mark.django_db(transaction=True) @patch("baserow.ws.tasks.broadcast_to_users.apply") +@pytest.mark.enable_signals( + "baserow.core.notifications.tasks.send_queued_notifications_to_users.delay", + "baserow.ws.tasks.broadcast_to_users.delay", +) def test_queued_notifications_are_sent_grouped_by_user( mocked_broadcast_to_users, data_fixture, diff --git a/changelog/entries/unreleased/refactor/4373_optimize_test_suite_by_deferring_heavy_signals_by_default.json b/changelog/entries/unreleased/refactor/4373_optimize_test_suite_by_deferring_heavy_signals_by_default.json new file mode 100644 index 0000000000..9efc9d6c2d --- /dev/null +++ b/changelog/entries/unreleased/refactor/4373_optimize_test_suite_by_deferring_heavy_signals_by_default.json @@ -0,0 +1,9 @@ +{ + "type": "refactor", + "message": "Optimize test suite by deferring heavy signals by default", + "issue_origin": "github", + "issue_number": 4373, + "domain": "database", + "bullet_points": [], + "created_at": "2025-12-02" +} \ No newline at end of file diff --git a/enterprise/backend/tests/baserow_enterprise_tests/ws/test_database_rbac.py b/enterprise/backend/tests/baserow_enterprise_tests/ws/test_database_rbac.py index 5695b3977f..15dbf88696 100644 --- a/enterprise/backend/tests/baserow_enterprise_tests/ws/test_database_rbac.py +++ b/enterprise/backend/tests/baserow_enterprise_tests/ws/test_database_rbac.py @@ -127,6 +127,7 @@ async def test_database_created_message_not_leaking(data_fixture): @pytest.mark.django_db(transaction=True) @pytest.mark.asyncio @pytest.mark.websockets +@pytest.mark.enable_all_signals async def test_workspace_restored_applications_arent_leaked(data_fixture): user_excluded, token = data_fixture.create_user_and_token() workspace = data_fixture.create_workspace(user=user_excluded) diff --git a/enterprise/backend/tests/baserow_enterprise_tests/ws/test_role_signals.py b/enterprise/backend/tests/baserow_enterprise_tests/ws/test_role_signals.py index ececa5a604..6587d2999a 100644 --- a/enterprise/backend/tests/baserow_enterprise_tests/ws/test_role_signals.py +++ b/enterprise/backend/tests/baserow_enterprise_tests/ws/test_role_signals.py @@ -449,6 +449,7 @@ async def test_unsubscribe_subject_from_table_teams_multiple_users( @pytest.mark.django_db(transaction=True) @pytest.mark.websockets @pytest.mark.flaky(retries=3, delay=1) +@pytest.mark.enable_all_signals async def test_unsubscribe_user_from_tables_and_rows_when_role_updated(data_fixture): channel_layer = get_channel_layer() user_1, token_1 = data_fixture.create_user_and_token() @@ -538,6 +539,7 @@ async def test_unsubscribe_user_from_tables_and_rows_when_role_updated(data_fixt @pytest.mark.django_db(transaction=True) @pytest.mark.websockets @pytest.mark.flaky(retries=3, delay=1) +@pytest.mark.enable_all_signals async def test_unsubscribe_user_from_tables_and_rows_when_team_trashed( data_fixture, enterprise_data_fixture ): diff --git a/enterprise/backend/tests/baserow_enterprise_tests/ws/test_table_rbac.py b/enterprise/backend/tests/baserow_enterprise_tests/ws/test_table_rbac.py index ed1d901dd6..ec9172e787 100644 --- a/enterprise/backend/tests/baserow_enterprise_tests/ws/test_table_rbac.py +++ b/enterprise/backend/tests/baserow_enterprise_tests/ws/test_table_rbac.py @@ -120,6 +120,7 @@ async def test_table_created_message_not_leaking(data_fixture): @pytest.mark.django_db(transaction=True) @pytest.mark.asyncio +@pytest.mark.enable_all_signals async def test_workspace_restored_tables_not_leaking(data_fixture): user = data_fixture.create_user() user_excluded, token = data_fixture.create_user_and_token() @@ -153,6 +154,7 @@ async def test_workspace_restored_tables_not_leaking(data_fixture): @pytest.mark.django_db(transaction=True) @pytest.mark.asyncio +@pytest.mark.enable_all_signals async def test_database_restored_tables_not_leaking(data_fixture): user = data_fixture.create_user() user_excluded, token = data_fixture.create_user_and_token() diff --git a/premium/backend/tests/baserow_premium_tests/row_comments/test_row_comments_handler.py b/premium/backend/tests/baserow_premium_tests/row_comments/test_row_comments_handler.py index b4c22c796c..89abed3225 100644 --- a/premium/backend/tests/baserow_premium_tests/row_comments/test_row_comments_handler.py +++ b/premium/backend/tests/baserow_premium_tests/row_comments/test_row_comments_handler.py @@ -255,7 +255,7 @@ def test_row_comment_deleted_signal_called( assert args == call(RowCommentHandler, row_comment=c, user=user, mentions=[]) -@pytest.mark.django_db(transaction=True) +@pytest.mark.django_db @override_settings(DEBUG=True) def test_row_comment_mentions_are_created(premium_data_fixture): user = premium_data_fixture.create_user( @@ -277,7 +277,7 @@ def test_row_comment_mentions_are_created(premium_data_fixture): assert list(c.mentions.all()) == [user2] -@pytest.mark.django_db(transaction=True) +@pytest.mark.django_db @override_settings(DEBUG=True) def test_row_comment_cant_mention_user_outside_workspace(premium_data_fixture): user = premium_data_fixture.create_user( @@ -297,7 +297,7 @@ def test_row_comment_cant_mention_user_outside_workspace(premium_data_fixture): assert comment.mentions.count() == 0 -@pytest.mark.django_db(transaction=True) +@pytest.mark.django_db @override_settings(DEBUG=True) def test_user_change_row_comments_notification_mode(premium_data_fixture): user = premium_data_fixture.create_user( diff --git a/premium/backend/tests/baserow_premium_tests/row_comments/test_row_comments_notification_types.py b/premium/backend/tests/baserow_premium_tests/row_comments/test_row_comments_notification_types.py index ad5ceff0c1..3be756d3aa 100644 --- a/premium/backend/tests/baserow_premium_tests/row_comments/test_row_comments_notification_types.py +++ b/premium/backend/tests/baserow_premium_tests/row_comments/test_row_comments_notification_types.py @@ -274,6 +274,7 @@ def test_email_notifications_are_created_correctly( @pytest.mark.django_db(transaction=True) @patch("baserow.ws.tasks.broadcast_to_users.apply") @override_settings(DEBUG=True) +@pytest.mark.enable_signals("baserow.ws.tasks.broadcast_to_users.delay") def test_user_receive_notification_if_subscribed_for_comments_on_a_row( mocked_broadcast_to_users, api_client, premium_data_fixture ): @@ -435,6 +436,7 @@ def post_comment(): @pytest.mark.django_db(transaction=True) @patch("baserow.ws.tasks.broadcast_to_users.apply") @override_settings(DEBUG=True) +@pytest.mark.enable_signals("baserow.ws.tasks.broadcast_to_users.delay") def test_all_interested_users_receive_the_notification_when_a_comment_is_posted( mocked_broadcast_to_users, api_client, premium_data_fixture ): @@ -516,6 +518,7 @@ def test_all_interested_users_receive_the_notification_when_a_comment_is_posted( @pytest.mark.django_db(transaction=True) @patch("baserow.ws.tasks.broadcast_to_users.apply") @override_settings(DEBUG=True) +@pytest.mark.enable_signals("baserow.ws.tasks.broadcast_to_users.delay") def test_only_users_with_access_to_the_table_receive_the_notification_for_new_comments( mocked_broadcast_to_users, api_client, premium_data_fixture ): diff --git a/premium/backend/tests/baserow_premium_tests/views/test_premium_view_notification_types.py b/premium/backend/tests/baserow_premium_tests/views/test_premium_view_notification_types.py index 3385b7457d..9fa38e01c4 100644 --- a/premium/backend/tests/baserow_premium_tests/views/test_premium_view_notification_types.py +++ b/premium/backend/tests/baserow_premium_tests/views/test_premium_view_notification_types.py @@ -14,6 +14,7 @@ @pytest.mark.django_db(transaction=True) @override_settings(DEBUG=True) @patch("baserow.ws.tasks.broadcast_to_users.apply") +@pytest.mark.enable_signals("baserow.ws.tasks.broadcast_to_users.delay") def test_user_stop_receiving_notification_if_another_user_change_view_ownership( mocked_broadcast_to_users, api_client, premium_data_fixture ): diff --git a/premium/backend/tests/baserow_premium_tests/ws/test_ws_row_comments_signals.py b/premium/backend/tests/baserow_premium_tests/ws/test_ws_row_comments_signals.py index a01f52b567..782ad06337 100644 --- a/premium/backend/tests/baserow_premium_tests/ws/test_ws_row_comments_signals.py +++ b/premium/backend/tests/baserow_premium_tests/ws/test_ws_row_comments_signals.py @@ -200,6 +200,7 @@ def test_row_comment_restored(premium_data_fixture): @pytest.mark.django_db(transaction=True) @patch("baserow.ws.tasks.broadcast_to_users.apply") @override_settings(DEBUG=True) +@pytest.mark.enable_signals("baserow.ws.tasks.broadcast_to_users.delay") def test_row_comments_notification_mode_updated( mocked_broadcast_to_users, premium_data_fixture, From a2f9ee7387eb26837b6ea7aa3c4319acce0701fc Mon Sep 17 00:00:00 2001 From: Tsering Paljor Date: Mon, 8 Dec 2025 11:35:22 +0400 Subject: [PATCH 2/3] Handle duplicate error gracefully for Page/Workflow rename (#4271) * Ensure that duplicate workflow name error is correctly handled * Ensure that duplicate page name error is correctly handled * Add builder changelog * Remove dev-specific safety checks. * Add automation changelog --- .../contrib/automation/workflows/handler.py | 3 ++- .../baserow/contrib/builder/pages/handler.py | 5 ++-- .../api/workflows/test_workflow_views.py | 23 +++++++++++++++++++ .../builder/api/pages/test_page_views.py | 21 +++++++++++++++++ ...page_is_being_renamed_to_an_existing_.json | 9 ++++++++ ...hen_a_workflow_is_being_renamed_to_an.json | 9 ++++++++ .../modules/automation/locales/en.json | 3 ++- web-frontend/modules/automation/plugin.js | 6 +++++ web-frontend/modules/builder/locales/en.json | 1 + web-frontend/modules/builder/plugin.js | 6 +++++ 10 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 changelog/entries/unreleased/bug/4183_gracefully_handle_when_page_is_being_renamed_to_an_existing_.json create mode 100644 changelog/entries/unreleased/bug/4183_show_a_specific_error_when_a_workflow_is_being_renamed_to_an.json diff --git a/backend/src/baserow/contrib/automation/workflows/handler.py b/backend/src/baserow/contrib/automation/workflows/handler.py index 1c7f03e384..96d3650e91 100644 --- a/backend/src/baserow/contrib/automation/workflows/handler.py +++ b/backend/src/baserow/contrib/automation/workflows/handler.py @@ -46,6 +46,7 @@ from baserow.contrib.automation.workflows.types import UpdatedAutomationWorkflow from baserow.core.cache import global_cache, local_cache from baserow.core.exceptions import IdDoesNotExist +from baserow.core.psycopg import is_unique_violation_error from baserow.core.registries import ImportExportConfig from baserow.core.services.exceptions import DispatchException from baserow.core.storage import ExportZipFile, get_default_storage @@ -247,7 +248,7 @@ def update_workflow( try: workflow.save() except IntegrityError as e: - if "unique constraint" in e.args[0] and "name" in e.args[0]: + if is_unique_violation_error(e) and "name" in str(e): raise AutomationWorkflowNameNotUnique( name=workflow.name, automation_id=workflow.automation_id ) from e diff --git a/backend/src/baserow/contrib/builder/pages/handler.py b/backend/src/baserow/contrib/builder/pages/handler.py index 021d675df9..8f3b1e31e7 100644 --- a/backend/src/baserow/contrib/builder/pages/handler.py +++ b/backend/src/baserow/contrib/builder/pages/handler.py @@ -42,6 +42,7 @@ ) from baserow.core.cache import global_cache from baserow.core.exceptions import IdDoesNotExist +from baserow.core.psycopg import is_unique_violation_error from baserow.core.storage import ExportZipFile from baserow.core.user_sources.user_source_user import UserSourceUser from baserow.core.utils import ChildProgressBuilder, MirrorDict, find_unused_name @@ -201,9 +202,9 @@ def update_page(self, page: Page, **kwargs) -> Page: try: page.save() except IntegrityError as e: - if "unique constraint" in e.args[0] and "name" in e.args[0]: + if is_unique_violation_error(e) and "name" in e.args[0]: raise PageNameNotUnique(name=page.name, builder_id=page.builder_id) - if "unique constraint" in e.args[0] and "path" in e.args[0]: + if is_unique_violation_error(e) and "path" in e.args[0]: raise PagePathNotUnique(path=page.path, builder_id=page.builder_id) raise e diff --git a/backend/tests/baserow/contrib/automation/api/workflows/test_workflow_views.py b/backend/tests/baserow/contrib/automation/api/workflows/test_workflow_views.py index b33a08ce9f..37db535afc 100644 --- a/backend/tests/baserow/contrib/automation/api/workflows/test_workflow_views.py +++ b/backend/tests/baserow/contrib/automation/api/workflows/test_workflow_views.py @@ -596,3 +596,26 @@ def test_get_workflow_history_permission_error(api_client, data_fixture): "detail": "You don't have the required permission to execute this operation.", "error": "PERMISSION_DENIED", } + + +@pytest.mark.django_db +def test_rename_workflow_using_existing_workflow_name(api_client, data_fixture): + user, token = data_fixture.create_user_and_token() + automation = data_fixture.create_automation_application(user) + workflow_1 = data_fixture.create_automation_workflow( + user, automation=automation, name="test1", order=1 + ) + workflow_2 = data_fixture.create_automation_workflow( + user, automation=automation, name="test2", order=2 + ) + + url = reverse(API_URL_WORKFLOW_ITEM, kwargs={"workflow_id": workflow_2.id}) + response = api_client.patch( + url, + {"name": workflow_1.name}, + format="json", + HTTP_AUTHORIZATION=f"JWT {token}", + ) + + assert response.status_code == HTTP_400_BAD_REQUEST + assert response.json()["error"] == "ERROR_AUTOMATION_WORKFLOW_NAME_NOT_UNIQUE" diff --git a/backend/tests/baserow/contrib/builder/api/pages/test_page_views.py b/backend/tests/baserow/contrib/builder/api/pages/test_page_views.py index e6360b7bb9..b9a90e53c1 100644 --- a/backend/tests/baserow/contrib/builder/api/pages/test_page_views.py +++ b/backend/tests/baserow/contrib/builder/api/pages/test_page_views.py @@ -709,3 +709,24 @@ def test_delete_shared_page(api_client, data_fixture): assert response.status_code == HTTP_400_BAD_REQUEST assert response.json()["error"] == "ERROR_SHARED_PAGE_READ_ONLY" + + +@pytest.mark.django_db +def test_rename_page_using_existing_page_name(api_client, data_fixture): + user, token = data_fixture.create_user_and_token() + builder = data_fixture.create_builder_application(user=user) + page_1 = data_fixture.create_builder_page(builder=builder, order=1, name="test1") + page_2 = data_fixture.create_builder_page(builder=builder, order=1, name="test2") + + url = reverse("api:builder:pages:item", kwargs={"page_id": page_2.id}) + response = api_client.patch( + url, + { + "name": page_1.name, + }, + format="json", + HTTP_AUTHORIZATION=f"JWT {token}", + ) + + assert response.status_code == HTTP_400_BAD_REQUEST + assert response.json()["error"] == "ERROR_PAGE_NAME_NOT_UNIQUE" diff --git a/changelog/entries/unreleased/bug/4183_gracefully_handle_when_page_is_being_renamed_to_an_existing_.json b/changelog/entries/unreleased/bug/4183_gracefully_handle_when_page_is_being_renamed_to_an_existing_.json new file mode 100644 index 0000000000..e3264091d3 --- /dev/null +++ b/changelog/entries/unreleased/bug/4183_gracefully_handle_when_page_is_being_renamed_to_an_existing_.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Show a specific error when a page is being renamed to an existing page's name.", + "issue_origin": "github", + "issue_number": 4183, + "domain": "builder", + "bullet_points": [], + "created_at": "2025-11-17" +} \ No newline at end of file diff --git a/changelog/entries/unreleased/bug/4183_show_a_specific_error_when_a_workflow_is_being_renamed_to_an.json b/changelog/entries/unreleased/bug/4183_show_a_specific_error_when_a_workflow_is_being_renamed_to_an.json new file mode 100644 index 0000000000..23c002db89 --- /dev/null +++ b/changelog/entries/unreleased/bug/4183_show_a_specific_error_when_a_workflow_is_being_renamed_to_an.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Show a specific error when a workflow is being renamed to an existing workflow's name.", + "issue_origin": "github", + "issue_number": 4183, + "domain": "automation", + "bullet_points": [], + "created_at": "2025-12-08" +} \ No newline at end of file diff --git a/web-frontend/modules/automation/locales/en.json b/web-frontend/modules/automation/locales/en.json index 39c090ea89..25169a5ef3 100644 --- a/web-frontend/modules/automation/locales/en.json +++ b/web-frontend/modules/automation/locales/en.json @@ -46,7 +46,8 @@ "duplicatedTitle": "Workflow duplicated" }, "automationWorkflowErrors": { - "errorNameNotUnique": "A workflow with this name already exists" + "errorNameNotUnique": "A workflow with this name already exists", + "errorNameNotUniqueDescription": "Please enter a unique name for the workflow" }, "trashType": { "workflow": "workflow", diff --git a/web-frontend/modules/automation/plugin.js b/web-frontend/modules/automation/plugin.js index 16e84e3323..2eba6e57b7 100644 --- a/web-frontend/modules/automation/plugin.js +++ b/web-frontend/modules/automation/plugin.js @@ -70,6 +70,12 @@ export default (context) => { registerRealtimeEvents(app.$realtime) + app.$clientErrorMap.setError( + 'ERROR_AUTOMATION_WORKFLOW_NAME_NOT_UNIQUE', + app.i18n.t('automationWorkflowErrors.errorNameNotUnique'), + app.i18n.t('automationWorkflowErrors.errorNameNotUniqueDescription') + ) + store.registerModule('automationApplication', automationApplicationStore) store.registerModule('automationWorkflow', automationWorkflowStore) store.registerModule('automationWorkflowNode', automationWorkflowNodeStore) diff --git a/web-frontend/modules/builder/locales/en.json b/web-frontend/modules/builder/locales/en.json index e413432aa4..b066fbf43e 100644 --- a/web-frontend/modules/builder/locales/en.json +++ b/web-frontend/modules/builder/locales/en.json @@ -56,6 +56,7 @@ }, "pageErrors": { "errorNameNotUnique": "A page with this name already exists", + "errorNameNotUniqueDescription": "Please enter a unique name for the page", "errorPathNotUnique": "A path with this name already exists", "errorStartingSlash": "A path needs to start with a '/'", "errorValidPathCharacters": "The path contains invalid characters", diff --git a/web-frontend/modules/builder/plugin.js b/web-frontend/modules/builder/plugin.js index 5eba13261d..cd76887898 100644 --- a/web-frontend/modules/builder/plugin.js +++ b/web-frontend/modules/builder/plugin.js @@ -172,6 +172,12 @@ export default (context) => { registerRealtimeEvents(app.$realtime) + app.$clientErrorMap.setError( + 'ERROR_PAGE_NAME_NOT_UNIQUE', + app.i18n.t('pageErrors.errorNameNotUnique'), + app.i18n.t('pageErrors.errorNameNotUniqueDescription') + ) + store.registerModule('page', pageStore) store.registerModule('element', elementStore) store.registerModule('domain', domainStore) From 6d81aaafef8981b0f47c1e0aafb7ccaa4995e354 Mon Sep 17 00:00:00 2001 From: Tsering Paljor Date: Mon, 8 Dec 2025 11:35:33 +0400 Subject: [PATCH 3/3] Improve domain handler to catch duplicate domain error (#4400) * Add error handling when handler tries to create a duplicate domain * Add changelog * Simplify error handling. * Rephrase changelog message --- .../contrib/builder/domains/handler.py | 11 ++++-- .../builder/domains/test_domain_handler.py | 34 +++++++++++++++++++ ...en_the_domain_handler_tries_to_create.json | 9 +++++ 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 changelog/entries/unreleased/bug/4399_added_error_handling_when_the_domain_handler_tries_to_create.json diff --git a/backend/src/baserow/contrib/builder/domains/handler.py b/backend/src/baserow/contrib/builder/domains/handler.py index 9a1cbc9660..d97eedf8d4 100644 --- a/backend/src/baserow/contrib/builder/domains/handler.py +++ b/backend/src/baserow/contrib/builder/domains/handler.py @@ -17,6 +17,7 @@ from baserow.core.db import specific_iterator from baserow.core.exceptions import IdDoesNotExist from baserow.core.models import Workspace +from baserow.core.psycopg import is_unique_violation_error from baserow.core.registries import ImportExportConfig, application_type_registry from baserow.core.storage import get_default_storage from baserow.core.trash.handler import TrashHandler @@ -131,7 +132,13 @@ def create_domain( prepared_values["domain_name"] = prepared_values["domain_name"].lower() domain = model_class(builder=builder, order=last_order, **prepared_values) - domain.save() + + try: + domain.save() + except IntegrityError as error: + if is_unique_violation_error(error): + raise DomainNameNotUniqueError(prepared_values["domain_name"]) + raise error return domain @@ -171,7 +178,7 @@ def update_domain(self, domain: Domain, **kwargs) -> Domain: try: domain.save() except IntegrityError as error: - if "unique" in str(error) and "domain_name" in prepared_values: + if is_unique_violation_error(error): raise DomainNameNotUniqueError(prepared_values["domain_name"]) raise error diff --git a/backend/tests/baserow/contrib/builder/domains/test_domain_handler.py b/backend/tests/baserow/contrib/builder/domains/test_domain_handler.py index da944772b2..2530ac2dbb 100644 --- a/backend/tests/baserow/contrib/builder/domains/test_domain_handler.py +++ b/backend/tests/baserow/contrib/builder/domains/test_domain_handler.py @@ -3,6 +3,7 @@ from baserow.contrib.builder.domains.domain_types import CustomDomainType from baserow.contrib.builder.domains.exceptions import ( DomainDoesNotExist, + DomainNameNotUniqueError, DomainNotInBuilder, ) from baserow.contrib.builder.domains.handler import DomainHandler @@ -68,6 +69,21 @@ def test_create_domain(data_fixture): assert domain.domain_name == "test.com" +@pytest.mark.django_db +def test_create_domain_with_duplicate_name(data_fixture): + builder = data_fixture.create_builder_application() + domain_name = "test.com" + + DomainHandler().create_domain(CustomDomainType(), builder, domain_name=domain_name) + + with pytest.raises(DomainNameNotUniqueError) as exc_info: + DomainHandler().create_domain( + CustomDomainType(), builder, domain_name=domain_name + ) + + assert exc_info.value.domain_name == domain_name + + @pytest.mark.django_db def test_delete_domain(data_fixture): domain = data_fixture.create_builder_custom_domain() @@ -88,6 +104,24 @@ def test_update_domain(data_fixture): assert domain.domain_name == "new.com" +@pytest.mark.django_db +def test_update_domain_with_duplicate_name(data_fixture): + builder = data_fixture.create_builder_application() + domain = data_fixture.create_builder_custom_domain( + domain_name="test.com", builder=builder + ) + + existing_domain = "other.com" + DomainHandler().create_domain( + CustomDomainType(), builder, domain_name=existing_domain + ) + + with pytest.raises(DomainNameNotUniqueError) as exc_info: + DomainHandler().update_domain(domain, domain_name=existing_domain) + + assert exc_info.value.domain_name == existing_domain + + @pytest.mark.django_db def test_order_domains(data_fixture): builder = data_fixture.create_builder_application() diff --git a/changelog/entries/unreleased/bug/4399_added_error_handling_when_the_domain_handler_tries_to_create.json b/changelog/entries/unreleased/bug/4399_added_error_handling_when_the_domain_handler_tries_to_create.json new file mode 100644 index 0000000000..5cd5761991 --- /dev/null +++ b/changelog/entries/unreleased/bug/4399_added_error_handling_when_the_domain_handler_tries_to_create.json @@ -0,0 +1,9 @@ +{ + "type": "bug", + "message": "Fixed a bug where domain names were not correctly validated during domain creation.", + "issue_origin": "github", + "issue_number": 4399, + "domain": "builder", + "bullet_points": [], + "created_at": "2025-12-05" +} \ No newline at end of file