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

shahar1 pushed a commit to branch airflow-ctl/v0-1-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/airflow-ctl/v0-1-test by this 
push:
     new b7bee2348a6 [airflow-ctl/v0-1-test] Validate the file structure in 
airflowctl connections import (#75176) (#76088)
b7bee2348a6 is described below

commit b7bee2348a6a2a4b8ed37d5cbecfdd1dde9f5ace
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Oct 30 20:55:07 2026 +0000

    [airflow-ctl/v0-1-test] Validate the file structure in airflowctl 
connections import (#75176) (#76088)
    
    `airflowctl connections import` assumed the file was a JSON object of
    objects. A file holding a list, a scalar, or an entry that is not an object
    failed with internal Python messages such as
    "'list' object has no attribute 'items'" or
    "'str' object has no attribute 'get'", which do not tell the user what is
    wrong with the file.
    
    Check the structure right after parsing, before building any request, and
    exit with a message that names the file, the offending connection id and the
    expected shape. `variables import` and `pools import` already validate their
    input the same way. No request is sent for a malformed file.
    (cherry picked from commit 1bcc8e6bdbc11f151a38a064a6950156b7a5e78a)
    
    Co-authored-by: Rishabh <[email protected]>
---
 .../airflowctl/ctl/commands/connection_command.py  | 11 ++++++++
 .../ctl/commands/test_connections_command.py       | 30 ++++++++++++++++++++++
 2 files changed, 41 insertions(+)

diff --git a/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py 
b/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py
index 02958740a36..4c4067ffcb7 100644
--- a/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py
+++ b/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py
@@ -45,6 +45,17 @@ def import_(args, api_client=NEW_API_CLIENT) -> None:
             connections_json = json.loads(file.read())
         except Exception as e:
             raise SystemExit(f"Error reading connections file {args.file}: 
{e}")
+    if not isinstance(connections_json, dict):
+        raise SystemExit(
+            f"Invalid connections file {args.file}: expected a JSON object 
mapping connection ids "
+            f"to connection details, got {type(connections_json).__name__}"
+        )
+    for connection_id, connection_details in connections_json.items():
+        if not isinstance(connection_details, dict):
+            raise SystemExit(
+                f"Invalid connection {connection_id!r} in {args.file}: 
expected a JSON object "
+                f"of connection details, got 
{type(connection_details).__name__}"
+            )
     try:
         connections_data = {
             k: ConnectionBody(
diff --git 
a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_connections_command.py 
b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_connections_command.py
index ba803ddd8e0..2f2b9274e17 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_connections_command.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_connections_command.py
@@ -215,6 +215,36 @@ class TestCliConnectionCommands:
             api_client=api_client,
         )
 
+    @pytest.mark.parametrize(
+        ("file_content", "expected_message"),
+        [
+            ([{"conn_type": "postgres"}], "expected a JSON object mapping 
connection ids"),
+            ("not-an-object", "expected a JSON object mapping connection ids"),
+            ({"my_conn": "postgres://host"}, "Invalid connection 'my_conn'"),
+            ({"my_conn": None}, "Invalid connection 'my_conn'"),
+        ],
+        ids=["list", "string", "string-entry", "null-entry"],
+    )
+    def test_import_rejects_malformed_file_structure(
+        self, tmp_path, monkeypatch, file_content, expected_message
+    ):
+        monkeypatch.chdir(tmp_path)
+        json_path = tmp_path / self.export_file_name
+        json_path.write_text(json.dumps(file_content))
+        mock_client = mock.MagicMock(spec=Client)
+
+        with pytest.raises(SystemExit) as exc_info:
+            connection_command.import_(
+                self.parser.parse_args(["connections", "import", 
json_path.as_posix()]),
+                api_client=mock_client,
+            )
+
+        message = str(exc_info.value)
+        assert expected_message in message
+        assert json_path.as_posix() in message
+        assert "has no attribute" not in message
+        mock_client.connections.bulk.assert_not_called()
+
     @pytest.mark.parametrize(
         ("action_on_existing_key", "expected_enum"),
         [

Reply via email to