diff --git a/changelog/entries/unreleased/bug/3375_builder_fixed_bug_that_caused_a_workspace_import_to_fail_whe.json b/changelog/entries/unreleased/bug/3375_builder_fixed_bug_that_caused_a_workspace_import_to_fail_whe.json new file mode 100644 index 000000000..b82fbb37c --- /dev/null +++ b/changelog/entries/unreleased/bug/3375_builder_fixed_bug_that_caused_a_workspace_import_to_fail_whe.json @@ -0,0 +1,7 @@ +{ + "type": "bug", + "message": "[Builder] Fixed bug that caused a workspace import to fail when certain fields were missing.", + "issue_number": 3375, + "bullet_points": [], + "created_at": "2025-01-28" +} \ No newline at end of file diff --git a/enterprise/backend/src/baserow_enterprise/integrations/local_baserow/auth_provider_types.py b/enterprise/backend/src/baserow_enterprise/integrations/local_baserow/auth_provider_types.py index f475496c5..6a86d5ad9 100644 --- a/enterprise/backend/src/baserow_enterprise/integrations/local_baserow/auth_provider_types.py +++ b/enterprise/backend/src/baserow_enterprise/integrations/local_baserow/auth_provider_types.py @@ -187,7 +187,7 @@ class LocalBaserowPasswordAppAuthProviderType(AppAuthProviderType): and value and "database_fields" in id_mapping ): - return id_mapping["database_fields"][value] + return id_mapping["database_fields"].get(value) return super().deserialize_property( prop_name, diff --git a/enterprise/backend/src/baserow_enterprise/integrations/local_baserow/user_source_types.py b/enterprise/backend/src/baserow_enterprise/integrations/local_baserow/user_source_types.py index 9d90f1092..e984c2411 100644 --- a/enterprise/backend/src/baserow_enterprise/integrations/local_baserow/user_source_types.py +++ b/enterprise/backend/src/baserow_enterprise/integrations/local_baserow/user_source_types.py @@ -416,14 +416,14 @@ class LocalBaserowUserSourceType(UserSourceType): """ if value and "database_tables" in id_mapping and prop_name == "table_id": - return id_mapping["database_tables"][value] + return id_mapping["database_tables"].get(value) if ( value and "database_fields" in id_mapping and prop_name in ("email_field_id", "name_field_id", "role_field_id") ): - return id_mapping["database_fields"][value] + return id_mapping["database_fields"].get(value) return super().deserialize_property( prop_name, diff --git a/enterprise/backend/tests/baserow_enterprise_tests/integrations/local_baserow/test_auth_provider_types.py b/enterprise/backend/tests/baserow_enterprise_tests/integrations/local_baserow/test_auth_provider_types.py index 040eab826..50266170e 100644 --- a/enterprise/backend/tests/baserow_enterprise_tests/integrations/local_baserow/test_auth_provider_types.py +++ b/enterprise/backend/tests/baserow_enterprise_tests/integrations/local_baserow/test_auth_provider_types.py @@ -395,3 +395,75 @@ def test_import_local_baserow_password_app_auth_provider(data_fixture): imported_instance.auth_providers.first().specific.password_field_id == password_field.id ) + + +@pytest.mark.django_db +def test_import_local_baserow_password_app_auth_provider_without_database(data_fixture): + """ + Test the import of the LocalBaserowPasswordAppAuthProvider when the + password field is missing in the id_mapping. + + The password field might be missing during an import, because the user + might have either deleted the password field in the database, or deleted + the table entirely. + """ + + user = data_fixture.create_user() + workspace = data_fixture.create_workspace(user=user) + application = data_fixture.create_builder_application(workspace=workspace) + database = data_fixture.create_database_application(workspace=workspace) + + integration = data_fixture.create_local_baserow_integration( + application=application, user=user + ) + + table_from_same_workspace1, fields, rows = data_fixture.build_table( + user=user, + database=database, + columns=[ + ("Email", "text"), + ("Name", "text"), + ("Password", "password"), + ], + rows=[ + ["test@baserow.io", "Test", "password"], + ], + ) + + email_field, name_field, password_field = fields + + TO_IMPORT = { + "email_field_id": 42, + "id": 28, + "integration_id": 42, + "name": "Test name", + "name_field_id": 43, + "order": "1.00000000000000000000", + "table_id": 42, + "type": "local_baserow", + "auth_providers": [ + { + "id": 42, + "type": "local_baserow_password", + "domain": None, + "enabled": True, + "password_field_id": 44, + } + ], + } + + id_mapping = defaultdict(MirrorDict) + id_mapping["integrations"] = {42: integration.id} + id_mapping["database_tables"] = {42: table_from_same_workspace1.id} + + # the password field is intentionally excluded from database_fields + id_mapping["database_fields"] = { + 42: email_field.id, + 43: name_field.id, + } + + imported_instance = UserSourceHandler().import_user_source( + application, TO_IMPORT, id_mapping + ) + + assert imported_instance.auth_providers.first().specific.password_field_id is None diff --git a/enterprise/backend/tests/baserow_enterprise_tests/integrations/local_baserow/test_user_source_types.py b/enterprise/backend/tests/baserow_enterprise_tests/integrations/local_baserow/test_user_source_types.py index 613a1b977..cbcf25521 100644 --- a/enterprise/backend/tests/baserow_enterprise_tests/integrations/local_baserow/test_user_source_types.py +++ b/enterprise/backend/tests/baserow_enterprise_tests/integrations/local_baserow/test_user_source_types.py @@ -868,6 +868,105 @@ def test_import_local_baserow_user_source(data_fixture): assert imported_instance.name_field_id == name_field.id +@pytest.mark.django_db +@pytest.mark.parametrize( + "id_mapping_key,missing_field_name", + [ + ("database_tables", "table_id"), + ("database_fields", "email_field_id"), + ("database_fields", "name_field_id"), + ("database_fields", "role_field_id"), + ], +) +def test_import_local_baserow_user_source_with_missing_fields( + data_fixture, id_mapping_key, missing_field_name +): + """ + Test the import of the LocalBaserowUserSource when one or more fields + are missing in the exported workspace's database. + + A field, such as the Role field might have been deleted before the user + has exported the workspace. As such, when importing the workspace, the + missing field needs to be safely handled. + """ + + user = data_fixture.create_user() + workspace = data_fixture.create_workspace(user=user) + application = data_fixture.create_builder_application(workspace=workspace) + database = data_fixture.create_database_application(workspace=workspace) + + integration = data_fixture.create_local_baserow_integration( + application=application, user=user + ) + + table_from_same_workspace1, fields, rows = data_fixture.build_table( + user=user, + database=database, + columns=[ + ("Email", "text"), + ("Name", "text"), + ("Role", "text"), + ], + rows=[ + ["test@baserow.io", "Test", "Foo Role"], + ], + ) + + email_field, name_field, role_field = fields + + TO_IMPORT = { + "id": 28, + "name": "Test name", + "order": "1.00000000000000000000", + "type": "local_baserow", + "integration_id": 42, + "table_id": 42, + "name_field_id": 43, + "email_field_id": 42, + "role_field_id": 44, + "auth_providers": [ + { + "id": 42, + "type": "local_baserow_password", + "domain": None, + "enabled": True, + "password_field_id": None, + } + ], + } + + id_mapping = defaultdict(MirrorDict) + + id_mapping_data = { + "table_id": (42, table_from_same_workspace1.id), + "email_field_id": (42, email_field.id), + "name_field_id": (43, name_field.id), + "role_field_id": (44, role_field.id), + } + id_mapping["integrations"] = {42: integration.id} + id_mapping["database_tables"] = {42: table_from_same_workspace1.id} + id_mapping["database_fields"] = { + 42: email_field.id, + 43: name_field.id, + 44: role_field.id, + } + + # Remove a specific field to simulate a "missing field" + id_mapping[id_mapping_key].pop(id_mapping_data[missing_field_name][0]) + + imported_instance = UserSourceHandler().import_user_source( + application, TO_IMPORT, id_mapping + ) + + for property in id_mapping_data: + value = getattr(imported_instance, property) + if property == missing_field_name: + # Ensure that None is returned instead of raising a KeyError + assert value is None + else: + assert value is id_mapping_data[property][1] + + @pytest.mark.django_db def test_create_local_baserow_user_source_w_auth_providers(api_client, data_fixture): user, token = data_fixture.create_user_and_token()