from importlib import import_module from unittest.mock import Mock, patch from django.apps import AppConfig, apps from django.core.exceptions import ImproperlyConfigured from django.test import override_settings from cms import app_registration from cms.apps import CMSConfig from cms.test_utils.testcases import CMSTestCase from cms.utils import setup class AutodiscoverTestCase(CMSTestCase): @override_settings(INSTALLED_APPS=[ 'cms.test_utils.project.app_with_cms_config', ]) def test_adds_only_cms_config_attr_to_config_only_app(self): app_registration.autodiscover_cms_configs() app = apps.get_app_config('app_with_cms_config') # Make sure the app has a cms_config attribute self.assertTrue(hasattr(app, 'cms_config')) self.assertEqual( app.cms_config.__class__.__name__, 'CMSConfigConfig') self.assertEqual(app.cms_config.app_config, app) # Make sure the app doesn't have a cms_extension attribute self.assertFalse(hasattr(app, 'cms_extension')) @override_settings(INSTALLED_APPS=[ 'cms.test_utils.project.app_with_cms_feature_and_config', ]) def test_adds_both_cms_config_and_cms_extension_attr(self): app_registration.autodiscover_cms_configs() app = apps.get_app_config('app_with_cms_feature_and_config') # Make sure the app has a cms_config attribute self.assertTrue(hasattr(app, 'cms_config')) self.assertEqual( app.cms_config.__class__.__name__, 'CMSConfig') self.assertEqual(app.cms_config.app_config, app) # Make sure the app has a cms_extension attribute self.assertTrue(hasattr(app, 'cms_extension')) self.assertEqual( app.cms_extension.__class__.__name__, 'CMSExtension') @override_settings(INSTALLED_APPS=[ 'cms.test_utils.project.app_without_cms_file', ]) def test_doesnt_add_attrs_to_app_without_cms_config(self): app_registration.autodiscover_cms_configs() app = apps.get_app_config('app_without_cms_file') # Make sure the app doesn't have a cms_config attribute self.assertFalse(hasattr(app, 'cms_config')) # Make sure the app doesn't have a cms_extension attribute self.assertFalse(hasattr(app, 'cms_extension')) @override_settings(INSTALLED_APPS=[ 'cms.test_utils.project.app_with_bad_cms_file', ]) def test_exception_propagates_from_cms_file(self): # The cms file intentionally raises a RuntimeError. We need # to make sure the exception definitely bubbles up and doesn't # get caught. with self.assertRaises(RuntimeError): app_registration.autodiscover_cms_configs() @override_settings(INSTALLED_APPS=[ 'cms.test_utils.project.app_without_cms_app_class', ]) def test_raises_exception_when_no_cms_app_class_found_in_cms_file(self): # No cms config defined in the cms file so raise exception with self.assertRaises(ImproperlyConfigured): app_registration.autodiscover_cms_configs() @override_settings(INSTALLED_APPS=[ 'cms.test_utils.project.app_with_two_cms_config_classes', ]) def test_raises_exception_when_more_than_one_cms_config_class_found_in_cms_file(self): # More than one cms config defined so raise exception with self.assertRaises(ImproperlyConfigured): app_registration.autodiscover_cms_configs() @override_settings(INSTALLED_APPS=[ 'cms.test_utils.project.app_with_two_cms_feature_classes', ]) def test_raises_exception_when_more_than_one_cms_extension_class_found_in_cms_file(self): # More than one cms extension defined so raise exception with self.assertRaises(ImproperlyConfigured): app_registration.autodiscover_cms_configs() class GetCmsExtensionAppsTestCase(CMSTestCase): def setUp(self): # The result of get_cms_extension_apps is cached. Clear this cache # because installed apps change between tests and therefore # unlike in a live environment, results of this function # can change between tests app_registration.get_cms_extension_apps.cache_clear() @patch.object(apps, 'get_app_configs') def test_returns_only_cms_apps_with_extension(self, mocked_apps): app_with_extension = Mock(label='a', cms_extension=Mock(), spec=[]) app_with_config = Mock(label='b', cms_config=Mock(), spec=[]) app_with_both = Mock( label='c', cms_config=Mock(), cms_extension=Mock(), spec=[]) non_cms_app = Mock(label='d', spec=[]), mocked_apps.return_value = [ app_with_extension, app_with_config, app_with_both, non_cms_app, ] cms_apps = app_registration.get_cms_extension_apps() # Of the 4 installed apps only 2 have extensions self.assertListEqual( cms_apps, [app_with_extension, app_with_both]) class GetCmsConfigAppsTestCase(CMSTestCase): def setUp(self): # The result of get_cms_config_apps is cached. Clear this cache # because installed apps change between tests and therefore # unlike in a live environment, results of this function # can change between tests app_registration.get_cms_config_apps.cache_clear() @patch.object(apps, 'get_app_configs') def test_returns_only_cms_apps_with_config(self, mocked_apps): app_with_extension = Mock(label='a', cms_extension=Mock(), spec=[]) app_with_config = Mock(label='b', cms_config=Mock(), spec=[]) app_with_both = Mock( label='c', cms_config=Mock(), cms_extension=Mock(), spec=[]) non_cms_app = Mock(label='d', spec=[]), mocked_apps.return_value = [ app_with_extension, app_with_config, app_with_both, non_cms_app, ] cms_apps = app_registration.get_cms_config_apps() # Of the 4 installed apps only 2 have configs self.assertListEqual( cms_apps, [app_with_config, app_with_both]) class ConfigureCmsAppsTestCase(CMSTestCase): def setUp(self): # The result of get_cms_config_apps is cached. Clear this cache # because installed apps change between tests and therefore # unlike in a live environment, results of this function # can change between tests app_registration.get_cms_config_apps.cache_clear() @patch.object(apps, 'get_app_configs') def test_runs_configure_app_method_for_app_with_enabled_config(self, mocked_apps): # Set up app with label djangocms_feature_x that has a cms feature feature_app = Mock(spec=AppConfig) feature_app.label = 'djangocms_feature_x' feature_app.cms_extension = Mock(spec=['configure_app']) # Set up app that makes use of djangocms_feature_x config_app = Mock(spec=AppConfig) config_app.cms_config = Mock( spec=['djangocms_feature_x_enabled']) config_app.cms_config.djangocms_feature_x_enabled = True # Pretend these mocked apps are in INSTALLED_APPS mocked_apps.return_value = [ feature_app, config_app] app_registration.configure_cms_apps([feature_app]) # If an app has enabled a feature, the configure method # for that feature should have run with that app as the arg feature_app.cms_extension.configure_app.assert_called_once_with( config_app.cms_config) @patch.object(apps, 'get_app_configs') def test_doesnt_run_configure_app_method_for_disabled_app(self, mocked_apps): # Set up app with label djangocms_feature_x that has a cms feature feature_app = Mock(spec=AppConfig) feature_app.label = 'djangocms_feature_x' feature_app.cms_extension = Mock(spec=['configure_app']) # Set up two apps that do not make use of djangocms_feature_x. # One does not define the enabled attr at all (most common # use case) and one defines it as False config_app_disabled1 = Mock(spec=AppConfig) config_app_disabled1.cms_config = Mock(spec=[]) config_app_disabled2 = Mock(spec=AppConfig) config_app_disabled2.cms_config = Mock( spec=['djangocms_feature_x_enabled']) config_app_disabled2.cms_config.djangocms_feature_x_enabled = False # Pretend all these mocked apps are in INSTALLED_APPS mocked_apps.return_value = [ feature_app, config_app_disabled1, config_app_disabled2] app_registration.configure_cms_apps([feature_app]) # If an app has not enabled a feature, the configure method # for that feature should not have been run for that app self.assertFalse(feature_app.cms_extension.configure_app.called) @patch.object(apps, 'get_app_configs') def test_doesnt_raise_exception_if_not_cms_app(self, mocked_apps): # Set up app with label djangocms_feature_x that has a cms feature feature_app = Mock(spec=AppConfig) feature_app.label = 'djangocms_feature_x' feature_app.cms_extension = Mock(spec=['configure_app']) # Set up non cms app non_cms_app = Mock(spec=AppConfig) # Pretend these mocked apps are in INSTALLED_APPS mocked_apps.return_value = [feature_app, non_cms_app] # An app that does not define a cms config should just be # ignored and not cause any exceptions try: app_registration.configure_cms_apps([feature_app]) except AttributeError: self.fail("Exception raised when cms app config not defined") @patch.object(apps, 'get_app_configs') def test_runs_configure_app_method_for_correct_apps_when_multiple_apps(self, mocked_apps): # Set up app with label djangocms_feature_x that has a cms feature feature_app_x = Mock(spec=AppConfig) feature_app_x.label = 'djangocms_feature_x' feature_app_x.cms_extension = Mock(spec=['configure_app']) # Set up app with label djangocms_feature_y that has a cms feature feature_app_y = Mock(spec=AppConfig) feature_app_y.label = 'djangocms_feature_y' feature_app_y.cms_extension = Mock(spec=['configure_app']) # Set up apps that make use of djangocms_feature_x config_app_x = Mock(spec=AppConfig) config_app_x.cms_config = Mock( spec=['djangocms_feature_x_enabled']) config_app_x.cms_config.djangocms_feature_x_enabled = True # Set up app that makes use of djangocms_feature_y config_app_y = Mock(spec=AppConfig) config_app_y.cms_config = Mock( spec=['djangocms_feature_y_enabled']) config_app_y.cms_config.djangocms_feature_y_enabled = True # Set up app that makes use of feature x & y config_app_xy = Mock(spec=AppConfig) config_app_xy.cms_config = Mock( spec=['djangocms_feature_x_enabled', 'djangocms_feature_y_enabled'] ) config_app_xy.cms_config.djangocms_feature_x_enabled = True config_app_xy.cms_config.djangocms_feature_y_enabled = True # Set up non cms app non_cms_app = Mock(spec=AppConfig) # Pretend these mocked apps are in INSTALLED_APPS mocked_apps.return_value = [ feature_app_x, non_cms_app, config_app_xy, config_app_y, config_app_x, feature_app_y] app_registration.configure_cms_apps( [feature_app_x, feature_app_y]) # Assert we configured the 2 apps we expected with feature x self.assertEqual( feature_app_x.cms_extension.configure_app.call_count, 2) self.assertEqual( feature_app_x.cms_extension.configure_app.call_args_list[0][0][0], config_app_xy.cms_config) self.assertEqual( feature_app_x.cms_extension.configure_app.call_args_list[1][0][0], config_app_x.cms_config) # Assert we configured the 2 apps we expected with feature y self.assertEqual( feature_app_y.cms_extension.configure_app.call_count, 2) self.assertEqual( feature_app_y.cms_extension.configure_app.call_args_list[0][0][0], config_app_xy.cms_config) self.assertEqual( feature_app_y.cms_extension.configure_app.call_args_list[1][0][0], config_app_y.cms_config) @patch.object(apps, 'get_app_configs') def test_runs_ready_for_all_extensions(self, mocked_apps): feature_app = Mock(spec=AppConfig) feature_app.label = 'djangocms_feature_x' feature_app.cms_extension = Mock(spec=['ready']) mocked_apps.return_value = [feature_app] app_registration.ready_cms_apps([feature_app]) feature_app.cms_extension.ready.assert_called_once_with() class SetupCmsAppsTestCase(CMSTestCase): def setUp(self): # The results of get_cms_extension_apps and get_cms_config_apps # are cached. Clear this cache because installed apps change # between tests and therefore unlike in a live environment, # results of this function can change between tests app_registration.get_cms_extension_apps.cache_clear() app_registration.get_cms_config_apps.cache_clear() @patch.object(setup, 'setup_cms_apps') def test_setup_cms_apps_function_run_on_startup(self, mocked_setup): cms_module = import_module('cms') CMSConfig('app_name', cms_module).ready() mocked_setup.assert_called_once() @override_settings(INSTALLED_APPS=[ 'cms.test_utils.project.app_with_cms_feature', 'cms.test_utils.project.app_with_cms_feature_and_config', 'cms.test_utils.project.app_without_cms_file', 'cms.test_utils.project.app_with_cms_config' ]) def test_cms_apps_setup_after_setup_function_run(self): # This is the function that gets run on startup setup.setup_cms_apps() # Get the django app configs to do asserts on them later feature_app = apps.get_app_config('app_with_cms_feature') config_app = apps.get_app_config('app_with_cms_config') feature_and_config_app = apps.get_app_config( 'app_with_cms_feature_and_config') non_cms_app = apps.get_app_config('app_without_cms_file') # cms_config attribute has been added to all app configs # that define a cms config class self.assertFalse(hasattr(non_cms_app, 'cms_config')) self.assertFalse(hasattr(feature_app, 'cms_config')) self.assertTrue(hasattr(config_app, 'cms_config')) self.assertTrue(hasattr(feature_and_config_app, 'cms_config')) # cms_extension attribute has been added to all app configs # that define a cms extension class self.assertFalse(hasattr(non_cms_app, 'cms_extension')) self.assertTrue(hasattr(feature_app, 'cms_extension')) self.assertFalse(hasattr(config_app, 'cms_extension')) self.assertTrue(hasattr(feature_and_config_app, 'cms_extension')) # Code from the configure method did what we expected. # Relying on checking that the code in the app_with_cms_feature # test app ran. This is so as to avoid mocking and allow the # whole app registration code to run through. self.assertEqual(feature_app.cms_extension.num_configured_apps, 1) self.assertListEqual( feature_app.cms_extension.configured_apps, ['app_with_cms_config']) def test_configure_cms_apps_uses_contract_name_as_enabled_property(self): """Test that configure_cms_apps respects the contract name when determining enabled property.""" # Create mock extension with a custom contract name mock_extension = Mock() contract_obj = Mock() mock_extension.contract = ('versioning', contract_obj) mock_extension.configure_app = Mock() mock_feature_app = Mock() mock_feature_app.cms_extension = mock_extension mock_feature_app.label = 'my_feature' # Create mock config app with versioning_enabled = True mock_cms_config = Mock() mock_cms_config.versioning_enabled = True mock_config_app = Mock() mock_config_app.cms_config = mock_cms_config with patch.object(app_registration, 'get_cms_config_apps', return_value=[mock_config_app]): app_registration.configure_cms_apps([mock_feature_app]) # configure_app should be called because versioning_enabled = True mock_extension.configure_app.assert_called_once_with(mock_cms_config) def test_configure_cms_apps_uses_app_label_when_no_contract(self): """Test that configure_cms_apps uses app label as contract name when no contract is defined.""" # Create mock extension without a contract attribute mock_extension = Mock(spec=['configure_app']) mock_extension.configure_app = Mock() mock_feature_app = Mock() mock_feature_app.cms_extension = mock_extension mock_feature_app.label = 'my_feature' # Create mock config app with my_feature_enabled = True mock_cms_config = Mock() mock_cms_config.my_feature_enabled = True mock_config_app = Mock() mock_config_app.cms_config = mock_cms_config with patch.object(app_registration, 'get_cms_config_apps', return_value=[mock_config_app]): app_registration.configure_cms_apps([mock_feature_app]) # configure_app should be called because my_feature_enabled = True mock_extension.configure_app.assert_called_once_with(mock_cms_config) def test_configure_cms_apps_skips_disabled_apps(self): """Test that configure_cms_apps doesn't configure apps where enabled property is False.""" # Create mock extension with custom contract name mock_extension = Mock() mock_extension.contract = ('versioning', Mock()) mock_extension.configure_app = Mock() mock_feature_app = Mock() mock_feature_app.cms_extension = mock_extension # Create mock config app with versioning_enabled = False mock_cms_config = Mock() mock_cms_config.versioning_enabled = False mock_config_app = Mock() mock_config_app.cms_config = mock_cms_config with patch.object(app_registration, 'get_cms_config_apps', return_value=[mock_config_app]): app_registration.configure_cms_apps([mock_feature_app]) # configure_app should NOT be called because versioning_enabled = False mock_extension.configure_app.assert_not_called() def test_configure_cms_apps_raises_error_on_invalid_contract(self): """Test that configure_cms_apps raises ImproperlyConfigured for invalid contract format.""" # Create mock extension with invalid contract (not a tuple) mock_extension = Mock() mock_extension.contract = 'invalid_contract' mock_feature_app = Mock() mock_feature_app.cms_extension = mock_extension with self.assertRaises(ImproperlyConfigured) as context: app_registration.configure_cms_apps([mock_feature_app]) self.assertIn('2-tuple', str(context.exception)) def test_configure_cms_apps_with_multiple_configs(self): """Test that configure_cms_apps configures multiple apps with matching enabled property.""" # Create mock extension with contract name mock_extension = Mock() mock_extension.contract = ('versioning', Mock()) mock_extension.configure_app = Mock() mock_feature_app = Mock() mock_feature_app.cms_extension = mock_extension # Create two mock config apps, both with versioning_enabled = True mock_cms_config1 = Mock() mock_cms_config1.versioning_enabled = True mock_config_app1 = Mock() mock_config_app1.cms_config = mock_cms_config1 mock_cms_config2 = Mock() mock_cms_config2.versioning_enabled = True mock_config_app2 = Mock() mock_config_app2.cms_config = mock_cms_config2 with patch.object(app_registration, 'get_cms_config_apps', return_value=[mock_config_app1, mock_config_app2]): app_registration.configure_cms_apps([mock_feature_app]) # configure_app should be called twice self.assertEqual(mock_extension.configure_app.call_count, 2) calls = [call[0][0] for call in mock_extension.configure_app.call_args_list] self.assertIn(mock_cms_config1, calls) self.assertIn(mock_cms_config2, calls) class CMSAppConfigGetContractTestCase(CMSTestCase): """Tests for CMSAppConfig.get_contract() method.""" def setUp(self): # Clear cache for get_cms_extension_apps app_registration.get_cms_extension_apps.cache_clear() def test_get_contract_returns_none_when_no_extensions_exist(self): """Test that get_contract returns None when no CMS extensions are registered.""" from cms.app_base import CMSAppConfig result = CMSAppConfig.get_contract('non_existent_contract') self.assertIsNone(result) @override_settings(INSTALLED_APPS=[ 'cms.test_utils.project.app_with_cms_feature', ]) def test_get_contract_returns_none_when_contract_not_found(self): """Test that get_contract returns None when contract name doesn't match any registered contract.""" from cms.app_base import CMSAppConfig app_registration.autodiscover_cms_configs() result = CMSAppConfig.get_contract('non_existent_contract') self.assertIsNone(result) def test_get_contract_with_mocked_extension(self): """Test that get_contract correctly retrieves a contract from a mocked extension.""" from cms.app_base import CMSAppConfig # Create a mock app with a contract mock_extension = Mock() contract_obj = Mock() contract_obj.some_method = Mock(return_value='contract_result') mock_extension.contract = ('test_contract', contract_obj) mock_app = Mock() mock_app.cms_extension = mock_extension # Patch get_cms_extension_apps to return our mock with patch.object(app_registration, 'get_cms_extension_apps', return_value=[mock_app]): result = CMSAppConfig.get_contract('test_contract') # Verify the contract was returned self.assertEqual(result, contract_obj) # Verify we can use the returned contract self.assertEqual(result.some_method(), 'contract_result') def test_get_contract_returns_none_when_extension_has_no_contract(self): """Test that get_contract returns None when extension doesn't define a contract attribute.""" from cms.app_base import CMSAppConfig # Create a mock app without a contract attribute mock_extension = Mock(spec=[]) # No contract attribute mock_app = Mock() mock_app.cms_extension = mock_extension with patch.object(app_registration, 'get_cms_extension_apps', return_value=[mock_app]): result = CMSAppConfig.get_contract('test_contract') self.assertIsNone(result) def test_get_contract_stops_at_first_match(self): """Test that get_contract returns the first matching contract and stops searching.""" from cms.app_base import CMSAppConfig # Create two mock apps with contracts contract_obj1 = Mock() mock_extension1 = Mock() mock_extension1.contract = ('matching_contract', contract_obj1) mock_app1 = Mock() mock_app1.cms_extension = mock_extension1 contract_obj2 = Mock() mock_extension2 = Mock() mock_extension2.contract = ('matching_contract', contract_obj2) mock_app2 = Mock() mock_app2.cms_extension = mock_extension2 with patch.object(app_registration, 'get_cms_extension_apps', return_value=[mock_app1, mock_app2]): result = CMSAppConfig.get_contract('matching_contract') # Should return the first contract, not the second self.assertEqual(result, contract_obj1) self.assertNotEqual(result, contract_obj2)