This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 19eebf29fe3 test: Add comprehensive variable import tests and refactor 
with fixtures (#54548)
19eebf29fe3 is described below

commit 19eebf29fe3c1e1b78f391106f8edca25252ab36
Author: Evgenii Prusov <[email protected]>
AuthorDate: Sat Aug 16 01:34:11 2025 +0200

    test: Add comprehensive variable import tests and refactor with fixtures 
(#54548)
    
    Add test coverage for:
      - YAML/JSON/ENV format imports with and without descriptions
      - Cross-format compatibility validation
      - Invalid format error handling
      - Action-on-existing-key parameter (skip/overwrite/fail)
    
      Refactor tests with reusable fixtures:
      - Extract test data into simple_variable_data, 
variable_data_with_descriptions fixtures
      - Add create_variable_file factory for DRY file creation
      - Simplify test logic by separating infrastructure from assertions
      - Add missing JSON import tests for format parity
    
    Co-authored-by: Evgenii Prusov <[email protected]>
---
 .../unit/cli/commands/test_variable_command.py     | 331 ++++++++++++++++++++-
 1 file changed, 321 insertions(+), 10 deletions(-)

diff --git a/airflow-core/tests/unit/cli/commands/test_variable_command.py 
b/airflow-core/tests/unit/cli/commands/test_variable_command.py
index 053ffa5b48f..8aff9c73d76 100644
--- a/airflow-core/tests/unit/cli/commands/test_variable_command.py
+++ b/airflow-core/tests/unit/cli/commands/test_variable_command.py
@@ -21,6 +21,7 @@ import json
 import os
 
 import pytest
+import yaml
 from sqlalchemy import select
 
 from airflow import models
@@ -34,6 +35,86 @@ from tests_common.test_utils.db import clear_db_variables
 pytestmark = pytest.mark.db_test
 
 
+# Test data fixtures
[email protected]
+def simple_variable_data():
+    """Simple variables without descriptions for testing"""
+    return {
+        "key1": "value1",
+        "key2": {"nested": "dict", "with": ["list", "values"]},
+        "key3": 123,
+        "key4": True,
+        "key5": None,
+    }
+
+
[email protected]
+def variable_data_with_descriptions():
+    """Variables with descriptions for testing"""
+    return {
+        "var1": {"value": "test_value", "description": "Test description for 
var1"},
+        "var2": {"value": {"complex": "object"}, "description": "Complex 
variable"},
+        "var3": "simple_value",  # No description
+    }
+
+
[email protected]
+def env_variable_data():
+    """ENV format test data"""
+    return """# Comment line
+KEY_A=value_a
+KEY_B=value with spaces
+KEY_C={"json": "value", "number": 42}
+
+# Another comment
+KEY_D=true
+KEY_E=123
+"""
+
+
+# Factory fixture for creating variable files
[email protected]
+def create_variable_file(tmp_path):
+    """Factory to create variable files in different formats"""
+
+    def _create(data, format="yaml", filename=None):
+        # Determine filename
+        if not filename:
+            ext = "yml" if format == "yml" else format
+            filename = f"variables.{ext}"
+
+        file_path = tmp_path / filename
+
+        if format in ["yaml", "yml"]:
+            with open(file_path, "w") as f:
+                yaml.dump(data, f)
+        elif format == "json":
+            with open(file_path, "w") as f:
+                json.dump(data, f)
+        elif format == "env":
+            # Handle string content directly for ENV format
+            if isinstance(data, str):
+                file_path.write_text(data)
+            else:
+                # Convert dict to env format
+                lines = []
+                for key, value in data.items():
+                    if isinstance(value, dict) and "value" in value:
+                        actual_value = value["value"]
+                    else:
+                        actual_value = value
+
+                    if isinstance(actual_value, dict):
+                        lines.append(f"{key}={json.dumps(actual_value)}")
+                    else:
+                        lines.append(f"{key}={actual_value}")
+                file_path.write_text("\n".join(lines))
+
+        return file_path
+
+    return _create
+
+
 class TestCliVariables:
     @classmethod
     def setup_class(cls):
@@ -112,9 +193,10 @@ class TestCliVariables:
         variable_command.variables_export(
             self.parser.parse_args(["variables", "export", 
"variables_types.json"])
         )
-        variable_command.variables_import(
-            self.parser.parse_args(["variables", "import", 
"variables_types.json"])
-        )
+        with create_session() as session:
+            variable_command.variables_import(
+                self.parser.parse_args(["variables", "import", 
"variables_types.json"]), session=session
+            )
 
         # Assert value
         assert Variable.get("dict", deserialize_json=True) == {"foo": "oops"}
@@ -157,10 +239,30 @@ class TestCliVariables:
         with pytest.raises(KeyError):
             Variable.get("foo")
 
-    def test_variables_import(self):
-        """Test variables_import command"""
+    @pytest.mark.parametrize(
+        "filename",
+        [
+            os.devnull,  # No extension (special file)
+            "variables.txt",  # Unsupported .txt extension
+            "variables",  # No extension
+            "variables.xml",  # Unsupported .xml extension
+        ],
+    )
+    def test_variables_import_unsupported_format(self, tmp_path, filename):
+        """Test variables_import command with unsupported file formats"""
+        # Use devnull directly or create a file with unsupported extension
+        if filename == os.devnull:
+            file_path = filename
+        else:
+            file_path = tmp_path / filename
+            file_path.write_text("some content")
+            file_path = os.fspath(file_path)
+
         with pytest.raises(SystemExit, match=r"Unsupported file format"):
-            
variable_command.variables_import(self.parser.parse_args(["variables", 
"import", os.devnull]))
+            with create_session() as session:
+                variable_command.variables_import(
+                    self.parser.parse_args(["variables", "import", 
file_path]), session=session
+                )
 
     def test_variables_export(self):
         """Test variables_export command"""
@@ -179,7 +281,10 @@ class TestCliVariables:
         variable_command.variables_set(self.parser.parse_args(["variables", 
"set", "bar", "updated"]))
         variable_command.variables_set(self.parser.parse_args(["variables", 
"set", "foo", '{"foo":"oops"}']))
         variable_command.variables_delete(self.parser.parse_args(["variables", 
"delete", "foo"]))
-        variable_command.variables_import(self.parser.parse_args(["variables", 
"import", os.fspath(path1)]))
+        with create_session() as session:
+            variable_command.variables_import(
+                self.parser.parse_args(["variables", "import", 
os.fspath(path1)]), session=session
+            )
 
         assert Variable.get("bar") == "original"
         assert Variable.get("foo") == '{\n  "foo": "bar"\n}'
@@ -217,9 +322,11 @@ class TestCliVariables:
                 "foo2": "bar2",
             }
 
-        variable_command.variables_import(
-            self.parser.parse_args(["variables", "import", 
os.fspath(variables_types_file)])
-        )
+        with create_session() as session:
+            variable_command.variables_import(
+                self.parser.parse_args(["variables", "import", 
os.fspath(variables_types_file)]),
+                session=session,
+            )
 
         assert Variable.get("foo") == "bar"
         assert Variable.get("foo1") == "bar1"
@@ -231,3 +338,207 @@ class TestCliVariables:
                 == "Foo var description"
             )
             assert 
session.scalar(select(Variable.description).where(Variable.key == "foo1")) == 
"12"
+
+    @pytest.mark.parametrize("format", ["json", "yaml", "yml"])
+    def test_variables_import_formats(self, create_variable_file, 
simple_variable_data, format):
+        """Test variables_import with different formats (JSON, YAML, YML)"""
+        file = create_variable_file(simple_variable_data, format=format)
+
+        with create_session() as session:
+            variable_command.variables_import(
+                self.parser.parse_args(["variables", "import", 
os.fspath(file)]), session=session
+            )
+
+        assert Variable.get("key1") == "value1"
+        assert Variable.get("key2", deserialize_json=True) == {"nested": 
"dict", "with": ["list", "values"]}
+        assert Variable.get("key3", deserialize_json=True) == 123
+        assert Variable.get("key4", deserialize_json=True) is True
+        assert Variable.get("key5", deserialize_json=True) is None
+
+    @pytest.mark.parametrize("format", ["json", "yaml"])
+    def test_variables_import_with_descriptions(
+        self, create_variable_file, variable_data_with_descriptions, format
+    ):
+        """Test variables_import with descriptions in different formats (JSON, 
YAML)"""
+        file = create_variable_file(variable_data_with_descriptions, 
format=format)
+
+        with create_session() as session:
+            variable_command.variables_import(
+                self.parser.parse_args(["variables", "import", 
os.fspath(file)]), session=session
+            )
+
+        assert Variable.get("var1") == "test_value"
+        assert Variable.get("var2", deserialize_json=True) == {"complex": 
"object"}
+        assert Variable.get("var3") == "simple_value"
+
+        with create_session() as session:
+            assert (
+                session.scalar(select(Variable.description).where(Variable.key 
== "var1"))
+                == "Test description for var1"
+            )
+            assert (
+                session.scalar(select(Variable.description).where(Variable.key 
== "var2"))
+                == "Complex variable"
+            )
+            assert 
session.scalar(select(Variable.description).where(Variable.key == "var3")) is 
None
+
+    def test_variables_import_env(self, create_variable_file, 
env_variable_data):
+        """Test variables_import with ENV format"""
+        env_file = create_variable_file(env_variable_data, format="env")
+
+        with create_session() as session:
+            variable_command.variables_import(
+                self.parser.parse_args(["variables", "import", 
os.fspath(env_file)]), session=session
+            )
+
+        assert Variable.get("KEY_A") == "value_a"
+        assert Variable.get("KEY_B") == "value with spaces"
+        assert Variable.get("KEY_C") == '{"json": "value", "number": 42}'
+        assert Variable.get("KEY_D") == "true"
+        assert Variable.get("KEY_E") == "123"
+
+    @pytest.mark.parametrize("format", ["json", "yaml", "yml"])
+    def test_variables_import_action_on_existing(self, create_variable_file, 
simple_variable_data, format):
+        """Test variables_import with action_on_existing_key parameter for 
different formats"""
+        file = create_variable_file(simple_variable_data, format=format)
+
+        # Set up one existing variable with different value
+        Variable.set("key1", "original_value")
+
+        # Test skip action - existing key1 should keep original value, others 
should be imported
+        with create_session() as session:
+            variable_command.variables_import(
+                self.parser.parse_args(
+                    ["variables", "import", os.fspath(file), 
"--action-on-existing-key", "skip"]
+                ),
+                session=session,
+            )
+        assert Variable.get("key1") == "original_value"  # Should NOT be 
overwritten
+        assert Variable.get("key2", deserialize_json=True) == {"nested": 
"dict", "with": ["list", "values"]}
+        assert Variable.get("key3", deserialize_json=True) == 123
+
+        # Clean up non-existing keys for next test
+        for key in ["key2", "key3", "key4", "key5"]:
+            Variable.delete(key)
+
+        # Test overwrite action (default) - existing key1 should be overwritten
+        with create_session() as session:
+            variable_command.variables_import(
+                self.parser.parse_args(
+                    ["variables", "import", os.fspath(file), 
"--action-on-existing-key", "overwrite"]
+                ),
+                session=session,
+            )
+        assert Variable.get("key1") == "value1"  # Should be overwritten with 
new value
+        assert Variable.get("key2", deserialize_json=True) == {"nested": 
"dict", "with": ["list", "values"]}
+
+        # Test fail action - should fail when key1 already exists
+        Variable.set("key1", "original_value")
+        with pytest.raises(SystemExit, match="already exists"):
+            with create_session() as session:
+                variable_command.variables_import(
+                    self.parser.parse_args(
+                        ["variables", "import", os.fspath(file), 
"--action-on-existing-key", "fail"]
+                    ),
+                    session=session,
+                )
+
+    def test_variables_import_env_action_on_existing(self, tmp_path):
+        """Test variables_import ENV with action_on_existing_key parameter"""
+        env_file = tmp_path / "variables_update.env"
+        env_content = """EXISTING_VAR=updated_value
+NEW_VAR=fresh_value"""
+        env_file.write_text(env_content)
+
+        # Set up existing variable
+        Variable.set("EXISTING_VAR", "initial_value")
+
+        # Test skip action
+        with create_session() as session:
+            variable_command.variables_import(
+                self.parser.parse_args(
+                    ["variables", "import", os.fspath(env_file), 
"--action-on-existing-key", "skip"]
+                ),
+                session=session,
+            )
+        assert Variable.get("EXISTING_VAR") == "initial_value"
+        assert Variable.get("NEW_VAR") == "fresh_value"
+
+        # Clean up for next test
+        Variable.delete("NEW_VAR")
+
+        # Test overwrite action
+        with create_session() as session:
+            variable_command.variables_import(
+                self.parser.parse_args(
+                    ["variables", "import", os.fspath(env_file), 
"--action-on-existing-key", "overwrite"]
+                ),
+                session=session,
+            )
+        assert Variable.get("EXISTING_VAR") == "updated_value"
+        assert Variable.get("NEW_VAR") == "fresh_value"
+
+    @pytest.mark.parametrize(
+        "format,invalid_content,error_pattern",
+        [
+            ("json", '{"invalid": "json", missing_quotes: true}', "Failed to 
load the secret file"),
+            ("yaml", "invalid:\n  - yaml\n  content: {missing", "Failed to 
load the secret file"),
+            ("yml", "invalid:\n  - yaml\n  content: {missing", "Failed to load 
the secret file"),
+            ("env", "INVALID_LINE_NO_EQUALS", "Invalid line format"),
+        ],
+    )
+    def test_variables_import_invalid_format(self, tmp_path, format, 
invalid_content, error_pattern):
+        """Test variables_import with invalid format files"""
+        invalid_file = tmp_path / f"invalid.{format}"
+        invalid_file.write_text(invalid_content)
+
+        with pytest.raises(SystemExit, match=error_pattern):
+            with create_session() as session:
+                variable_command.variables_import(
+                    self.parser.parse_args(["variables", "import", 
os.fspath(invalid_file)]),
+                    session=session,
+                )
+
+    def test_variables_import_cross_format_compatibility(self, 
create_variable_file, simple_variable_data):
+        """Test that the same variables can be imported from different formats 
consistently"""
+        # Create files in both formats using the same test data
+        json_file = create_variable_file(simple_variable_data, format="json")
+        yaml_file = create_variable_file(simple_variable_data, format="yaml")
+
+        # Test JSON import
+        with create_session() as session:
+            variable_command.variables_import(
+                self.parser.parse_args(["variables", "import", 
os.fspath(json_file)]), session=session
+            )
+
+        json_results = {}
+        for key in simple_variable_data:
+            if key in ["key1"]:  # String values don't need JSON 
deserialization
+                json_results[key] = Variable.get(key)
+            else:
+                json_results[key] = Variable.get(key, deserialize_json=True)
+
+        # Clear variables
+        for key in simple_variable_data:
+            Variable.delete(key)
+
+        # Test YAML import
+        with create_session() as session:
+            variable_command.variables_import(
+                self.parser.parse_args(["variables", "import", 
os.fspath(yaml_file)]), session=session
+            )
+
+        yaml_results = {}
+        for key in simple_variable_data:
+            if key in ["key1"]:  # String values don't need JSON 
deserialization
+                yaml_results[key] = Variable.get(key)
+            else:
+                yaml_results[key] = Variable.get(key, deserialize_json=True)
+
+        # Compare results - both formats should produce identical results
+        assert json_results == yaml_results
+        assert json_results["key1"] == "value1"
+        assert json_results["key2"] == {"nested": "dict", "with": ["list", 
"values"]}
+        assert json_results["key3"] == 123
+        assert json_results["key4"] is True
+        assert json_results["key5"] is None

Reply via email to