| # 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. |
| |
| import os |
| from unittest.mock import MagicMock, patch, PropertyMock |
| |
| from sqlalchemy.exc import OperationalError |
| |
| from superset.app import AppRootMiddleware, create_app, SupersetApp |
| from superset.commands.database.exceptions import DatabaseInvalidError |
| 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" |
| ) |
| |
| def test_check_and_warn_database_connection_masks_password(self) -> None: |
| mock_app = MagicMock() |
| mock_app.app_context.return_value.__enter__.return_value = MagicMock() |
| |
| with patch("superset.initialization.db") as mock_db: |
| mock_db.engine.execute.side_effect = Exception("Connection Failed") |
| |
| with patch.object( |
| SupersetAppInitializer, |
| "database_uri", |
| new_callable=PropertyMock, |
| ) as mock_uri: |
| mock_uri.return_value = "postgresql://user:secretpass@localhost:5432/db" |
| app_initializer = SupersetAppInitializer(mock_app) |
| |
| with patch("builtins.print") as mock_print: |
| app_initializer.check_and_warn_database_connection() |
| |
| mock_print.assert_called_once() |
| output = mock_print.call_args[0][0] |
| assert "secretpass" not in output |
| assert "postgresql://user:***@localhost:5432/db" in output |
| |
| def test_check_and_warn_database_connection_invalid_uri(self) -> None: |
| """Test that invalid URIs are handled safely without crashing.""" |
| mock_app = MagicMock() |
| mock_app.app_context.return_value.__enter__.return_value = MagicMock() |
| |
| with patch("superset.initialization.db") as mock_db: |
| mock_db.engine.execute.side_effect = Exception("Connection Failed") |
| |
| with patch.object( |
| SupersetAppInitializer, |
| "database_uri", |
| new_callable=PropertyMock, |
| ) as mock_uri: |
| mock_uri.return_value = "sqlite://" |
| app_initializer = SupersetAppInitializer(mock_app) |
| |
| with patch( |
| "superset.initialization.make_url_safe", |
| side_effect=DatabaseInvalidError(), |
| ): |
| with patch("builtins.print") as mock_print: |
| app_initializer.check_and_warn_database_connection() |
| |
| mock_print.assert_called_once() |
| output = mock_print.call_args[0][0] |
| assert ( |
| "ERROR: Cannot connect to database <invalid database URI>" in output |
| ) |
| |
| |
| class TestCreateAppRoot: |
| """Test app root resolution precedence in create_app.""" |
| |
| @patch("superset.initialization.SupersetAppInitializer.init_app") |
| def test_default_app_root_no_middleware(self, mock_init_app): |
| """No param, no config, no env var: app_root is '/', no middleware.""" |
| env = os.environ.copy() |
| env.pop("SUPERSET_APP_ROOT", None) |
| env.pop("SUPERSET_CONFIG", None) |
| with patch.dict(os.environ, env, clear=True): |
| app = create_app() |
| |
| assert not isinstance(app.wsgi_app, AppRootMiddleware) |
| |
| @patch("superset.initialization.SupersetAppInitializer.init_app") |
| def test_application_root_config_activates_middleware(self, mock_init_app): |
| """APPLICATION_ROOT in config activates AppRootMiddleware.""" |
| env = os.environ.copy() |
| env.pop("SUPERSET_APP_ROOT", None) |
| env.pop("SUPERSET_CONFIG", None) |
| with ( |
| patch.dict(os.environ, env, clear=True), |
| patch("superset.config.APPLICATION_ROOT", "/from-config", create=True), |
| ): |
| app = create_app() |
| |
| assert isinstance(app.wsgi_app, AppRootMiddleware) |
| assert app.wsgi_app.app_root == "/from-config" |
| |
| @patch("superset.initialization.SupersetAppInitializer.init_app") |
| def test_env_var_activates_middleware(self, mock_init_app): |
| """SUPERSET_APP_ROOT env var activates AppRootMiddleware.""" |
| env = os.environ.copy() |
| env.pop("SUPERSET_CONFIG", None) |
| env["SUPERSET_APP_ROOT"] = "/from-env" |
| with patch.dict(os.environ, env, clear=True): |
| app = create_app() |
| |
| assert isinstance(app.wsgi_app, AppRootMiddleware) |
| assert app.wsgi_app.app_root == "/from-env" |
| |
| @patch("superset.initialization.SupersetAppInitializer.init_app") |
| def test_env_var_takes_precedence_over_config(self, mock_init_app): |
| """SUPERSET_APP_ROOT env var wins over APPLICATION_ROOT config.""" |
| env = os.environ.copy() |
| env.pop("SUPERSET_CONFIG", None) |
| env["SUPERSET_APP_ROOT"] = "/from-env" |
| with ( |
| patch.dict(os.environ, env, clear=True), |
| patch("superset.config.APPLICATION_ROOT", "/from-config", create=True), |
| ): |
| app = create_app() |
| |
| assert isinstance(app.wsgi_app, AppRootMiddleware) |
| assert app.wsgi_app.app_root == "/from-env" |
| |
| @patch("superset.initialization.SupersetAppInitializer.init_app") |
| def test_param_takes_precedence_over_env_var(self, mock_init_app): |
| """superset_app_root param wins over SUPERSET_APP_ROOT env var.""" |
| env = os.environ.copy() |
| env.pop("SUPERSET_CONFIG", None) |
| env["SUPERSET_APP_ROOT"] = "/from-env" |
| with patch.dict(os.environ, env, clear=True): |
| app = create_app(superset_app_root="/from-param") |
| |
| assert isinstance(app.wsgi_app, AppRootMiddleware) |
| assert app.wsgi_app.app_root == "/from-param" |