Files
superset2/tests/unit_tests/initialization_test.py

260 lines
11 KiB
Python

# 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
from sqlalchemy.exc import OperationalError
from superset.app import AppRootMiddleware, create_app, 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"
)
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"