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 5dabdb8f586 Improve code coverage for the connection hook metadata 
service (#72248)
5dabdb8f586 is described below

commit 5dabdb8f586d9f022de5269a064be05f6fa7aafe
Author: Aaron Chen <[email protected]>
AuthorDate: Sun Aug 30 08:39:16 2026 +0800

    Improve code coverage for the connection hook metadata service (#72248)
    
    * Improve code coverage for the connection hook metadata service
    
    * Add tests for HookMetaService and enhance make_widget function
---
 .../core_api/services/ui/test_connections.py       | 382 +++++++++++++++++++++
 1 file changed, 382 insertions(+)

diff --git 
a/airflow-core/tests/unit/api_fastapi/core_api/services/ui/test_connections.py 
b/airflow-core/tests/unit/api_fastapi/core_api/services/ui/test_connections.py
index e6ed84778d3..3800a6c901f 100644
--- 
a/airflow-core/tests/unit/api_fastapi/core_api/services/ui/test_connections.py
+++ 
b/airflow-core/tests/unit/api_fastapi/core_api/services/ui/test_connections.py
@@ -14,9 +14,84 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+
 from __future__ import annotations
 
+import sys
+from unittest import mock
+
+import pytest
+
+from airflow.api_fastapi.core_api.datamodels.connections import (
+    ConnectionHookFieldBehavior,
+    ConnectionHookMetaData,
+    StandardHookFields,
+)
 from airflow.api_fastapi.core_api.services.ui.connections import 
HookMetaService
+from airflow.providers_manager import ConnectionFormWidgetInfo, 
ConnectionTypeHookUIMetadata, ProvidersManager
+
+CONNECTIONS_MODULE = "airflow.api_fastapi.core_api.services.ui.connections"
+
+MOCKED_FAB_MODULES = [
+    "wtforms",
+    "wtforms.csrf",
+    "wtforms.fields",
+    "wtforms.fields.simple",
+    "wtforms.validators",
+    "flask_babel",
+    "flask_appbuilder",
+    "flask_appbuilder.fieldwidgets",
+]
+
+
+class UnboundField:
+    """Stand-in for ``wtforms.fields.core.UnboundField``, which is matched by 
class name."""
+
+    def __init__(self, field_class, *args, **kwargs):
+        self.field_class = field_class
+        self.args = args
+        self.kwargs = kwargs
+
+
+class AnyOf:
+    """Stand-in for ``wtforms.validators.AnyOf``, which is matched by class 
name."""
+
+    def __init__(self, values):
+        self.values = values
+
+
+class StringField: ...
+
+
+class BooleanField: ...
+
+
+class IntegerField: ...
+
+
+class PasswordField: ...
+
+
+def find_spec_succeeds(name, package=None):
+    return object()
+
+
+def find_spec_returns_none(name, package=None):
+    return None
+
+
+def find_spec_raises(name, package=None):
+    raise ModuleNotFoundError(f"No module named {name!r}", name=name)
+
+
+def make_widget(field, field_name: str) -> ConnectionFormWidgetInfo:
+    return ConnectionFormWidgetInfo(
+        hook_class_name="SomeHook",
+        package_name="apache-airflow-providers-some",
+        field=field,
+        field_name=field_name,
+        is_sensitive=False,
+    )
 
 
 class TestMockOptional:
@@ -30,3 +105,310 @@ class TestMockOptional:
         validator = HookMetaService.MockOptional()
         result = validator(None, None)
         assert result is None
+
+
+class TestMockBaseField:
+    @pytest.mark.parametrize(
+        ("field_class", "expected_type", "expected_format"),
+        [
+            (HookMetaService.MockStringField, "string", None),
+            (HookMetaService.MockIntegerField, "integer", None),
+            (HookMetaService.MockPasswordField, "string", "password"),
+            (HookMetaService.MockBooleanField, "boolean", None),
+        ],
+    )
+    def test_schema_type_and_format_per_field_class(self, field_class, 
expected_type, expected_format):
+        schema = field_class(label="Some label").param.dump()["schema"]
+
+        assert schema["type"] == [expected_type, "null"]
+        assert schema.get("format") == expected_format
+
+    @pytest.mark.parametrize(
+        ("field_kwargs", "expected_dump"),
+        [
+            pytest.param(
+                {},
+                {
+                    "value": None,
+                    "schema": {"title": None, "type": ["string", "null"]},
+                    "description": None,
+                    "source": None,
+                },
+                id="omitted",
+            ),
+            pytest.param(
+                {
+                    "label": "Some label",
+                    "description": "Some description",
+                    "default": "abc",
+                    "source": "task",
+                },
+                {
+                    "value": "abc",
+                    "schema": {"title": "Some label", "type": ["string", 
"null"]},
+                    "description": "Some description",
+                    "source": "task",
+                },
+                id="provided",
+            ),
+        ],
+    )
+    def test_dumped_param_reflects_the_field_arguments(self, field_kwargs, 
expected_dump):
+        assert HookMetaService.MockStringField(**field_kwargs).param.dump() == 
expected_dump
+
+    @pytest.mark.parametrize(
+        "validators",
+        [
+            pytest.param([HookMetaService.MockEnum(["a", "b"])], 
id="enum-only"),
+            pytest.param(
+                [HookMetaService.MockOptional(), 
HookMetaService.MockEnum(["a", "b"])],
+                id="optional-and-enum",
+            ),
+        ],
+    )
+    def test_validators_are_reflected_in_the_schema(self, validators):
+        field = HookMetaService.MockStringField(label="Some label", 
validators=validators)
+
+        schema = field.param.dump()["schema"]
+
+        assert schema["type"] == ["string", "null"]
+        assert schema["enum"] == ["a", "b"]
+
+    def test_widget_and_field_class_are_recorded(self):
+        widget = HookMetaService.MockAnyWidget()
+        field = HookMetaService.MockStringField(widget=widget)
+
+        assert field.widget is widget
+        # ProvidersManager checks this attribute before accepting connection 
widgets.
+        assert field.field_class is HookMetaService.MockStringField
+
+
+class TestMakeStandardFields:
+    def test_returns_none_without_field_behaviour(self):
+        assert HookMetaService._make_standard_fields(None) is None
+
+    def test_builds_configured_field_behaviours(self):
+        result = HookMetaService._make_standard_fields(
+            {
+                "hidden_fields": ["description"],
+                "relabeling": {"host": "Server"},
+                "placeholders": {"port": "5432"},
+            }
+        )
+
+        assert result == StandardHookFields(
+            description=ConnectionHookFieldBehavior(hidden=True),
+            url_schema=None,
+            host=ConnectionHookFieldBehavior(title="Server"),
+            port=ConnectionHookFieldBehavior(placeholder="5432"),
+            login=None,
+            password=None,
+        )
+
+
+class TestConvertExtraFields:
+    def 
test_dict_fields_are_passed_through_and_grouped_by_connection_type(self):
+        mysql_foo = {"value": None, "schema": {"type": ["string", "null"]}, 
"description": None}
+        mysql_bar = {"value": "abc", "schema": {}, "description": None}
+        ftp_baz = {"value": None, "schema": {}, "description": "Some 
description"}
+
+        result = HookMetaService._convert_extra_fields(
+            {
+                "extra__mysql__foo": make_widget(mysql_foo, "foo"),
+                "extra__mysql__bar": make_widget(mysql_bar, "bar"),
+                "extra__ftp__baz": make_widget(ftp_baz, "baz"),
+            }
+        )
+
+        assert result == {"mysql": {"foo": mysql_foo, "bar": mysql_bar}, 
"ftp": {"baz": ftp_baz}}
+
+    def test_mocked_wtforms_field_is_dumped(self):
+        field = HookMetaService.MockStringField(label="Some label")
+
+        result = HookMetaService._convert_extra_fields({"extra__mysql__foo": 
make_widget(field, "foo")})
+
+        assert result == {"mysql": {"foo": field.param.dump()}}
+
+    @pytest.mark.parametrize(
+        ("field_class", "expected_type", "expected_format"),
+        [
+            (StringField, "string", None),
+            (BooleanField, "boolean", None),
+            (IntegerField, "integer", None),
+            (PasswordField, "string", "password"),
+        ],
+    )
+    def test_unbound_field_type_and_format_per_field_class(self, field_class, 
expected_type, expected_format):
+        field = UnboundField(field_class, "Some label")
+
+        result = HookMetaService._convert_extra_fields({"extra__mysql__foo": 
make_widget(field, "foo")})
+
+        schema = result["mysql"]["foo"]["schema"]
+        assert schema["type"] == [expected_type, "null"]
+        assert schema.get("format") == expected_format
+
+    @pytest.mark.parametrize(
+        ("field_args", "field_kwargs", "expected_title"),
+        [
+            pytest.param(("Some label",), {}, "Some label", id="positional"),
+            pytest.param((), {"label": "Some label"}, "Some label", 
id="keyword"),
+            pytest.param((), {}, None, id="missing"),
+        ],
+    )
+    def test_unbound_field_label_becomes_the_schema_title(self, field_args, 
field_kwargs, expected_title):
+        field = UnboundField(StringField, *field_args, **field_kwargs)
+
+        result = HookMetaService._convert_extra_fields({"extra__mysql__foo": 
make_widget(field, "foo")})
+
+        assert result["mysql"]["foo"]["schema"]["title"] == expected_title
+
+    def test_unbound_field_keeps_description_and_default(self):
+        field = UnboundField(StringField, "Some label", description="Some 
description", default="abc")
+
+        result = HookMetaService._convert_extra_fields({"extra__mysql__foo": 
make_widget(field, "foo")})
+
+        assert result["mysql"]["foo"]["description"] == "Some description"
+        assert result["mysql"]["foo"]["value"] == "abc"
+
+    def test_unbound_field_reads_the_enum_from_any_of_validators_only(self):
+        field = UnboundField(StringField, "Some label", validators=[object(), 
AnyOf(["a", "b"])])
+
+        result = HookMetaService._convert_extra_fields({"extra__mysql__foo": 
make_widget(field, "foo")})
+
+        assert result["mysql"]["foo"]["schema"]["enum"] == ["a", "b"]
+
+    def test_unknown_field_is_skipped_without_dropping_the_other_widgets(self):
+        result = HookMetaService._convert_extra_fields(
+            {
+                "extra__mysql__foo": make_widget({"schema": {}}, "foo"),
+                "extra__mysql__unknown": make_widget(object(), "unknown"),
+            }
+        )
+
+        assert result == {"mysql": {"foo": {"schema": {}}}}
+
+
+class TestGetHooksWithMockedFab:
+    """
+    Both branches are pinned to a patched ``find_spec`` rather than to what is 
installed.
+
+    ``airflow-core`` does not depend on FAB, so the real branch taken varies 
by environment, and the
+    production code deliberately leaves its ``sys.modules`` mocks in place — 
hence the ``patch.dict``.
+    """
+
+    @staticmethod
+    def call_without_fab_modules(find_spec, build_providers_manager):
+        with (
+            mock.patch.dict(sys.modules),
+            mock.patch("importlib.util.find_spec", autospec=True, 
side_effect=find_spec),
+            mock.patch(
+                f"{CONNECTIONS_MODULE}.ProvidersManager",
+                autospec=True,
+                side_effect=build_providers_manager,
+            ),
+        ):
+            for module_name in MOCKED_FAB_MODULES:
+                sys.modules.pop(module_name, None)
+
+            result = HookMetaService._get_hooks_with_mocked_fab()
+            mocked_modules = [
+                isinstance(sys.modules.get(name), mock.MagicMock) for name in 
MOCKED_FAB_MODULES
+            ]
+
+        return result, mocked_modules
+
+    def test_providers_manager_is_used_directly_when_fab_is_importable(self):
+        providers_manager = mock.MagicMock(spec=ProvidersManager)
+
+        result, mocked_modules = 
self.call_without_fab_modules(find_spec_succeeds, lambda: providers_manager)
+
+        assert not any(mocked_modules)
+        assert result == (
+            providers_manager.hooks,
+            providers_manager.connection_form_widgets,
+            providers_manager.field_behaviours,
+        )
+
+    @pytest.mark.parametrize(
+        "find_spec",
+        [
+            pytest.param(find_spec_raises, id="import-raises"),
+            pytest.param(find_spec_returns_none, id="spec-not-found"),
+        ],
+    )
+    def test_missing_fab_dependencies_are_replaced_by_mock_modules(self, 
find_spec):
+        providers_manager = mock.MagicMock(spec=ProvidersManager)
+
+        result, mocked_modules = self.call_without_fab_modules(find_spec, 
lambda: providers_manager)
+
+        assert all(mocked_modules)
+        assert result == (
+            providers_manager.hooks,
+            providers_manager.connection_form_widgets,
+            providers_manager.field_behaviours,
+        )
+
+    def test_patched_helpers_return_the_mock_substitutes(self):
+        """
+        The substitutes are reached through ``wtforms.validators``, not 
``sys.modules``.
+
+        ``mock.patch`` resolves ``wtforms.validators.any_of`` by attribute 
lookup on the ``wtforms``
+        mock, which is a different object from the 
``sys.modules["wtforms.validators"]`` mock that
+        ``from wtforms.validators import any_of`` would return.
+        """
+        captured = {}
+
+        def read_patched_helpers():
+            import flask_babel
+            import wtforms
+
+            captured["label"] = flask_babel.lazy_gettext("Some label")
+            captured["validator"] = wtforms.validators.any_of(["a", "b"])
+            return mock.MagicMock(spec=ProvidersManager)
+
+        self.call_without_fab_modules(find_spec_raises, read_patched_helpers)
+
+        assert captured["label"] == "Some label"
+        assert isinstance(captured["validator"], HookMetaService.MockEnum)
+        assert captured["validator"].allowed_values == ["a", "b"]
+
+
+class TestHookMetaData:
+    @pytest.fixture(autouse=True)
+    def clear_cache(self):
+        HookMetaService.hook_meta_data.cache_clear()
+        yield
+        HookMetaService.hook_meta_data.cache_clear()
+
+    @mock.patch(f"{CONNECTIONS_MODULE}.ProvidersManager", autospec=True)
+    def test_builds_metadata(self, providers_manager):
+        provider_manager = providers_manager.return_value
+        provider_manager._connection_form_widgets_from_metadata = {
+            "extra__some__token": make_widget({"schema": {}}, "token")
+        }
+        provider_manager.iter_connection_type_hook_ui_metadata.return_value = [
+            ConnectionTypeHookUIMetadata(
+                connection_type="some",
+                hook_name="Some hook",
+                hook_class_name="some.hooks.SomeHook",
+                field_behaviour={"hidden_fields": ["password"]},
+            )
+        ]
+
+        assert HookMetaService.hook_meta_data() == [
+            ConnectionHookMetaData(
+                connection_type="some",
+                hook_class_name="some.hooks.SomeHook",
+                default_conn_name=None,
+                hook_name="Some hook",
+                standard_fields=StandardHookFields(
+                    description=None,
+                    url_schema=None,
+                    host=None,
+                    port=None,
+                    login=None,
+                    password=ConnectionHookFieldBehavior(hidden=True),
+                ),
+                extra_fields={"token": {"schema": {}}},
+            )
+        ]

Reply via email to