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

bugraoz 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 395cb1e759e Add integration tests for config sensitive masking in 
airflowctl (#61105)
395cb1e759e is described below

commit 395cb1e759e8d93c2bea697d0e0b472660853467
Author: Subham <[email protected]>
AuthorDate: Wed Feb 11 13:13:31 2026 +0530

    Add integration tests for config sensitive masking in airflowctl (#61105)
    
    * Add integration tests for config sensitive masking in airflowctl
    
    * Fix static check failures: Sort imports in 
test_config_sensitive_masking.py
    
    * Refactor integration tests to reduce duplication by moving shared logic 
to conftest.py
    
    * Fix static checks in conftest.py
    
    * Address PR review feedback: Remove flaky decorator and fix CI test 
discovery
    
    - Remove @pytest.mark.flaky decorator from test_config_sensitive_masking.py 
as suggested
    - Fix CI test discovery by changing hardcoded path in run_tests.py from
      'test_airflowctl_commands.py' to 'airflowctl_tests' directory to allow
      pytest to discover all test files in the directory
---
 .../tests/airflowctl_tests/conftest.py             | 75 ++++++++++++++++++++++
 .../tests/airflowctl_tests/constants.py            |  3 +
 .../airflowctl_tests/test_airflowctl_commands.py   | 67 +------------------
 .../test_config_sensitive_masking.py               | 51 +++++++++++++++
 dev/breeze/src/airflow_breeze/utils/run_tests.py   |  2 +-
 5 files changed, 133 insertions(+), 65 deletions(-)

diff --git a/airflow-ctl-tests/tests/airflowctl_tests/conftest.py 
b/airflow-ctl-tests/tests/airflowctl_tests/conftest.py
index 3ac692722f2..9e6ef75b7fa 100644
--- a/airflow-ctl-tests/tests/airflowctl_tests/conftest.py
+++ b/airflow-ctl-tests/tests/airflowctl_tests/conftest.py
@@ -20,6 +20,7 @@ import os
 import subprocess
 import sys
 
+import pytest
 from python_on_whales import DockerClient, docker
 
 from airflowctl_tests import console
@@ -27,11 +28,85 @@ from airflowctl_tests.constants import (
     AIRFLOW_ROOT_PATH,
     DOCKER_COMPOSE_FILE_PATH,
     DOCKER_IMAGE,
+    LOGIN_COMMAND,
+    LOGIN_OUTPUT,
 )
 
 from tests_common.test_utils.fernet import generate_fernet_key_string
 
 
[email protected]
+def run_command():
+    """Fixture that provides a helper to run airflowctl commands."""
+
+    def _run_command(command: str, skip_login: bool = False) -> str:
+        import os
+        from subprocess import PIPE, STDOUT, Popen
+
+        host_envs = os.environ.copy()
+        host_envs["AIRFLOW_CLI_DEBUG_MODE"] = "true"
+
+        command_from_config = f"airflowctl {command}"
+
+        # We need to run auth login first for all commands except login itself 
(unless skipped)
+        if not skip_login and command != LOGIN_COMMAND:
+            run_cmd = f"airflowctl {LOGIN_COMMAND} && {command_from_config}"
+        else:
+            run_cmd = command_from_config
+
+        console.print(f"[yellow]Running command: {command}")
+
+        # Give some time for the command to execute and output to be ready
+        proc = Popen(run_cmd.encode(), stdout=PIPE, stderr=STDOUT, stdin=PIPE, 
shell=True, env=host_envs)
+        stdout_bytes, stderr_result = proc.communicate(timeout=60)
+
+        # CLI command gave errors
+        assert not stderr_result, (
+            f"Errors while executing command 
'{command_from_config}':\n{stderr_result.decode()}"
+        )
+
+        # Decode the output
+        stdout_result = stdout_bytes.decode()
+
+        # We need to trim auth login output if the command is not login itself 
and clean backspaces
+        if not skip_login and command != LOGIN_COMMAND:
+            assert LOGIN_OUTPUT in stdout_result, (
+                f"❌ Login output not found before command output for 
'{command_from_config}'\nFull output:\n{stdout_result}"
+            )
+            stdout_result = stdout_result.split(f"{LOGIN_OUTPUT}\n")[1].strip()
+        else:
+            stdout_result = stdout_result.strip()
+
+        # Check for non-zero exit code
+        assert proc.returncode == 0, (
+            f"❌ Command '{command_from_config}' exited with code 
{proc.returncode}\nOutput:\n{stdout_result}"
+        )
+
+        # Error patterns to detect failures that might otherwise slip through
+        # Please ensure it is aligning with 
airflowctl.api.client.get_json_error
+        error_patterns = [
+            "Server error",
+            "command error",
+            "unrecognized arguments",
+            "invalid choice",
+            "Traceback (most recent call last):",
+        ]
+        matched_error = next((error for error in error_patterns if error in 
stdout_result), None)
+        assert not matched_error, (
+            f"❌ Output contained unexpected text for command 
'{command_from_config}'\n"
+            f"Matched error pattern: {matched_error}\n"
+            f"Output:\n{stdout_result}"
+        )
+
+        console.print(f"[green]✅ Output did not contain unexpected text for 
command '{command_from_config}'")
+        console.print(f"[cyan]Result:\n{stdout_result}\n")
+        proc.kill()
+
+        return stdout_result
+
+    return _run_command
+
+
 class _CtlTestState:
     docker_client: DockerClient | None = None
 
diff --git a/airflow-ctl-tests/tests/airflowctl_tests/constants.py 
b/airflow-ctl-tests/tests/airflowctl_tests/constants.py
index 549a9ad8c74..4ddf2636c7f 100644
--- a/airflow-ctl-tests/tests/airflowctl_tests/constants.py
+++ b/airflow-ctl-tests/tests/airflowctl_tests/constants.py
@@ -30,3 +30,6 @@ DOCKER_COMPOSE_HOST_PORT = os.environ.get("HOST_PORT", 
"localhost:8080")
 DOCKER_COMPOSE_FILE_PATH = (
     AIRFLOW_ROOT_PATH / "airflow-core" / "docs" / "howto" / "docker-compose" / 
"docker-compose.yaml"
 )
+
+LOGIN_COMMAND = "auth login --username airflow --password airflow"
+LOGIN_OUTPUT = "Login successful! Welcome to airflowctl!"
diff --git 
a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py 
b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
index ceb29f0143d..228321ac7e8 100644
--- a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
+++ b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
@@ -16,12 +16,9 @@
 # under the License.
 from __future__ import annotations
 
-import os
-from subprocess import PIPE, STDOUT, Popen
-
 import pytest
 
-from airflowctl_tests import console
+from airflowctl_tests.constants import LOGIN_COMMAND
 
 
 def date_param():
@@ -47,8 +44,6 @@ def date_param():
     return random_dt.isoformat()
 
 
-LOGIN_COMMAND = "auth login --username airflow --password airflow"
-LOGIN_OUTPUT = "Login successful! Welcome to airflowctl!"
 ONE_DATE_PARAM = date_param()
 TEST_COMMANDS = [
     # Passing password via command line is insecure but acceptable for testing 
purposes
@@ -134,62 +129,6 @@ TEST_COMMANDS = [
 @pytest.mark.parametrize(
     "command", TEST_COMMANDS, ids=[" ".join(command.split(" ", 2)[:2]) for 
command in TEST_COMMANDS]
 )
-def test_airflowctl_commands(command: str):
+def test_airflowctl_commands(command: str, run_command):
     """Test airflowctl commands using docker-compose environment."""
-    host_envs = os.environ.copy()
-    host_envs["AIRFLOW_CLI_DEBUG_MODE"] = "true"
-
-    command_from_config = f"airflowctl {command}"
-    # We need to run auth login first for all commands except login itself
-    if command != LOGIN_COMMAND:
-        run_command = f"airflowctl {LOGIN_COMMAND} && {command_from_config}"
-    else:
-        run_command = command_from_config
-    console.print(f"[yellow]Running command: {command}")
-
-    # Give some time for the command to execute and output to be ready
-    proc = Popen(run_command.encode(), stdout=PIPE, stderr=STDOUT, stdin=PIPE, 
shell=True, env=host_envs)
-    stdout_bytes, stderr_result = proc.communicate(timeout=60)
-
-    # CLI command gave errors
-    assert not stderr_result, (
-        f"Errors while executing command 
'{command_from_config}':\n{stderr_result.decode()}"
-    )
-
-    # Decode the output
-    stdout_result = stdout_bytes.decode()
-    # We need to trim auth login output if the command is not login itself and 
clean backspaces
-    if command != LOGIN_COMMAND:
-        assert LOGIN_OUTPUT in stdout_result, (
-            f"❌ Login output not found before command output for 
'{command_from_config}'",
-            f"\nFull output:\n{stdout_result}",
-        )
-        stdout_result = stdout_result.split(f"{LOGIN_OUTPUT}\n")[1].strip()
-    else:
-        stdout_result = stdout_result.strip()
-
-    # Check for non-zero exit code
-    assert proc.returncode == 0, (
-        f"❌ Command '{command_from_config}' exited with code 
{proc.returncode}",
-        f"\nOutput:\n{stdout_result}",
-    )
-
-    # Error patterns to detect failures that might otherwise slip through
-    # Please ensure it is aligning with airflowctl.api.client.get_json_error
-    error_patterns = [
-        "Server error",
-        "command error",
-        "unrecognized arguments",
-        "invalid choice",
-        "Traceback (most recent call last):",
-    ]
-    matched_error = next((error for error in error_patterns if error in 
stdout_result), None)
-    assert not matched_error, (
-        f"❌ Output contained unexpected text for command 
'{command_from_config}'",
-        f"\nMatched error pattern: {matched_error}",
-        f"\nOutput:\n{stdout_result}",
-    )
-
-    console.print(f"[green]✅ Output did not contain unexpected text for 
command '{command_from_config}'")
-    console.print(f"[cyan]Result:\n{stdout_result}\n")
-    proc.kill()
+    run_command(command)
diff --git 
a/airflow-ctl-tests/tests/airflowctl_tests/test_config_sensitive_masking.py 
b/airflow-ctl-tests/tests/airflowctl_tests/test_config_sensitive_masking.py
new file mode 100644
index 00000000000..84719bb4cc3
--- /dev/null
+++ b/airflow-ctl-tests/tests/airflowctl_tests/test_config_sensitive_masking.py
@@ -0,0 +1,51 @@
+# 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 __future__ import annotations
+
+import pytest
+
+# Test commands for config sensitive masking verification
+SENSITIVE_CONFIG_COMMANDS = [
+    # Test that config list shows masked sensitive values
+    "config list",
+    # Test that getting specific sensitive config values are masked
+    "config get --section core --option fernet_key",
+    "config get --section database --option sql_alchemy_conn",
+]
+
+
[email protected](
+    "command",
+    SENSITIVE_CONFIG_COMMANDS,
+    ids=[" ".join(command.split(" ", 2)[:2]) for command in 
SENSITIVE_CONFIG_COMMANDS],
+)
+def test_config_sensitive_masking(command: str, run_command):
+    """
+    Test that sensitive config values are properly masked by airflowctl.
+
+    This integration test verifies that when airflowctl retrieves config data 
from the
+    Airflow API, sensitive values (like fernet_key, sql_alchemy_conn) appear 
masked
+    as '< hidden >' and do not leak actual secret values.
+    """
+    stdout_result = run_command(command)
+
+    # CRITICAL: Verify that sensitive values are masked
+    # The Airflow API returns masked values as "< hidden >" for sensitive 
configs
+    assert "< hidden >" in stdout_result, (
+        f"❌ Expected masked value '< hidden >' not found in output for 
'airflowctl {command}'\n"
+        f"Output:\n{stdout_result}"
+    )
diff --git a/dev/breeze/src/airflow_breeze/utils/run_tests.py 
b/dev/breeze/src/airflow_breeze/utils/run_tests.py
index 9dff5d3935b..211384eeafe 100644
--- a/dev/breeze/src/airflow_breeze/utils/run_tests.py
+++ b/dev/breeze/src/airflow_breeze/utils/run_tests.py
@@ -152,7 +152,7 @@ def run_docker_compose_tests(
         test_path = Path("tests") / "airflow_e2e_tests" / f"{test_mode}_tests"
         cwd = AIRFLOW_E2E_TESTS_ROOT_PATH.as_posix()
     elif test_type == "airflow-ctl-integration":
-        test_path = Path("tests") / "airflowctl_tests" / 
"test_airflowctl_commands.py"
+        test_path = Path("tests") / "airflowctl_tests"
         cwd = AIRFLOW_CTL_TESTS_ROOT_PATH.as_posix()
     else:
         test_path = Path("tests") / "docker_tests" / 
"test_docker_compose_quick_start.py"

Reply via email to