From 1764be1e98a6b127495cf0693df54d2c201ee2ad Mon Sep 17 00:00:00 2001 From: Patrick Decat Date: Fri, 14 Nov 2025 16:13:40 +0100 Subject: [PATCH 1/4] test: add unit tests to demonstrate Dynaconf fresh vars behavior --- .../unittest/test_fresh_vars_functionality.py | 464 ++++++++++++++++++ 1 file changed, 464 insertions(+) create mode 100644 tests/unittest/test_fresh_vars_functionality.py diff --git a/tests/unittest/test_fresh_vars_functionality.py b/tests/unittest/test_fresh_vars_functionality.py new file mode 100644 index 00000000..3bd0a201 --- /dev/null +++ b/tests/unittest/test_fresh_vars_functionality.py @@ -0,0 +1,464 @@ +""" +Comprehensive unit tests for Dynaconf fresh_vars functionality. + +These tests verify that the fresh_vars feature works correctly with the custom_merge_loader, +particularly for the GitLab credentials use case where values should be reloaded from disk +on each access rather than being cached. + +The tests are designed to detect if fresh_vars is broken due to custom loader changes, +such as those introduced in https://github.com/qodo-ai/pr-agent/pull/2087. +""" + +import os +import tempfile +import time +from pathlib import Path +from unittest.mock import patch + +import pytest +from dynaconf import Dynaconf + +# Import get_settings at module level to complete the import chain and avoid circular import issues +# This ensures pr_agent.config_loader is fully loaded before custom_merge_loader is used in tests +from pr_agent.config_loader import get_settings # noqa: F401 + +# Module-level helper functions + + +def ensure_file_timestamp_changes(file_path): + """ + Force file timestamp to change by using os.utime with explicit time. + + This is much faster than sleeping and works on all filesystems. + + Args: + file_path: Path to the file to update + """ + # Get current time and add a small increment to ensure it's different + current_time = time.time() + new_time = current_time + 0.001 # Add 1ms + + # Set both access time and modification time + os.utime(file_path, (new_time, new_time)) + + +def create_dynaconf_with_custom_loader(temp_dir, secrets_file, fresh_vars=None): + """ + Create a Dynaconf instance matching the production configuration. + + This mimics the config_loader.py setup with: + - core_loaders disabled + - custom_merge_loader and env_loader enabled + - merge_enabled = True + + Args: + temp_dir: Temporary directory path + secrets_file: Path to secrets file + fresh_vars: List of section names to mark as fresh (e.g., ["GITLAB"]) + + Returns: + Dynaconf instance configured like production + """ + kwargs = { + "core_loaders": [], + "loaders": ["pr_agent.custom_merge_loader", "dynaconf.loaders.env_loader"], + "root_path": temp_dir, + "merge_enabled": True, + "envvar_prefix": False, + "load_dotenv": False, + "settings_files": [str(secrets_file)], + } + + if fresh_vars: + kwargs["fresh_vars"] = fresh_vars + + return Dynaconf(**kwargs) + + +class TestFreshVarsGitLabScenario: + """ + Test fresh_vars functionality for the GitLab credentials use case. + + This class tests the specific scenario where: + - FRESH_VARS_FOR_DYNACONF='["GITLAB"]' is set + - .secrets.toml contains gitlab.personal_access_token and gitlab.shared_secret + - Values should be reloaded from disk on each access (not cached) + """ + + def setup_method(self): + """Set up temporary directory and files for each test.""" + self.temp_dir = tempfile.mkdtemp() + self.secrets_file = Path(self.temp_dir) / ".secrets.toml" + + def teardown_method(self): + """Clean up temporary files after each test.""" + import shutil + + if hasattr(self, "temp_dir") and Path(self.temp_dir).exists(): + shutil.rmtree(self.temp_dir) + + def create_secrets_toml(self, personal_access_token="initial_token", shared_secret="initial_secret"): + """ + Create a .secrets.toml file with GitLab credentials. + + Args: + personal_access_token: The GitLab personal access token value + shared_secret: The GitLab shared secret value + """ + content = f"""[gitlab] +personal_access_token = "{personal_access_token}" +shared_secret = "{shared_secret}" +""" + self.secrets_file.write_text(content) + + def test_gitlab_personal_access_token_reload(self): + """ + Test that gitlab.personal_access_token is reloaded when marked as fresh. + + This is the critical test for the user's use case. It verifies that: + 1. Initial value is loaded correctly + 2. After modifying the file, the new value is returned (not cached) + 3. This works with the custom_merge_loader + """ + # Create initial secrets file + self.create_secrets_toml(personal_access_token="token_v1", shared_secret="secret_v1") + + # Create Dynaconf with GITLAB marked as fresh + settings = create_dynaconf_with_custom_loader(self.temp_dir, self.secrets_file, fresh_vars=["GITLAB"]) + + # First access - should return initial value + first_token = settings.GITLAB.PERSONAL_ACCESS_TOKEN + assert first_token == "token_v1", "Initial personal_access_token should be 'token_v1'" + + # Modify the secrets file + ensure_file_timestamp_changes(self.secrets_file) + self.create_secrets_toml(personal_access_token="token_v2_updated", shared_secret="secret_v1") + + # Second access - should return NEW value (not cached) + second_token = settings.GITLAB.PERSONAL_ACCESS_TOKEN + assert second_token == "token_v2_updated", ( + "After file modification, personal_access_token should be reloaded to 'token_v2_updated'" + ) + + # Verify the values are different (fresh_vars working) + assert first_token != second_token, "fresh_vars should cause values to be reloaded, not cached" + + def test_gitlab_shared_secret_reload(self): + """ + Test that gitlab.shared_secret is reloaded when GITLAB is marked as fresh. + + Verifies that fresh_vars applies to all fields in the GITLAB section, + not just personal_access_token. + """ + # Create initial secrets file + self.create_secrets_toml(personal_access_token="token_v1", shared_secret="secret_v1") + + # Create Dynaconf with GITLAB marked as fresh + settings = create_dynaconf_with_custom_loader(self.temp_dir, self.secrets_file, fresh_vars=["GITLAB"]) + + # First access + first_secret = settings.GITLAB.SHARED_SECRET + assert first_secret == "secret_v1", "Initial shared_secret should be 'secret_v1'" + + # Modify the secrets file + ensure_file_timestamp_changes(self.secrets_file) + self.create_secrets_toml(personal_access_token="token_v1", shared_secret="secret_v2_updated") + + # Second access - should return NEW value + second_secret = settings.GITLAB.SHARED_SECRET + assert second_secret == "secret_v2_updated", ( + "After file modification, shared_secret should be reloaded to 'secret_v2_updated'" + ) + + # Verify fresh_vars is working + assert first_secret != second_secret, "fresh_vars should cause shared_secret to be reloaded" + + def test_gitlab_multiple_fields_reload(self): + """ + Test that both gitlab fields reload together when GITLAB is marked as fresh. + + This verifies that fresh_vars works correctly when multiple fields + in the same section are modified simultaneously. + """ + # Create initial secrets file + self.create_secrets_toml(personal_access_token="token_v1", shared_secret="secret_v1") + + # Create Dynaconf with GITLAB marked as fresh + settings = create_dynaconf_with_custom_loader(self.temp_dir, self.secrets_file, fresh_vars=["GITLAB"]) + + # First access - both fields + first_token = settings.GITLAB.PERSONAL_ACCESS_TOKEN + first_secret = settings.GITLAB.SHARED_SECRET + assert first_token == "token_v1" + assert first_secret == "secret_v1" + + # Modify both fields in the secrets file + ensure_file_timestamp_changes(self.secrets_file) + self.create_secrets_toml(personal_access_token="token_v2_both_updated", shared_secret="secret_v2_both_updated") + + # Second access - both fields should be updated + second_token = settings.GITLAB.PERSONAL_ACCESS_TOKEN + second_secret = settings.GITLAB.SHARED_SECRET + + assert second_token == "token_v2_both_updated", "personal_access_token should be reloaded" + assert second_secret == "secret_v2_both_updated", "shared_secret should be reloaded" + + # Verify both fields were reloaded + assert first_token != second_token, "personal_access_token should not be cached" + assert first_secret != second_secret, "shared_secret should not be cached" + + +class TestFreshVarsCustomLoaderIntegration: + """ + Test fresh_vars integration with custom_merge_loader. + + These tests verify that fresh_vars works correctly when using the + custom_merge_loader instead of Dynaconf's default core loaders. + """ + + def setup_method(self): + """Set up temporary directory and files for each test.""" + self.temp_dir = tempfile.mkdtemp() + self.secrets_file = Path(self.temp_dir) / ".secrets.toml" + + def teardown_method(self): + """Clean up temporary files after each test.""" + import shutil + + if hasattr(self, "temp_dir") and Path(self.temp_dir).exists(): + shutil.rmtree(self.temp_dir) + + def create_secrets_toml(self, personal_access_token="initial_token", shared_secret="initial_secret"): + """Create a .secrets.toml file with GitLab credentials.""" + content = f"""[gitlab] +personal_access_token = "{personal_access_token}" +shared_secret = "{shared_secret}" +""" + self.secrets_file.write_text(content) + + def test_fresh_vars_without_core_loaders(self): + """ + Critical test: Verify fresh_vars works when core_loaders are disabled. + + This test detects if the bug exists where fresh_vars stops working + when core_loaders=[] is set. This is the key issue that may have been + introduced by the custom_merge_loader changes. + + Expected behavior: + - If fresh_vars works: second_value != first_value + - If fresh_vars is broken: second_value == first_value (cached) + """ + # Create initial secrets file + self.create_secrets_toml(personal_access_token="token_before_bug_test") + + # Create Dynaconf WITHOUT core loaders but WITH fresh_vars + settings = create_dynaconf_with_custom_loader(self.temp_dir, self.secrets_file, fresh_vars=["GITLAB"]) + + # First access + first_value = settings.GITLAB.PERSONAL_ACCESS_TOKEN + assert first_value == "token_before_bug_test", "Initial value should be loaded correctly" + + # Modify the file + ensure_file_timestamp_changes(self.secrets_file) + self.create_secrets_toml(personal_access_token="token_after_bug_test") + + # Second access - THIS IS THE CRITICAL CHECK + second_value = settings.GITLAB.PERSONAL_ACCESS_TOKEN + + # If this assertion fails, fresh_vars is broken with custom_merge_loader + assert second_value == "token_after_bug_test", ( + "CRITICAL: fresh_vars should reload the value even with core_loaders=[]" + ) + + assert first_value != second_value, "CRITICAL: Values should be different, indicating fresh_vars is working" + + def test_custom_loader_respects_fresh_vars(self): + """ + Test that custom_merge_loader respects the fresh_vars configuration. + + Verifies that when a section is marked as fresh, the custom loader + doesn't cache values from that section. + """ + # Create initial secrets file with multiple sections + content = """[gitlab] +personal_access_token = "gitlab_token_v1" + +[github] +user_token = "github_token_v1" +""" + self.secrets_file.write_text(content) + + # Create Dynaconf with only GITLAB marked as fresh + settings = create_dynaconf_with_custom_loader(self.temp_dir, self.secrets_file, fresh_vars=["GITLAB"]) + + # Access both sections + gitlab_token_1 = settings.GITLAB.PERSONAL_ACCESS_TOKEN + github_token_1 = settings.GITHUB.USER_TOKEN + + # Modify both sections + ensure_file_timestamp_changes(self.secrets_file) + content = """[gitlab] +personal_access_token = "gitlab_token_v2" + +[github] +user_token = "github_token_v2" +""" + self.secrets_file.write_text(content) + + # Access again + gitlab_token_2 = settings.GITLAB.PERSONAL_ACCESS_TOKEN + github_token_2 = settings.GITHUB.USER_TOKEN + + # GITLAB should be reloaded (marked as fresh) + assert gitlab_token_2 == "gitlab_token_v2", "GITLAB section should be reloaded (marked as fresh)" + assert gitlab_token_1 != gitlab_token_2, "GITLAB values should not be cached" + + # GITHUB should be cached (not marked as fresh) + assert github_token_2 == "github_token_v1", "GITHUB section should be cached (not marked as fresh)" + assert github_token_1 == github_token_2, "GITHUB values should be cached" + + +class TestFreshVarsBasicFunctionality: + """ + Test basic fresh_vars functionality and edge cases. + + These tests verify fundamental fresh_vars behavior and ensure + the feature works as expected in various scenarios. + """ + + def setup_method(self): + """Set up temporary directory and files for each test.""" + self.temp_dir = tempfile.mkdtemp() + self.secrets_file = Path(self.temp_dir) / ".secrets.toml" + + def teardown_method(self): + """Clean up temporary files after each test.""" + import shutil + + if hasattr(self, "temp_dir") and Path(self.temp_dir).exists(): + shutil.rmtree(self.temp_dir) + + def create_secrets_toml(self, personal_access_token="initial_token"): + """Create a .secrets.toml file with GitLab credentials.""" + content = f"""[gitlab] +personal_access_token = "{personal_access_token}" +""" + self.secrets_file.write_text(content) + + def test_fresh_vars_from_environment_variable(self): + """ + Test that fresh_vars can be set via FRESH_VARS_FOR_DYNACONF environment variable. + + This tests the common use case where fresh_vars is configured through + an environment variable rather than in code. + """ + # Create initial secrets file + self.create_secrets_toml(personal_access_token="env_token_v1") + + # Set FRESH_VARS_FOR_DYNACONF environment variable + with patch.dict(os.environ, {"FRESH_VARS_FOR_DYNACONF": '["GITLAB"]'}): + # Create Dynaconf (should pick up fresh_vars from env) + settings = create_dynaconf_with_custom_loader(self.temp_dir, self.secrets_file) + + # First access + first_value = settings.GITLAB.PERSONAL_ACCESS_TOKEN + assert first_value == "env_token_v1" + + # Modify file + ensure_file_timestamp_changes(self.secrets_file) + self.create_secrets_toml(personal_access_token="env_token_v2") + + # Second access - should be reloaded + second_value = settings.GITLAB.PERSONAL_ACCESS_TOKEN + assert second_value == "env_token_v2", "fresh_vars from environment variable should cause reload" + + assert first_value != second_value, "Values should be different when fresh_vars is set via env var" + + def test_gitlab_credentials_not_cached_when_fresh(self): + """ + Test that GitLab credentials are not cached when marked as fresh. + + This verifies the core requirement: when GITLAB is in fresh_vars, + accessing the credentials multiple times should reload from disk + each time, not return a cached value. + """ + # Create initial secrets file + self.create_secrets_toml(personal_access_token="no_cache_v1") + + # Create Dynaconf with GITLAB marked as fresh + settings = create_dynaconf_with_custom_loader(self.temp_dir, self.secrets_file, fresh_vars=["GITLAB"]) + + # Access the token multiple times before modification + access_1 = settings.GITLAB.PERSONAL_ACCESS_TOKEN + access_2 = settings.GITLAB.PERSONAL_ACCESS_TOKEN + access_3 = settings.GITLAB.PERSONAL_ACCESS_TOKEN + + # All should return the same value (file hasn't changed) + assert access_1 == access_2 == access_3 == "no_cache_v1", ( + "Multiple accesses before modification should return same value" + ) + + # Modify the file + ensure_file_timestamp_changes(self.secrets_file) + self.create_secrets_toml(personal_access_token="no_cache_v2") + + # Access again - should get new value immediately + access_4 = settings.GITLAB.PERSONAL_ACCESS_TOKEN + assert access_4 == "no_cache_v2", "First access after modification should return new value" + + # Verify no caching occurred + assert access_1 != access_4, "Value should change after file modification (no caching)" + + # Modify again + ensure_file_timestamp_changes(self.secrets_file) + self.create_secrets_toml(personal_access_token="no_cache_v3") + + # Access again - should get newest value + access_5 = settings.GITLAB.PERSONAL_ACCESS_TOKEN + assert access_5 == "no_cache_v3", "Second modification should also be detected" + + # Verify the progression + assert access_1 != access_4 != access_5, "Each modification should result in a different value (no caching)" + + def test_fresh_vars_works_with_default_loaders(self): + """ + Test that fresh_vars works correctly with Dynaconf's default core loaders. + + This is a control test to prove that fresh_vars functionality works + as expected when using the standard Dynaconf configuration (with core_loaders). + This helps isolate the bug to the custom_merge_loader configuration. + """ + # Create initial secrets file + self.create_secrets_toml(personal_access_token="default_v1") + + # Create Dynaconf with DEFAULT loaders (not custom_merge_loader) + settings = Dynaconf( + # Use default core_loaders (don't disable them) + root_path=self.temp_dir, + merge_enabled=True, + envvar_prefix=False, + load_dotenv=False, + settings_files=[str(self.secrets_file)], + fresh_vars=["GITLAB"], + ) + + # First access + first_value = settings.GITLAB.PERSONAL_ACCESS_TOKEN + assert first_value == "default_v1" + + # Modify file + ensure_file_timestamp_changes(self.secrets_file) + self.create_secrets_toml(personal_access_token="default_v2") + + # Second access - should be reloaded with default loaders + second_value = settings.GITLAB.PERSONAL_ACCESS_TOKEN + assert second_value == "default_v2", ( + "With default loaders, fresh_vars SHOULD work correctly. " + "If this test fails, the issue is not specific to custom_merge_loader." + ) + + assert first_value != second_value, "Values should be different when using default loaders with fresh_vars" + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) From 4f5e1f708c22f2b224091437b8e1b78245f14a74 Mon Sep 17 00:00:00 2001 From: Patrick Decat Date: Fri, 14 Nov 2025 16:52:00 +0100 Subject: [PATCH 2/4] fix: compare keys as uppercase to restore Dynaconf fresh_vars support --- pr_agent/custom_merge_loader.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pr_agent/custom_merge_loader.py b/pr_agent/custom_merge_loader.py index 8aa29e8d..75b07a77 100644 --- a/pr_agent/custom_merge_loader.py +++ b/pr_agent/custom_merge_loader.py @@ -12,6 +12,7 @@ def load(obj, env=None, silent=True, key=None, filename=None): - Replaces list and dict fields instead of appending/updating (non-default Dynaconf behavior). - Enforces several security checks (e.g., disallows includes/preloads and enforces .toml files). - Supports optional single-key loading. + - Supports Dynaconf's fresh_vars feature for dynamic reloading. Args: obj: The Dynaconf settings instance to update. env: The current environment name (upper case). Defaults to 'DEVELOPMENT'. Note: currently unused. @@ -93,7 +94,8 @@ def load(obj, env=None, silent=True, key=None, filename=None): # Update the settings object for k, v in accumulated_data.items(): - if key is None or key == k: + # For fresh_vars support: key parameter is uppercase, but accumulated_data keys are lowercase + if key is None or key.upper() == k.upper(): obj.set(k, v) def validate_file_security(file_data, filename): From 979bc6788ef01856f78e28a6230dfda5584b082d Mon Sep 17 00:00:00 2001 From: Patrick Decat Date: Sat, 15 Nov 2025 09:15:05 +0100 Subject: [PATCH 3/4] test: remove ensure_file_timestamp_changes, tests pass without it --- .../unittest/test_fresh_vars_functionality.py | 30 +------------------ 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/tests/unittest/test_fresh_vars_functionality.py b/tests/unittest/test_fresh_vars_functionality.py index 3bd0a201..8b7cee7e 100644 --- a/tests/unittest/test_fresh_vars_functionality.py +++ b/tests/unittest/test_fresh_vars_functionality.py @@ -11,7 +11,6 @@ such as those introduced in https://github.com/qodo-ai/pr-agent/pull/2087. import os import tempfile -import time from pathlib import Path from unittest.mock import patch @@ -22,26 +21,8 @@ from dynaconf import Dynaconf # This ensures pr_agent.config_loader is fully loaded before custom_merge_loader is used in tests from pr_agent.config_loader import get_settings # noqa: F401 -# Module-level helper functions - - -def ensure_file_timestamp_changes(file_path): - """ - Force file timestamp to change by using os.utime with explicit time. - - This is much faster than sleeping and works on all filesystems. - - Args: - file_path: Path to the file to update - """ - # Get current time and add a small increment to ensure it's different - current_time = time.time() - new_time = current_time + 0.001 # Add 1ms - - # Set both access time and modification time - os.utime(file_path, (new_time, new_time)) - +# Module-level helper function def create_dynaconf_with_custom_loader(temp_dir, secrets_file, fresh_vars=None): """ Create a Dynaconf instance matching the production configuration. @@ -131,7 +112,6 @@ shared_secret = "{shared_secret}" assert first_token == "token_v1", "Initial personal_access_token should be 'token_v1'" # Modify the secrets file - ensure_file_timestamp_changes(self.secrets_file) self.create_secrets_toml(personal_access_token="token_v2_updated", shared_secret="secret_v1") # Second access - should return NEW value (not cached) @@ -161,7 +141,6 @@ shared_secret = "{shared_secret}" assert first_secret == "secret_v1", "Initial shared_secret should be 'secret_v1'" # Modify the secrets file - ensure_file_timestamp_changes(self.secrets_file) self.create_secrets_toml(personal_access_token="token_v1", shared_secret="secret_v2_updated") # Second access - should return NEW value @@ -193,7 +172,6 @@ shared_secret = "{shared_secret}" assert first_secret == "secret_v1" # Modify both fields in the secrets file - ensure_file_timestamp_changes(self.secrets_file) self.create_secrets_toml(personal_access_token="token_v2_both_updated", shared_secret="secret_v2_both_updated") # Second access - both fields should be updated @@ -259,7 +237,6 @@ shared_secret = "{shared_secret}" assert first_value == "token_before_bug_test", "Initial value should be loaded correctly" # Modify the file - ensure_file_timestamp_changes(self.secrets_file) self.create_secrets_toml(personal_access_token="token_after_bug_test") # Second access - THIS IS THE CRITICAL CHECK @@ -296,7 +273,6 @@ user_token = "github_token_v1" github_token_1 = settings.GITHUB.USER_TOKEN # Modify both sections - ensure_file_timestamp_changes(self.secrets_file) content = """[gitlab] personal_access_token = "gitlab_token_v2" @@ -365,7 +341,6 @@ personal_access_token = "{personal_access_token}" assert first_value == "env_token_v1" # Modify file - ensure_file_timestamp_changes(self.secrets_file) self.create_secrets_toml(personal_access_token="env_token_v2") # Second access - should be reloaded @@ -399,7 +374,6 @@ personal_access_token = "{personal_access_token}" ) # Modify the file - ensure_file_timestamp_changes(self.secrets_file) self.create_secrets_toml(personal_access_token="no_cache_v2") # Access again - should get new value immediately @@ -410,7 +384,6 @@ personal_access_token = "{personal_access_token}" assert access_1 != access_4, "Value should change after file modification (no caching)" # Modify again - ensure_file_timestamp_changes(self.secrets_file) self.create_secrets_toml(personal_access_token="no_cache_v3") # Access again - should get newest value @@ -447,7 +420,6 @@ personal_access_token = "{personal_access_token}" assert first_value == "default_v1" # Modify file - ensure_file_timestamp_changes(self.secrets_file) self.create_secrets_toml(personal_access_token="default_v2") # Second access - should be reloaded with default loaders From d3878ef4128145c52aa01e6d4dc9dd62a5a4eb27 Mon Sep 17 00:00:00 2001 From: Patrick Decat Date: Sat, 15 Nov 2025 09:25:15 +0100 Subject: [PATCH 4/4] test: only set fresh vars via FRESH_VARS_FOR_DYNACONF as that's the only actual way to use it with pr-agent --- .../unittest/test_fresh_vars_functionality.py | 194 +++++++----------- 1 file changed, 73 insertions(+), 121 deletions(-) diff --git a/tests/unittest/test_fresh_vars_functionality.py b/tests/unittest/test_fresh_vars_functionality.py index 8b7cee7e..fe2cfa15 100644 --- a/tests/unittest/test_fresh_vars_functionality.py +++ b/tests/unittest/test_fresh_vars_functionality.py @@ -23,7 +23,7 @@ from pr_agent.config_loader import get_settings # noqa: F401 # Module-level helper function -def create_dynaconf_with_custom_loader(temp_dir, secrets_file, fresh_vars=None): +def create_dynaconf_with_custom_loader(temp_dir, secrets_file): """ Create a Dynaconf instance matching the production configuration. @@ -32,28 +32,25 @@ def create_dynaconf_with_custom_loader(temp_dir, secrets_file, fresh_vars=None): - custom_merge_loader and env_loader enabled - merge_enabled = True + Note: fresh_vars should be configured via FRESH_VARS_FOR_DYNACONF environment variable, + which is the only way to configure it in pr-agent. + Args: temp_dir: Temporary directory path secrets_file: Path to secrets file - fresh_vars: List of section names to mark as fresh (e.g., ["GITLAB"]) Returns: Dynaconf instance configured like production """ - kwargs = { - "core_loaders": [], - "loaders": ["pr_agent.custom_merge_loader", "dynaconf.loaders.env_loader"], - "root_path": temp_dir, - "merge_enabled": True, - "envvar_prefix": False, - "load_dotenv": False, - "settings_files": [str(secrets_file)], - } - - if fresh_vars: - kwargs["fresh_vars"] = fresh_vars - - return Dynaconf(**kwargs) + return Dynaconf( + core_loaders=[], + loaders=["pr_agent.custom_merge_loader", "dynaconf.loaders.env_loader"], + root_path=temp_dir, + merge_enabled=True, + envvar_prefix=False, + load_dotenv=False, + settings_files=[str(secrets_file)], + ) class TestFreshVarsGitLabScenario: @@ -104,11 +101,13 @@ shared_secret = "{shared_secret}" # Create initial secrets file self.create_secrets_toml(personal_access_token="token_v1", shared_secret="secret_v1") - # Create Dynaconf with GITLAB marked as fresh - settings = create_dynaconf_with_custom_loader(self.temp_dir, self.secrets_file, fresh_vars=["GITLAB"]) + # Set FRESH_VARS_FOR_DYNACONF environment variable (the only way to configure fresh_vars in pr-agent) + with patch.dict(os.environ, {"FRESH_VARS_FOR_DYNACONF": '["GITLAB"]'}): + # Create Dynaconf with GITLAB marked as fresh via env var + settings = create_dynaconf_with_custom_loader(self.temp_dir, self.secrets_file) - # First access - should return initial value - first_token = settings.GITLAB.PERSONAL_ACCESS_TOKEN + # First access - should return initial value + first_token = settings.GITLAB.PERSONAL_ACCESS_TOKEN assert first_token == "token_v1", "Initial personal_access_token should be 'token_v1'" # Modify the secrets file @@ -123,35 +122,6 @@ shared_secret = "{shared_secret}" # Verify the values are different (fresh_vars working) assert first_token != second_token, "fresh_vars should cause values to be reloaded, not cached" - def test_gitlab_shared_secret_reload(self): - """ - Test that gitlab.shared_secret is reloaded when GITLAB is marked as fresh. - - Verifies that fresh_vars applies to all fields in the GITLAB section, - not just personal_access_token. - """ - # Create initial secrets file - self.create_secrets_toml(personal_access_token="token_v1", shared_secret="secret_v1") - - # Create Dynaconf with GITLAB marked as fresh - settings = create_dynaconf_with_custom_loader(self.temp_dir, self.secrets_file, fresh_vars=["GITLAB"]) - - # First access - first_secret = settings.GITLAB.SHARED_SECRET - assert first_secret == "secret_v1", "Initial shared_secret should be 'secret_v1'" - - # Modify the secrets file - self.create_secrets_toml(personal_access_token="token_v1", shared_secret="secret_v2_updated") - - # Second access - should return NEW value - second_secret = settings.GITLAB.SHARED_SECRET - assert second_secret == "secret_v2_updated", ( - "After file modification, shared_secret should be reloaded to 'secret_v2_updated'" - ) - - # Verify fresh_vars is working - assert first_secret != second_secret, "fresh_vars should cause shared_secret to be reloaded" - def test_gitlab_multiple_fields_reload(self): """ Test that both gitlab fields reload together when GITLAB is marked as fresh. @@ -162,28 +132,32 @@ shared_secret = "{shared_secret}" # Create initial secrets file self.create_secrets_toml(personal_access_token="token_v1", shared_secret="secret_v1") - # Create Dynaconf with GITLAB marked as fresh - settings = create_dynaconf_with_custom_loader(self.temp_dir, self.secrets_file, fresh_vars=["GITLAB"]) + # Set FRESH_VARS_FOR_DYNACONF environment variable + with patch.dict(os.environ, {"FRESH_VARS_FOR_DYNACONF": '["GITLAB"]'}): + # Create Dynaconf with GITLAB marked as fresh via env var + settings = create_dynaconf_with_custom_loader(self.temp_dir, self.secrets_file) - # First access - both fields - first_token = settings.GITLAB.PERSONAL_ACCESS_TOKEN - first_secret = settings.GITLAB.SHARED_SECRET - assert first_token == "token_v1" - assert first_secret == "secret_v1" + # First access - both fields + first_token = settings.GITLAB.PERSONAL_ACCESS_TOKEN + first_secret = settings.GITLAB.SHARED_SECRET + assert first_token == "token_v1" + assert first_secret == "secret_v1" - # Modify both fields in the secrets file - self.create_secrets_toml(personal_access_token="token_v2_both_updated", shared_secret="secret_v2_both_updated") + # Modify both fields in the secrets file + self.create_secrets_toml( + personal_access_token="token_v2_both_updated", shared_secret="secret_v2_both_updated" + ) - # Second access - both fields should be updated - second_token = settings.GITLAB.PERSONAL_ACCESS_TOKEN - second_secret = settings.GITLAB.SHARED_SECRET + # Second access - both fields should be updated + second_token = settings.GITLAB.PERSONAL_ACCESS_TOKEN + second_secret = settings.GITLAB.SHARED_SECRET - assert second_token == "token_v2_both_updated", "personal_access_token should be reloaded" - assert second_secret == "secret_v2_both_updated", "shared_secret should be reloaded" + assert second_token == "token_v2_both_updated", "personal_access_token should be reloaded" + assert second_secret == "secret_v2_both_updated", "shared_secret should be reloaded" - # Verify both fields were reloaded - assert first_token != second_token, "personal_access_token should not be cached" - assert first_secret != second_secret, "shared_secret should not be cached" + # Verify both fields were reloaded + assert first_token != second_token, "personal_access_token should not be cached" + assert first_secret != second_secret, "shared_secret should not be cached" class TestFreshVarsCustomLoaderIntegration: @@ -229,11 +203,13 @@ shared_secret = "{shared_secret}" # Create initial secrets file self.create_secrets_toml(personal_access_token="token_before_bug_test") - # Create Dynaconf WITHOUT core loaders but WITH fresh_vars - settings = create_dynaconf_with_custom_loader(self.temp_dir, self.secrets_file, fresh_vars=["GITLAB"]) + # Set FRESH_VARS_FOR_DYNACONF environment variable + with patch.dict(os.environ, {"FRESH_VARS_FOR_DYNACONF": '["GITLAB"]'}): + # Create Dynaconf WITHOUT core loaders but WITH fresh_vars via env var + settings = create_dynaconf_with_custom_loader(self.temp_dir, self.secrets_file) - # First access - first_value = settings.GITLAB.PERSONAL_ACCESS_TOKEN + # First access + first_value = settings.GITLAB.PERSONAL_ACCESS_TOKEN assert first_value == "token_before_bug_test", "Initial value should be loaded correctly" # Modify the file @@ -265,33 +241,35 @@ user_token = "github_token_v1" """ self.secrets_file.write_text(content) - # Create Dynaconf with only GITLAB marked as fresh - settings = create_dynaconf_with_custom_loader(self.temp_dir, self.secrets_file, fresh_vars=["GITLAB"]) + # Set FRESH_VARS_FOR_DYNACONF environment variable (only GITLAB) + with patch.dict(os.environ, {"FRESH_VARS_FOR_DYNACONF": '["GITLAB"]'}): + # Create Dynaconf with only GITLAB marked as fresh via env var + settings = create_dynaconf_with_custom_loader(self.temp_dir, self.secrets_file) - # Access both sections - gitlab_token_1 = settings.GITLAB.PERSONAL_ACCESS_TOKEN - github_token_1 = settings.GITHUB.USER_TOKEN + # Access both sections + gitlab_token_1 = settings.GITLAB.PERSONAL_ACCESS_TOKEN + github_token_1 = settings.GITHUB.USER_TOKEN - # Modify both sections - content = """[gitlab] + # Modify both sections + content = """[gitlab] personal_access_token = "gitlab_token_v2" [github] user_token = "github_token_v2" """ - self.secrets_file.write_text(content) + self.secrets_file.write_text(content) - # Access again - gitlab_token_2 = settings.GITLAB.PERSONAL_ACCESS_TOKEN - github_token_2 = settings.GITHUB.USER_TOKEN + # Access again + gitlab_token_2 = settings.GITLAB.PERSONAL_ACCESS_TOKEN + github_token_2 = settings.GITHUB.USER_TOKEN - # GITLAB should be reloaded (marked as fresh) - assert gitlab_token_2 == "gitlab_token_v2", "GITLAB section should be reloaded (marked as fresh)" - assert gitlab_token_1 != gitlab_token_2, "GITLAB values should not be cached" + # GITLAB should be reloaded (marked as fresh) + assert gitlab_token_2 == "gitlab_token_v2", "GITLAB section should be reloaded (marked as fresh)" + assert gitlab_token_1 != gitlab_token_2, "GITLAB values should not be cached" - # GITHUB should be cached (not marked as fresh) - assert github_token_2 == "github_token_v1", "GITHUB section should be cached (not marked as fresh)" - assert github_token_1 == github_token_2, "GITHUB values should be cached" + # GITHUB should be cached (not marked as fresh) + assert github_token_2 == "github_token_v1", "GITHUB section should be cached (not marked as fresh)" + assert github_token_1 == github_token_2, "GITHUB values should be cached" class TestFreshVarsBasicFunctionality: @@ -321,34 +299,6 @@ personal_access_token = "{personal_access_token}" """ self.secrets_file.write_text(content) - def test_fresh_vars_from_environment_variable(self): - """ - Test that fresh_vars can be set via FRESH_VARS_FOR_DYNACONF environment variable. - - This tests the common use case where fresh_vars is configured through - an environment variable rather than in code. - """ - # Create initial secrets file - self.create_secrets_toml(personal_access_token="env_token_v1") - - # Set FRESH_VARS_FOR_DYNACONF environment variable - with patch.dict(os.environ, {"FRESH_VARS_FOR_DYNACONF": '["GITLAB"]'}): - # Create Dynaconf (should pick up fresh_vars from env) - settings = create_dynaconf_with_custom_loader(self.temp_dir, self.secrets_file) - - # First access - first_value = settings.GITLAB.PERSONAL_ACCESS_TOKEN - assert first_value == "env_token_v1" - - # Modify file - self.create_secrets_toml(personal_access_token="env_token_v2") - - # Second access - should be reloaded - second_value = settings.GITLAB.PERSONAL_ACCESS_TOKEN - assert second_value == "env_token_v2", "fresh_vars from environment variable should cause reload" - - assert first_value != second_value, "Values should be different when fresh_vars is set via env var" - def test_gitlab_credentials_not_cached_when_fresh(self): """ Test that GitLab credentials are not cached when marked as fresh. @@ -360,13 +310,15 @@ personal_access_token = "{personal_access_token}" # Create initial secrets file self.create_secrets_toml(personal_access_token="no_cache_v1") - # Create Dynaconf with GITLAB marked as fresh - settings = create_dynaconf_with_custom_loader(self.temp_dir, self.secrets_file, fresh_vars=["GITLAB"]) + # Set FRESH_VARS_FOR_DYNACONF environment variable + with patch.dict(os.environ, {"FRESH_VARS_FOR_DYNACONF": '["GITLAB"]'}): + # Create Dynaconf with GITLAB marked as fresh via env var + settings = create_dynaconf_with_custom_loader(self.temp_dir, self.secrets_file) - # Access the token multiple times before modification - access_1 = settings.GITLAB.PERSONAL_ACCESS_TOKEN - access_2 = settings.GITLAB.PERSONAL_ACCESS_TOKEN - access_3 = settings.GITLAB.PERSONAL_ACCESS_TOKEN + # Access the token multiple times before modification + access_1 = settings.GITLAB.PERSONAL_ACCESS_TOKEN + access_2 = settings.GITLAB.PERSONAL_ACCESS_TOKEN + access_3 = settings.GITLAB.PERSONAL_ACCESS_TOKEN # All should return the same value (file hasn't changed) assert access_1 == access_2 == access_3 == "no_cache_v1", (