| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| |
| from unittest.mock import MagicMock, patch |
| |
| from sqlalchemy.exc import OperationalError |
| |
| from superset.app import SupersetApp |
| from superset.initialization import SupersetAppInitializer |
| |
| |
| class TestSupersetApp: |
| @patch("superset.app.logger") |
| def test_sync_config_to_db_skips_when_no_tables(self, mock_logger): |
| """Test that sync is skipped when database is not up-to-date.""" |
| # Setup |
| app = SupersetApp(__name__) |
| app.config = {"SQLALCHEMY_DATABASE_URI": "postgresql://user:pass@host:5432/db"} |
| |
| # Mock _is_database_up_to_date to return False |
| with patch.object(app, "_is_database_up_to_date", return_value=False): |
| # Execute |
| app.sync_config_to_db() |
| |
| # Assert |
| mock_logger.info.assert_called_once_with( |
| "Pending database migrations: run 'superset db upgrade'" |
| ) |
| |
| @patch("superset.extensions.db") |
| @patch("superset.app.logger") |
| def test_sync_config_to_db_handles_operational_error(self, mock_logger, mock_db): |
| """Test that OperationalError during migration check is handled gracefully.""" |
| # Setup |
| app = SupersetApp(__name__) |
| app.config = {"SQLALCHEMY_DATABASE_URI": "postgresql://user:pass@host:5432/db"} |
| error_msg = "Cannot connect to database" |
| |
| # Mock db.engine.connect to raise an OperationalError |
| mock_db.engine.connect.side_effect = OperationalError(error_msg, None, None) |
| |
| # Execute |
| app.sync_config_to_db() |
| |
| # Assert - _is_database_up_to_date should catch the error and return False |
| # which causes the info log about pending migrations |
| mock_logger.info.assert_called_once_with( |
| "Pending database migrations: run 'superset db upgrade'" |
| ) |
| |
| @patch("superset.extensions.feature_flag_manager") |
| @patch("superset.app.logger") |
| @patch("superset.commands.theme.seed.SeedSystemThemesCommand") |
| def test_sync_config_to_db_initializes_when_tables_exist( |
| self, |
| mock_seed_themes_command, |
| mock_logger, |
| mock_feature_flag_manager, |
| ): |
| """Test that features are initialized when database is up-to-date.""" |
| # Setup |
| app = SupersetApp(__name__) |
| app.config = {"SQLALCHEMY_DATABASE_URI": "postgresql://user:pass@host:5432/db"} |
| mock_feature_flag_manager.is_feature_enabled.return_value = True |
| mock_seed_themes = MagicMock() |
| mock_seed_themes_command.return_value = mock_seed_themes |
| |
| # Mock _is_database_up_to_date to return True |
| with ( |
| patch.object(app, "_is_database_up_to_date", return_value=True), |
| patch( |
| "superset.tags.core.register_sqla_event_listeners" |
| ) as mock_register_listeners, |
| ): |
| # Execute |
| app.sync_config_to_db() |
| |
| # Assert |
| mock_feature_flag_manager.is_feature_enabled.assert_called_with( |
| "TAGGING_SYSTEM" |
| ) |
| mock_register_listeners.assert_called_once() |
| # Should seed themes |
| mock_seed_themes_command.assert_called_once() |
| mock_seed_themes.run.assert_called_once() |
| # Should log successful completion |
| mock_logger.info.assert_any_call("Syncing configuration to database...") |
| mock_logger.info.assert_any_call( |
| "Configuration sync to database completed successfully" |
| ) |
| |
| |
| class TestSupersetAppInitializer: |
| @patch("superset.initialization.logger") |
| def test_init_app_in_ctx_calls_sync_config_to_db(self, mock_logger): |
| """Test that initialization calls app.sync_config_to_db().""" |
| # Setup |
| mock_app = MagicMock() |
| mock_app.config = { |
| "SQLALCHEMY_DATABASE_URI": "postgresql://user:pass@host:5432/db", |
| "FLASK_APP_MUTATOR": None, |
| } |
| app_initializer = SupersetAppInitializer(mock_app) |
| |
| # Execute init_app_in_ctx which calls sync_config_to_db |
| with ( |
| patch.object(app_initializer, "configure_fab"), |
| patch.object(app_initializer, "configure_url_map_converters"), |
| patch.object(app_initializer, "configure_data_sources"), |
| patch.object(app_initializer, "configure_auth_provider"), |
| patch.object(app_initializer, "configure_async_queries"), |
| patch.object(app_initializer, "configure_ssh_manager"), |
| patch.object(app_initializer, "configure_stats_manager"), |
| patch.object(app_initializer, "init_views"), |
| ): |
| app_initializer.init_app_in_ctx() |
| |
| # Assert that sync_config_to_db was called on the app |
| mock_app.sync_config_to_db.assert_called_once() |
| |
| def test_database_uri_lazy_property(self): |
| """Test database_uri property uses lazy initialization with smart caching.""" |
| # Setup |
| mock_app = MagicMock() |
| test_uri = "postgresql://user:pass@host:5432/testdb" |
| mock_app.config = {"SQLALCHEMY_DATABASE_URI": test_uri} |
| app_initializer = SupersetAppInitializer(mock_app) |
| |
| # Ensure cache is None initially |
| assert app_initializer._db_uri_cache is None |
| |
| # First access should set the cache (valid URI) |
| uri = app_initializer.database_uri |
| assert uri == test_uri |
| assert app_initializer._db_uri_cache is not None |
| assert app_initializer._db_uri_cache == test_uri |
| |
| # Second access should use cache (not call config.get again) |
| # Change the config to verify cache is being used |
| mock_app.config["SQLALCHEMY_DATABASE_URI"] = "different_uri" |
| uri2 = app_initializer.database_uri |
| assert ( |
| uri2 == test_uri |
| ) # Should still return cached value (not "different_uri") |
| |
| def test_database_uri_doesnt_cache_fallback_values(self): |
| """Test that fallback values like 'nouser' are not cached.""" |
| # Setup |
| mock_app = MagicMock() |
| |
| # Initially return the fallback nouser URI |
| config_dict = { |
| "SQLALCHEMY_DATABASE_URI": "postgresql://nouser:nopassword@nohost:5432/nodb" |
| } |
| mock_app.config = config_dict |
| app_initializer = SupersetAppInitializer(mock_app) |
| |
| # First access returns fallback but shouldn't cache it |
| uri1 = app_initializer.database_uri |
| assert uri1 == "postgresql://nouser:nopassword@nohost:5432/nodb" |
| assert app_initializer._db_uri_cache is None # Should NOT be cached |
| |
| # Now config is properly loaded - update the same dict |
| config_dict["SQLALCHEMY_DATABASE_URI"] = ( |
| "postgresql://realuser:realpass@realhost:5432/realdb" |
| ) |
| |
| # Second access should get the new value since fallback wasn't cached |
| uri2 = app_initializer.database_uri |
| assert uri2 == "postgresql://realuser:realpass@realhost:5432/realdb" |
| assert app_initializer._db_uri_cache is not None # Now it should be cached |
| assert ( |
| app_initializer._db_uri_cache |
| == "postgresql://realuser:realpass@realhost:5432/realdb" |
| ) |