mirror of
https://github.com/apache/superset.git
synced 2026-04-10 03:45:22 +00:00
431 lines
17 KiB
Python
431 lines
17 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.
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from sqlalchemy.exc import OperationalError
|
|
|
|
from superset.initialization import SupersetAppInitializer
|
|
|
|
|
|
class TestSupersetAppInitializer:
|
|
@patch("superset.initialization.db")
|
|
@patch("superset.initialization.inspect")
|
|
@patch("superset.initialization.logger")
|
|
def test_init_database_dependent_features_skips_when_no_tables(
|
|
self, mock_logger, mock_inspect_func, mock_db
|
|
):
|
|
"""Test that initialization is skipped when core tables don't exist."""
|
|
# Setup
|
|
mock_app = MagicMock()
|
|
app_initializer = SupersetAppInitializer(mock_app)
|
|
mock_inspector = MagicMock()
|
|
mock_inspector.has_table.return_value = False
|
|
mock_inspect_func.return_value = mock_inspector
|
|
mock_db.engine = MagicMock()
|
|
|
|
# Execute
|
|
app_initializer._init_database_dependent_features()
|
|
|
|
# Assert
|
|
mock_inspect_func.assert_called_once_with(mock_db.engine)
|
|
mock_inspector.has_table.assert_called_once_with("dashboards")
|
|
mock_logger.debug.assert_called_once_with(
|
|
"Superset tables not yet created. Skipping database-dependent "
|
|
"initialization. These features will be initialized after "
|
|
"migration."
|
|
)
|
|
|
|
@patch("superset.initialization.db")
|
|
@patch("superset.initialization.inspect")
|
|
@patch("superset.initialization.logger")
|
|
def test_init_database_dependent_features_handles_operational_error(
|
|
self, mock_logger, mock_inspect_func, mock_db
|
|
):
|
|
"""Test that OperationalError during inspection is handled gracefully."""
|
|
# Setup
|
|
mock_app = MagicMock()
|
|
app_initializer = SupersetAppInitializer(mock_app)
|
|
error_msg = "Cannot connect to database"
|
|
mock_inspect_func.side_effect = OperationalError(error_msg, None, None)
|
|
mock_db.engine = MagicMock()
|
|
|
|
# Execute
|
|
app_initializer._init_database_dependent_features()
|
|
|
|
# Assert
|
|
mock_inspect_func.assert_called_once_with(mock_db.engine)
|
|
mock_logger.debug.assert_called_once()
|
|
call_args = mock_logger.debug.call_args
|
|
assert "Error inspecting database tables" in call_args[0][0]
|
|
# The error is passed as second argument with %s formatting
|
|
assert str(call_args[0][1]) == str(OperationalError(error_msg, None, None))
|
|
|
|
@patch("superset.initialization.db")
|
|
@patch("superset.initialization.inspect")
|
|
@patch("superset.initialization.feature_flag_manager")
|
|
@patch("superset.initialization.register_sqla_event_listeners")
|
|
@patch("superset.initialization.logger")
|
|
@patch("superset.commands.theme.seed.SeedSystemThemesCommand")
|
|
def test_init_database_dependent_features_initializes_when_tables_exist(
|
|
self,
|
|
mock_seed_themes_command,
|
|
mock_logger,
|
|
mock_register_listeners,
|
|
mock_feature_flag_manager,
|
|
mock_inspect_func,
|
|
mock_db,
|
|
):
|
|
"""Test that features are initialized when database tables exist."""
|
|
# Setup
|
|
mock_app = MagicMock()
|
|
app_initializer = SupersetAppInitializer(mock_app)
|
|
mock_inspector = MagicMock()
|
|
mock_inspector.has_table.return_value = True
|
|
mock_inspect_func.return_value = mock_inspector
|
|
mock_db.engine = MagicMock()
|
|
mock_feature_flag_manager.is_feature_enabled.return_value = True
|
|
mock_seed_themes = MagicMock()
|
|
mock_seed_themes_command.return_value = mock_seed_themes
|
|
|
|
# Execute
|
|
app_initializer._init_database_dependent_features()
|
|
|
|
# Assert
|
|
mock_inspect_func.assert_called_once_with(mock_db.engine)
|
|
# Check both tables are checked
|
|
assert mock_inspector.has_table.call_count == 2
|
|
mock_inspector.has_table.assert_any_call("dashboards")
|
|
mock_inspector.has_table.assert_any_call("themes")
|
|
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 not log skip message when tables exist
|
|
mock_logger.debug.assert_not_called()
|
|
|
|
@patch("superset.initialization.db")
|
|
@patch("superset.initialization.inspect")
|
|
@patch("superset.initialization.feature_flag_manager")
|
|
@patch("superset.initialization.register_sqla_event_listeners")
|
|
@patch("superset.commands.theme.seed.SeedSystemThemesCommand")
|
|
def test_init_database_dependent_features_skips_tagging_when_disabled(
|
|
self,
|
|
mock_seed_themes_command,
|
|
mock_register_listeners,
|
|
mock_feature_flag_manager,
|
|
mock_inspect_func,
|
|
mock_db,
|
|
):
|
|
"""Test that tagging system is not initialized when feature flag is disabled."""
|
|
# Setup
|
|
mock_app = MagicMock()
|
|
app_initializer = SupersetAppInitializer(mock_app)
|
|
mock_inspector = MagicMock()
|
|
mock_inspector.has_table.return_value = True
|
|
mock_inspect_func.return_value = mock_inspector
|
|
mock_db.engine = MagicMock()
|
|
mock_feature_flag_manager.is_feature_enabled.return_value = False
|
|
mock_seed_themes = MagicMock()
|
|
mock_seed_themes_command.return_value = mock_seed_themes
|
|
|
|
# Execute
|
|
app_initializer._init_database_dependent_features()
|
|
|
|
# Assert
|
|
mock_feature_flag_manager.is_feature_enabled.assert_called_with(
|
|
"TAGGING_SYSTEM"
|
|
)
|
|
mock_register_listeners.assert_not_called()
|
|
# Check both tables are checked
|
|
assert mock_inspector.has_table.call_count == 2
|
|
mock_inspector.has_table.assert_any_call("dashboards")
|
|
mock_inspector.has_table.assert_any_call("themes")
|
|
|
|
@patch("superset.initialization.db")
|
|
@patch("superset.initialization.inspect")
|
|
@patch("superset.initialization.logger")
|
|
def test_init_database_dependent_features_handles_inspector_error_in_has_table(
|
|
self, mock_logger, mock_inspect_func, mock_db
|
|
):
|
|
"""Test that OperationalError from has_table check is handled gracefully."""
|
|
# Setup
|
|
mock_app = MagicMock()
|
|
app_initializer = SupersetAppInitializer(mock_app)
|
|
mock_inspector = MagicMock()
|
|
error_msg = "Table check failed"
|
|
mock_inspector.has_table.side_effect = OperationalError(error_msg, None, None)
|
|
mock_inspect_func.return_value = mock_inspector
|
|
mock_db.engine = MagicMock()
|
|
|
|
# Execute
|
|
app_initializer._init_database_dependent_features()
|
|
|
|
# Assert
|
|
mock_inspect_func.assert_called_once_with(mock_db.engine)
|
|
mock_inspector.has_table.assert_called_once_with("dashboards")
|
|
# Should handle the error gracefully
|
|
mock_logger.debug.assert_called_once()
|
|
call_args = mock_logger.debug.call_args
|
|
assert "Error inspecting database tables" in call_args[0][0]
|
|
|
|
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_lazy_property_with_missing_config(self):
|
|
"""Test that database_uri property returns empty string when config missing."""
|
|
# Setup
|
|
mock_app = MagicMock()
|
|
mock_app.config = {} # Empty config
|
|
app_initializer = SupersetAppInitializer(mock_app)
|
|
|
|
# Should return empty string when config key doesn't exist
|
|
uri = app_initializer.database_uri
|
|
assert uri == ""
|
|
# Empty string is a fallback value, so it should NOT be cached
|
|
assert app_initializer._db_uri_cache is None
|
|
|
|
def test_database_uri_prevents_nouser_fallback(self):
|
|
"""Test that lazy initialization prevents nouser fallback during deployment."""
|
|
# Setup - simulate deployment scenario where config is loaded properly
|
|
mock_app = MagicMock()
|
|
|
|
# Config is properly loaded with real database URI (not nouser)
|
|
mock_app.config = {
|
|
"SQLALCHEMY_DATABASE_URI": "postgresql://realuser:realpass@realhost:5432/realdb"
|
|
}
|
|
app_initializer = SupersetAppInitializer(mock_app)
|
|
|
|
# Access the database URI - should get the real URI, not nouser
|
|
uri = app_initializer.database_uri
|
|
assert uri == "postgresql://realuser:realpass@realhost:5432/realdb"
|
|
assert "nouser" not in uri
|
|
assert "nopassword" not in uri
|
|
assert "nohost" not in uri
|
|
assert "nodb" not in uri
|
|
|
|
# Verify cache is working
|
|
assert app_initializer._db_uri_cache is not None
|
|
assert (
|
|
app_initializer._db_uri_cache
|
|
== "postgresql://realuser:realpass@realhost:5432/realdb"
|
|
)
|
|
|
|
@patch("superset.initialization.make_url_safe")
|
|
@patch("superset.initialization.db")
|
|
def test_set_db_default_isolation_uses_lazy_property(
|
|
self, mock_db, mock_make_url_safe
|
|
):
|
|
"""Test that set_db_default_isolation uses the lazy database_uri property."""
|
|
# Setup
|
|
mock_app = MagicMock()
|
|
test_uri = "postgresql://user:pass@host:5432/testdb"
|
|
mock_app.config = {
|
|
"SQLALCHEMY_DATABASE_URI": test_uri,
|
|
"SQLALCHEMY_ENGINE_OPTIONS": {},
|
|
}
|
|
app_initializer = SupersetAppInitializer(mock_app)
|
|
|
|
# Mock make_url_safe to return a URL with postgresql backend
|
|
mock_url = MagicMock()
|
|
mock_url.get_backend_name.return_value = "postgresql"
|
|
mock_make_url_safe.return_value = mock_url
|
|
|
|
# Mock db.engine
|
|
mock_engine = MagicMock()
|
|
mock_db.engine = mock_engine
|
|
|
|
# Execute
|
|
app_initializer.set_db_default_isolation()
|
|
|
|
# Assert that make_url_safe was called with the lazy property value
|
|
mock_make_url_safe.assert_called_once_with(test_uri)
|
|
|
|
# Should set isolation level to READ COMMITTED for postgresql
|
|
mock_engine.execution_options.assert_called_once_with(
|
|
isolation_level="READ COMMITTED"
|
|
)
|
|
|
|
# Verify the cache was created
|
|
assert app_initializer._db_uri_cache is not None
|
|
assert app_initializer._db_uri_cache == test_uri
|
|
|
|
@patch("superset.initialization.make_url_safe")
|
|
@patch("superset.initialization.db")
|
|
def test_set_db_default_isolation_with_empty_uri(self, mock_db, mock_make_url_safe):
|
|
"""Test that set_db_default_isolation handles empty URI gracefully."""
|
|
# Setup
|
|
mock_app = MagicMock()
|
|
mock_app.config = {
|
|
"SQLALCHEMY_DATABASE_URI": "", # Empty URI
|
|
"SQLALCHEMY_ENGINE_OPTIONS": {},
|
|
}
|
|
app_initializer = SupersetAppInitializer(mock_app)
|
|
|
|
# Mock make_url_safe to return a URL with no backend
|
|
mock_url = MagicMock()
|
|
mock_url.get_backend_name.return_value = None
|
|
mock_make_url_safe.return_value = mock_url
|
|
|
|
# Execute
|
|
app_initializer.set_db_default_isolation()
|
|
|
|
# Should handle empty URI gracefully
|
|
mock_make_url_safe.assert_called_once_with("")
|
|
|
|
# Should not set isolation level for empty/unknown backend
|
|
mock_db.engine.execution_options.assert_not_called()
|
|
|
|
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"
|
|
)
|
|
|
|
# Third access should use cache even if config changes again
|
|
config_dict["SQLALCHEMY_DATABASE_URI"] = (
|
|
"postgresql://different:uri@host:5432/db"
|
|
)
|
|
uri3 = app_initializer.database_uri
|
|
assert (
|
|
uri3 == "postgresql://realuser:realpass@realhost:5432/realdb"
|
|
) # Still cached value
|
|
|
|
def test_database_uri_caches_valid_uri(self):
|
|
"""Test that valid URIs are properly cached."""
|
|
# Setup
|
|
mock_app = MagicMock()
|
|
valid_uri = "postgresql://validuser:validpass@validhost:5432/validdb"
|
|
mock_app.config = {"SQLALCHEMY_DATABASE_URI": valid_uri}
|
|
app_initializer = SupersetAppInitializer(mock_app)
|
|
|
|
# First access should cache valid URI
|
|
uri1 = app_initializer.database_uri
|
|
assert uri1 == valid_uri
|
|
assert app_initializer._db_uri_cache is not None
|
|
assert app_initializer._db_uri_cache == valid_uri
|
|
|
|
# Change config
|
|
mock_app.config = {
|
|
"SQLALCHEMY_DATABASE_URI": "postgresql://changed:uri@host:5432/db"
|
|
}
|
|
|
|
# Second access should still return cached value
|
|
uri2 = app_initializer.database_uri
|
|
assert uri2 == valid_uri # Still the cached value, not the changed one
|
|
|
|
def test_database_uri_fallback_patterns(self):
|
|
"""Test that various fallback patterns are not cached."""
|
|
# Test various fallback patterns
|
|
fallback_uris = [
|
|
"postgresql://nouser:nopassword@nohost:5432/nodb",
|
|
"mysql://NOUSER:NOPASSWORD@NOHOST:3306/NODB",
|
|
"postgresql://noUser:pass@host:5432/db", # Contains 'nouser' (case insens.)
|
|
"sqlite:///nohost.db", # Contains 'nohost'
|
|
"", # Empty string
|
|
]
|
|
|
|
for fallback_uri in fallback_uris:
|
|
# Create a fresh initializer for each test
|
|
mock_app = MagicMock()
|
|
mock_app.config = {"SQLALCHEMY_DATABASE_URI": fallback_uri}
|
|
app_initializer = SupersetAppInitializer(mock_app)
|
|
|
|
uri = app_initializer.database_uri
|
|
|
|
# Should return the value but not cache it
|
|
assert uri == fallback_uri
|
|
assert app_initializer._db_uri_cache is None, (
|
|
f"Should not cache: {fallback_uri}"
|
|
)
|
|
|
|
@patch("superset.initialization.logger")
|
|
@patch("superset.initialization.inspect")
|
|
@patch("superset.initialization.db")
|
|
def test_init_database_dependent_features_skips_with_fallback_uri(
|
|
self, mock_db, mock_inspect, mock_logger
|
|
):
|
|
"""Test that database-dependent features are skipped when URI is a fallback."""
|
|
# Setup
|
|
mock_app = MagicMock()
|
|
# Set a fallback URI that would cause connection to fail
|
|
mock_app.config = {
|
|
"SQLALCHEMY_DATABASE_URI": "postgresql://nouser:nopassword@nohost:5432/nodb"
|
|
}
|
|
app_initializer = SupersetAppInitializer(mock_app)
|
|
|
|
# Execute
|
|
app_initializer._init_database_dependent_features()
|
|
|
|
# Assert - should not try to inspect database
|
|
mock_inspect.assert_not_called()
|
|
# Should log warning about fallback URI
|
|
mock_logger.warning.assert_called_once()
|
|
warning_message = mock_logger.warning.call_args[0][0]
|
|
assert "fallback value" in warning_message
|
|
assert "workspace context" in warning_message
|