mirror of
				https://github.com/kevinveenbirkenbach/computer-playbook.git
				synced 2025-11-04 04:08:15 +00:00 
			
		
		
		
	Improve get_app_conf robustness and add skip_missing_app parameter support
- Added new optional parameter 'skip_missing_app' to get_app_conf() in module_utils/config_utils.py to safely return defaults when applications are missing. - Updated group_vars/all/00_general.yml and roles/web-app-nextcloud/config/main.yml to include skip_missing_app=True in all Nextcloud-related calls. - Added comprehensive unit tests under tests/unit/module_utils/test_config_utils.py covering missing app handling, schema enforcement, nested lists, and index edge cases. Ref: https://chatgpt.com/share/68ee6b5c-6db0-800f-bc20-d51470d7b39f
This commit is contained in:
		@@ -76,8 +76,9 @@ _applications_nextcloud_oidc_flavor: >-
 | 
			
		||||
          False,
 | 
			
		||||
          'oidc_login'
 | 
			
		||||
          if applications
 | 
			
		||||
            | get_app_conf('web-app-nextcloud','features.ldap',False, True)
 | 
			
		||||
          else 'sociallogin'
 | 
			
		||||
            | get_app_conf('web-app-nextcloud','features.ldap',False, True, True)
 | 
			
		||||
          else 'sociallogin',
 | 
			
		||||
          True
 | 
			
		||||
        )
 | 
			
		||||
  }}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ class ConfigEntryNotSetError(AppConfigKeyError):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_app_conf(applications, application_id, config_path, strict=True, default=None):
 | 
			
		||||
def get_app_conf(applications, application_id, config_path, strict=True, default=None, skip_missing_app=False):
 | 
			
		||||
    # Path to the schema file for this application
 | 
			
		||||
    schema_path = os.path.join('roles', application_id, 'schema', 'main.yml')
 | 
			
		||||
 | 
			
		||||
