diff --git a/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py b/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py index 8a894202206d8..b689083faa28e 100644 --- a/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py +++ b/airflow-ctl/src/airflowctl/ctl/commands/connection_command.py @@ -54,7 +54,7 @@ def import_(args, api_client=NEW_API_CLIENT) -> None: login=v.get("login"), password=v.get("password"), port=v.get("port"), - extra=v.get("extra", {}), + extra=v.get("extra"), description=v.get("description", ""), ) for k, v in connections_json.items() 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 a63a4b4fa6aa6..02b56eda99b0e 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 @@ -17,6 +17,7 @@ from __future__ import annotations import json +from unittest.mock import patch import pytest @@ -24,6 +25,7 @@ from airflowctl.api.datamodels.generated import ( BulkActionResponse, BulkResponse, + ConnectionBody, ConnectionCollectionResponse, ConnectionResponse, ) @@ -125,3 +127,52 @@ def test_import_error(self, api_client_maker, tmp_path, monkeypatch): self.parser.parse_args(["connections", "import", expected_json_path.as_posix()]), api_client=api_client, ) + + def test_import_without_extra_field(self, api_client_maker, tmp_path, monkeypatch): + """Import succeeds when JSON omits the ``extra`` field (#62653). + + Before the fix, ``v.get("extra", {})`` returned ``{}`` (a dict) when + the key was absent, but ``ConnectionBody.extra`` expects ``str | None``, + causing a Pydantic ``ValidationError``. + """ + api_client = api_client_maker( + path="/api/v2/connections", + response_json=self.bulk_response_success.model_dump(), + expected_http_status_code=200, + kind=ClientKind.CLI, + ) + + monkeypatch.chdir(tmp_path) + json_path = tmp_path / self.export_file_name + # Intentionally omit "extra" (and several other optional keys) to + # mirror a minimal real-world connection JSON export. + connection_file = { + self.connection_id: { + "conn_type": "test_type", + "host": "test_host", + } + } + + json_path.write_text(json.dumps(connection_file)) + + with patch( + "airflowctl.ctl.commands.connection_command.ConnectionBody", + wraps=ConnectionBody, + ) as mock_body: + connection_command.import_( + self.parser.parse_args(["connections", "import", json_path.as_posix()]), + api_client=api_client, + ) + + # Verify that ``extra`` was passed as None (not {} which would fail + # Pydantic validation) and all other absent keys default correctly. + mock_body.assert_called_once_with( + connection_id=self.connection_id, + conn_type="test_type", + host="test_host", + login=None, + password=None, + port=None, + extra=None, + description="", + )