@@ -133,6 +133,9 @@ def get_app_conf(applications, application_id, config_path, strict=True, default
 | 
			
		||||
    try:
 | 
			
		||||
        obj = applications[application_id]
 | 
			
		||||
    except KeyError:
 | 
			
		||||
        if skip_missing_app:
 | 
			
		||||
            # Simply return default instead of failing
 | 
			
		||||
            return default if default is not None else False
 | 
			
		||||
        raise AppConfigKeyError(
 | 
			
		||||
            f"Application ID '{application_id}' not found in applications dict.\n"
 | 
			
		||||
            f"path_trace: {path_trace}\n"
 | 
			
		||||
 
 | 
			
		||||
@@ -91,7 +91,7 @@ docker:
 | 
			
		||||
      mem_reservation:      "128m"
 | 
			
		||||
      mem_limit:            "512m"
 | 
			
		||||
      pids_limit:           256
 | 
			
		||||
  enabled:  "{{ applications | get_app_conf('web-app-nextcloud', 'features.oidc', False) }}"   # Activate OIDC for Nextcloud
 | 
			
		||||
  enabled:  "{{ applications | get_app_conf('web-app-nextcloud', 'features.oidc', False, True, True) }}"   # Activate OIDC for Nextcloud
 | 
			
		||||
  # floavor decides which OICD plugin should be used. 
 | 
			
		||||
  # Available options: oidc_login, sociallogin
 | 
			
		||||
  # @see https://apps.nextcloud.com/apps/oidc_login
 | 
			
		||||
@@ -194,7 +194,7 @@ plugins:
 | 
			
		||||
    enabled: false
 | 
			
		||||
  fileslibreofficeedit:
 | 
			
		||||
    # Nextcloud LibreOffice integration: allows online editing of documents with LibreOffice (https://apps.nextcloud.com/apps/fileslibreofficeedit)
 | 
			
		||||
    enabled: "{{ not (applications | get_app_conf('web-app-nextcloud', 'plugins.richdocuments.enabled', False, True)) }}"
 | 
			
		||||
    enabled: "{{ not (applications | get_app_conf('web-app-nextcloud', 'plugins.richdocuments.enabled', False, True, True)) }}"
 | 
			
		||||
  forms:
 | 
			
		||||
    # Nextcloud forms: facilitates creation of forms and surveys (https://apps.nextcloud.com/apps/forms)
 | 
			
		||||
    enabled: true
 | 
			
		||||
@@ -292,13 +292,13 @@ plugins:
 | 
			
		||||
  #  enabled: false
 | 
			
		||||
  twofactor_nextcloud_notification:
 | 
			
		||||
    # Nextcloud two-factor notification: sends notifications for two-factor authentication events (https://apps.nextcloud.com/apps/twofactor_nextcloud_notification)
 | 
			
		||||
    enabled: "{{ not applications | get_app_conf('web-app-nextcloud', 'features.oidc', False, True) }}" # Deactivate 2FA if oidc is active
 | 
			
		||||
    enabled: "{{ not applications | get_app_conf('web-app-nextcloud', 'features.oidc', False, True, True) }}" # Deactivate 2FA if oidc is active
 | 
			
		||||
  twofactor_totp:
 | 
			
		||||
    # Nextcloud two-factor TOTP: provides time-based one-time password authentication (https://apps.nextcloud.com/apps/twofactor_totp)
 | 
			
		||||
    enabled: "{{ not applications | get_app_conf('web-app-nextcloud', 'features.oidc', False, True) }}" # Deactivate 2FA if oidc is active
 | 
			
		||||
    enabled: "{{ not applications | get_app_conf('web-app-nextcloud', 'features.oidc', False, True, True) }}" # Deactivate 2FA if oidc is active
 | 
			
		||||
  user_ldap:
 | 
			
		||||
    # Nextcloud user LDAP: integrates LDAP for user management and authentication (https://apps.nextcloud.com/apps/user_ldap)
 | 
			
		||||
    enabled: "{{ applications | get_app_conf('web-app-nextcloud', 'features.ldap', False, True) }}"
 | 
			
		||||
    enabled: "{{ applications | get_app_conf('web-app-nextcloud', 'features.ldap', False, True, True) }}"
 | 
			
		||||
    user_directory:
 | 
			
		||||
      enabled:  true # Enables the LDAP User Directory Search
 | 
			
		||||
  user_oidc:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										125
									
								
								tests/unit/module_utils/test_config_utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								tests/unit/module_utils/test_config_utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,125 @@
 | 
			
		||||
import os
 | 
			
		||||
import shutil
 | 
			
		||||
import tempfile
 | 
			
		||||
import unittest
 | 
			
		||||
 | 
			
		||||
from module_utils.config_utils import (
 | 
			
		||||
    get_app_conf,
 | 
			
		||||
    AppConfigKeyError,
 | 
			
		||||
    ConfigEntryNotSetError,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
class TestGetAppConf(unittest.TestCase):
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
        # Isolate working directory so that schema files can be discovered
 | 
			
		||||
        self._cwd = os.getcwd()
 | 
			
		||||
        self.tmpdir = tempfile.mkdtemp(prefix="cfgutilstest_")
 | 
			
		||||
        os.chdir(self.tmpdir)
 | 
			
		||||
 | 
			
		||||
        # Minimal schema structure:
 | 
			
		||||
        # roles/web-app-demo/schema/main.yml
 | 
			
		||||
        os.makedirs(os.path.join("roles", "web-app-demo", "schema"), exist_ok=True)
 | 
			
		||||
        with open(os.path.join("roles", "web-app-demo", "schema", "main.yml"), "w") as f:
 | 
			
		||||
            f.write(
 | 
			
		||||
                # Defines 'features.defined_but_unset' in schema (without a value in applications),
 | 
			
		||||
                # plus 'features.oidc' and 'features.nested.list'
 | 
			
		||||
                "features:\n"
 | 
			
		||||
                "  oidc: {}\n"
 | 
			
		||||
                "  defined_but_unset: {}\n"
 | 
			
		||||
                "  nested:\n"
 | 
			
		||||
                "    list:\n"
 | 
			
		||||
                "      - {}\n"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        # Example configuration with actual values
 | 
			
		||||
        self.applications = {
 | 
			
		||||
            "web-app-demo": {
 | 
			
		||||
                "features": {
 | 
			
		||||
                    "oidc": True,
 | 
			
		||||
                    "nested": {
 | 
			
		||||
                        "list": ["first", "second"]
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def tearDown(self):
 | 
			
		||||
        os.chdir(self._cwd)
 | 
			
		||||
        shutil.rmtree(self.tmpdir, ignore_errors=True)
 | 
			
		||||
 | 
			
		||||
    # --- Tests ---
 | 
			
		||||
 | 
			
		||||
    def test_missing_app_with_skip_missing_app_returns_default_true(self):
 | 
			
		||||
        """If app ID is missing and skip_missing_app=True, it should return the default (True)."""
 | 
			
		||||
        apps = {"some-other-app": {}}
 | 
			
		||||
        val = get_app_conf(apps, "web-app-nextcloud", "features.oidc",
 | 
			
		||||
                           strict=True, default=True, skip_missing_app=True)
 | 
			
		||||
        self.assertTrue(val)
 | 
			
		||||
 | 
			
		||||
    def test_missing_app_with_skip_missing_app_returns_default_false(self):
 | 
			
		||||
        """If app ID is missing and skip_missing_app=True, it should return the default (False)."""
 | 
			
		||||
        apps = {"svc-bkp-rmt-2-loc": {}}
 | 
			
		||||
        val = get_app_conf(apps, "web-app-nextcloud", "features.oidc",
 | 
			
		||||
                           strict=True, default=False, skip_missing_app=True)
 | 
			
		||||
        self.assertFalse(val)
 | 
			
		||||
 | 
			
		||||
    def test_missing_app_without_skip_missing_app_and_strict_true_raises(self):
 | 
			
		||||
        """Missing app ID without skip_missing_app and strict=True should raise."""
 | 
			
		||||
        apps = {}
 | 
			
		||||
        with self.assertRaises(AppConfigKeyError):
 | 
			
		||||
            get_app_conf(apps, "web-app-nextcloud", "features.oidc",
 | 
			
		||||
                         strict=True, default=True, skip_missing_app=False)
 | 
			
		||||
 | 
			
		||||
    def test_missing_app_without_skip_missing_app_and_strict_false_raises(self):
 | 
			
		||||
        apps = {}
 | 
			
		||||
        with self.assertRaises(AppConfigKeyError):
 | 
			
		||||
            get_app_conf(apps, "web-app-nextcloud", "features.oidc",
 | 
			
		||||
                        strict=False, default=True, skip_missing_app=False)
 | 
			
		||||
 | 
			
		||||
    def test_existing_app_returns_expected_value(self):
 | 
			
		||||
        """Existing app and key should return the configured value."""
 | 
			
		||||
        val = get_app_conf(self.applications, "web-app-demo", "features.oidc",
 | 
			
		||||
                           strict=True, default=False, skip_missing_app=False)
 | 
			
		||||
        self.assertTrue(val)
 | 
			
		||||
 | 
			
		||||
    def test_nested_list_index_access(self):
 | 
			
		||||
        """Accessing list indices should work correctly."""
 | 
			
		||||
        val0 = get_app_conf(self.applications, "web-app-demo", "features.nested.list[0]",
 | 
			
		||||
                            strict=True, default=None, skip_missing_app=False)
 | 
			
		||||
        val1 = get_app_conf(self.applications, "web-app-demo", "features.nested.list[1]",
 | 
			
		||||
                            strict=True, default=None, skip_missing_app=False)
 | 
			
		||||
        self.assertEqual(val0, "first")
 | 
			
		||||
        self.assertEqual(val1, "second")
 | 
			
		||||
 | 
			
		||||
    def test_schema_defined_but_unset_raises_in_strict_mode(self):
 | 
			
		||||
        """Schema-defined but unset value should raise in strict mode."""
 | 
			
		||||
        with self.assertRaises(ConfigEntryNotSetError):
 | 
			
		||||
            get_app_conf(self.applications, "web-app-demo", "features.defined_but_unset",
 | 
			
		||||
                         strict=True, default=False, skip_missing_app=False)
 | 
			
		||||
 | 
			
		||||
    def test_schema_defined_but_unset_strict_false_returns_default(self):
 | 
			
		||||
        """Schema-defined but unset value should return default when strict=False."""
 | 
			
		||||
        val = get_app_conf(self.applications, "web-app-demo", "features.defined_but_unset",
 | 
			
		||||
                           strict=False, default=True, skip_missing_app=False)
 | 
			
		||||
        self.assertTrue(val)
 | 
			
		||||
 | 
			
		||||
    def test_invalid_key_format_raises(self):
 | 
			
		||||
        """Invalid key format in path should raise AppConfigKeyError."""
 | 
			
		||||
        with self.assertRaises(AppConfigKeyError):
 | 
			
		||||
            get_app_conf(self.applications, "web-app-demo", "features.nested.list[not-an-int]",
 | 
			
		||||
                         strict=True, default=None, skip_missing_app=False)
 | 
			
		||||
 | 
			
		||||
    def test_index_out_of_range_respects_strict(self):
 | 
			
		||||
        """Out-of-range index should respect strict parameter."""
 | 
			
		||||
        # strict=False returns default
 | 
			
		||||
        val = get_app_conf(self.applications, "web-app-demo", "features.nested.list[99]",
 | 
			
		||||
                           strict=False, default="fallback", skip_missing_app=False)
 | 
			
		||||
        self.assertEqual(val, "fallback")
 | 
			
		||||
        # strict=True raises
 | 
			
		||||
        with self.assertRaises(AppConfigKeyError):
 | 
			
		||||
            get_app_conf(self.applications, "web-app-demo", "features.nested.list[99]",
 | 
			
		||||
                         strict=True, default=None, skip_missing_app=False)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    unittest.main()
 | 
			
		||||
		Reference in New Issue
	
	Block a user