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

vincbeck 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 f1cdbcb9712  Fix team-scoped auth check for POST variables, 
connections, and pools in multi-team mode (#62511)
f1cdbcb9712 is described below

commit f1cdbcb9712a0930b84bd675172a6a50aa0252ca
Author: Mathieu Monet <[email protected]>
AuthorDate: Mon Mar 2 16:21:10 2026 +0100

     Fix team-scoped auth check for POST variables, connections, and pools in 
multi-team mode (#62511)
    
    * Fix team_name resolution in requires_access_{variable,connection,pool}
    
    Previously, these three security dependencies fetched `team_name`
    exclusively from the resource's own DB row via `Model.get_team_name()`.
    This had two problems:
    
    1. For POST requests (creating a new resource), the resource does not
       exist yet, so the DB lookup always returned None and the team-scoped
       auth check was silently skipped.
    
    2. The returned string was never validated against the `team` table, so
       a stale or otherwise invalid team_name (e.g. after a team deletion
       under SQLite without FK enforcement) would be forwarded as-is to the
       auth manager.
    
    Fix:
    - Fall back to `request.json().get("team_name")` when the DB lookup
      returns None (covers POST). Pattern follows `requires_access_backfill`.
    - Validate the resolved name via the new `Team.get_name_if_exists()`
      classmethod, which returns None for names not present in the `team`
      table, ensuring only real teams reach the auth manager.
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
    
    * Add and fix unit tests for team_name resolution in requires_access_* 
dependencies
    
    The three inner functions were made async in the previous commit, so the
    existing tests needed to be updated: made async, awaited the inner call,
    added AsyncMock for request.json, and mocked Team.get_name_if_exists.
    
    Also adds three new tests covering the POST body-fallback path: when the
    resource doesn't exist yet (no DB row), team_name is read from the request
    body so that valid POST requests are not rejected in multi-team mode.
    
    Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
    
    * DRY team-scoped auth checks and gate on multi_team config
    
    Extract _collect_teams_to_check helper to deduplicate team collection
    and validation logic across requires_access_{variable,connection,pool}.
    Gate team checks on conf.getboolean("core", "multi_team") so mono-tenant
    setups skip team lookups entirely. Validate team existence on POST/PUT
    and return 400 for nonexistent teams.
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * Add unit tests for Team.get_name_if_exists
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
    
    * Remove AIRFLOW_V_3_2_PLUS checks from team-related tests
    
    * Refactor access checks to use named  and typed callbacks for team 
authorization in security.py
---
 .../src/airflow/api_fastapi/core_api/security.py   |  85 +++++--
 airflow-core/src/airflow/models/team.py            |   6 +
 .../unit/api_fastapi/core_api/test_security.py     | 272 ++++++++++++++++++++-
 airflow-core/tests/unit/models/test_team.py        |   8 +
 4 files changed, 336 insertions(+), 35 deletions(-)

diff --git a/airflow-core/src/airflow/api_fastapi/core_api/security.py 
b/airflow-core/src/airflow/api_fastapi/core_api/security.py
index 6acbb82494d..188ee168693 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/security.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/security.py
@@ -345,19 +345,20 @@ def permitted_pool_filter_factory(
 ReadablePoolsFilterDep = Annotated[PermittedPoolFilter, 
Depends(permitted_pool_filter_factory("GET"))]
 
 
-def requires_access_pool(method: ResourceMethod) -> Callable[[Request, 
BaseUser], None]:
-    def inner(
+def requires_access_pool(method: ResourceMethod) -> Callable[[Request, 
BaseUser], Coroutine[Any, Any, None]]:
+    async def inner(
         request: Request,
         user: GetUserDep,
     ) -> None:
         pool_name = request.path_params.get("pool_name")
-        team_name = Pool.get_team_name(pool_name) if pool_name else None
+        for team_name in await _collect_teams_to_check(method, request, 
pool_name, Pool.get_team_name):
 
-        _requires_access(
-            is_authorized_callback=lambda: 
get_auth_manager().is_authorized_pool(
-                method=method, details=PoolDetails(name=pool_name, 
team_name=team_name), user=user
-            )
-        )
+            def _callback(tn: str | None = team_name) -> bool:
+                return get_auth_manager().is_authorized_pool(
+                    method=method, details=PoolDetails(name=pool_name, 
team_name=tn), user=user
+                )
+
+            _requires_access(is_authorized_callback=_callback)
 
     return inner
 
@@ -439,21 +440,26 @@ ReadableConnectionsFilterDep = Annotated[
 ]
 
 
-def requires_access_connection(method: ResourceMethod) -> Callable[[Request, 
BaseUser], None]:
-    def inner(
+def requires_access_connection(
+    method: ResourceMethod,
+) -> Callable[[Request, BaseUser], Coroutine[Any, Any, None]]:
+    async def inner(
         request: Request,
         user: GetUserDep,
     ) -> None:
         connection_id = request.path_params.get("connection_id")
-        team_name = Connection.get_team_name(connection_id) if connection_id 
else None
+        for team_name in await _collect_teams_to_check(
+            method, request, connection_id, Connection.get_team_name
+        ):
+
+            def _callback(tn: str | None = team_name) -> bool:
+                return get_auth_manager().is_authorized_connection(
+                    method=method,
+                    details=ConnectionDetails(conn_id=connection_id, 
team_name=tn),
+                    user=user,
+                )
 
-        _requires_access(
-            is_authorized_callback=lambda: 
get_auth_manager().is_authorized_connection(
-                method=method,
-                details=ConnectionDetails(conn_id=connection_id, 
team_name=team_name),
-                user=user,
-            )
-        )
+            _requires_access(is_authorized_callback=_callback)
 
     return inner
 
@@ -580,19 +586,22 @@ ReadableVariablesFilterDep = Annotated[
 ]
 
 
-def requires_access_variable(method: ResourceMethod) -> Callable[[Request, 
BaseUser], None]:
-    def inner(
+def requires_access_variable(
+    method: ResourceMethod,
+) -> Callable[[Request, BaseUser], Coroutine[Any, Any, None]]:
+    async def inner(
         request: Request,
         user: GetUserDep,
     ) -> None:
         variable_key: str | None = request.path_params.get("variable_key")
-        team_name = Variable.get_team_name(variable_key) if variable_key else 
None
+        for team_name in await _collect_teams_to_check(method, request, 
variable_key, Variable.get_team_name):
 
-        _requires_access(
-            is_authorized_callback=lambda: 
get_auth_manager().is_authorized_variable(
-                method=method, details=VariableDetails(key=variable_key, 
team_name=team_name), user=user
-            ),
-        )
+            def _callback(tn: str | None = team_name) -> bool:
+                return get_auth_manager().is_authorized_variable(
+                    method=method, details=VariableDetails(key=variable_key, 
team_name=tn), user=user
+                )
+
+            _requires_access(is_authorized_callback=_callback)
 
     return inner
 
@@ -703,6 +712,30 @@ def requires_authenticated() -> Callable:
     return inner
 
 
+async def _collect_teams_to_check(
+    method: ResourceMethod,
+    request: Request,
+    resource_id: str | None,
+    get_existing_team: Callable[[str], str | None],
+) -> set[str | None]:
+    """Collect validated team names from existing resource (DB) and/or request 
body."""
+    if not conf.getboolean("core", "multi_team"):
+        return {None}
+    teams: set[str | None] = set()
+    if method != "POST":
+        teams.add(get_existing_team(resource_id) if resource_id else None)
+    if method in ("POST", "PUT"):
+        with suppress(JSONDecodeError):
+            raw = (await request.json()).get("team_name")
+            if raw and not Team.get_name_if_exists(raw):
+                raise HTTPException(
+                    status_code=status.HTTP_400_BAD_REQUEST,
+                    detail=f"Team {raw!r} does not exist",
+                )
+            teams.add(raw)
+    return teams
+
+
 def _requires_access(
     *,
     is_authorized_callback: Callable[[], bool],
diff --git a/airflow-core/src/airflow/models/team.py 
b/airflow-core/src/airflow/models/team.py
index 3ec9d97edbb..7ff6085eabf 100644
--- a/airflow-core/src/airflow/models/team.py
+++ b/airflow-core/src/airflow/models/team.py
@@ -60,6 +60,12 @@ class Team(Base):
     def __repr__(self):
         return f"Team(name={self.name})"
 
+    @classmethod
+    @provide_session
+    def get_name_if_exists(cls, name: str, *, session: Session = NEW_SESSION) 
-> str | None:
+        """Return name if a Team row with that name exists, otherwise None."""
+        return session.scalar(select(cls.name).where(cls.name == name))
+
     @classmethod
     @provide_session
     def get_all_team_names(cls, session: Session = NEW_SESSION) -> set[str]:
diff --git a/airflow-core/tests/unit/api_fastapi/core_api/test_security.py 
b/airflow-core/tests/unit/api_fastapi/core_api/test_security.py
index 8773d882d93..9b599a41c5c 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/test_security.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/test_security.py
@@ -51,6 +51,7 @@ from airflow.api_fastapi.core_api.security import (
 )
 from airflow.models import Connection, Pool, Variable
 from airflow.models.dag import DagModel
+from airflow.models.team import Team
 
 from tests_common.test_utils.config import conf_vars
 
@@ -434,18 +435,24 @@ class TestFastApiSecurity:
         "team_name",
         [None, "team1"],
     )
+    @patch.object(Team, "get_name_if_exists")
     @patch.object(Connection, "get_team_name")
     @patch("airflow.api_fastapi.core_api.security.get_auth_manager")
-    def test_requires_access_connection(self, mock_get_auth_manager, 
mock_get_team_name, team_name):
+    async def test_requires_access_connection(
+        self, mock_get_auth_manager, mock_get_team_name, 
mock_get_name_if_exists, team_name
+    ):
         auth_manager = Mock()
         auth_manager.is_authorized_connection.return_value = True
         mock_get_auth_manager.return_value = auth_manager
+        mock_get_team_name.return_value = team_name
+        mock_get_name_if_exists.return_value = team_name
         fastapi_request = Mock()
         fastapi_request.path_params = {"connection_id": "conn_id"}
-        mock_get_team_name.return_value = team_name
+        fastapi_request.json = AsyncMock(return_value={})
         user = Mock()
 
-        requires_access_connection("GET")(fastapi_request, user)
+        with conf_vars({("core", "multi_team"): "True"}):
+            await requires_access_connection("GET")(fastapi_request, user)
 
         auth_manager.is_authorized_connection.assert_called_once_with(
             method="GET",
@@ -454,6 +461,85 @@ class TestFastApiSecurity:
         )
         mock_get_team_name.assert_called_once_with("conn_id")
 
+    @pytest.mark.db_test
+    @patch.object(Team, "get_name_if_exists")
+    @patch("airflow.api_fastapi.core_api.security.get_auth_manager")
+    async def test_requires_access_connection_team_from_body(
+        self, mock_get_auth_manager, mock_get_name_if_exists
+    ):
+        """When connection doesn't exist yet (POST), team_name is read from 
request body."""
+        auth_manager = Mock()
+        auth_manager.is_authorized_connection.return_value = True
+        mock_get_auth_manager.return_value = auth_manager
+        mock_get_name_if_exists.return_value = "team1"
+        fastapi_request = Mock()
+        fastapi_request.path_params = {}
+        fastapi_request.json = AsyncMock(return_value={"team_name": "team1"})
+        user = Mock()
+
+        with conf_vars({("core", "multi_team"): "True"}):
+            await requires_access_connection("POST")(fastapi_request, user)
+
+        auth_manager.is_authorized_connection.assert_called_once_with(
+            method="POST",
+            details=ConnectionDetails(conn_id=None, team_name="team1"),
+            user=user,
+        )
+        mock_get_name_if_exists.assert_called_once_with("team1")
+
+    @pytest.mark.db_test
+    @patch.object(Team, "get_name_if_exists")
+    @patch.object(Connection, "get_team_name")
+    @patch("airflow.api_fastapi.core_api.security.get_auth_manager")
+    async def test_requires_access_connection_put_checks_both_teams(
+        self, mock_get_auth_manager, mock_get_team_name, 
mock_get_name_if_exists
+    ):
+        """PUT checks both existing team (from DB) and new team (from body)."""
+        auth_manager = Mock()
+        auth_manager.is_authorized_connection.return_value = True
+        mock_get_auth_manager.return_value = auth_manager
+        mock_get_team_name.return_value = "old-team"
+        mock_get_name_if_exists.side_effect = lambda name: name
+        fastapi_request = Mock()
+        fastapi_request.path_params = {"connection_id": "conn_id"}
+        fastapi_request.json = AsyncMock(return_value={"team_name": 
"new-team"})
+        user = Mock()
+
+        with conf_vars({("core", "multi_team"): "True"}):
+            await requires_access_connection("PUT")(fastapi_request, user)
+
+        assert auth_manager.is_authorized_connection.call_count == 2
+        auth_manager.is_authorized_connection.assert_any_call(
+            method="PUT",
+            details=ConnectionDetails(conn_id="conn_id", team_name="old-team"),
+            user=user,
+        )
+        auth_manager.is_authorized_connection.assert_any_call(
+            method="PUT",
+            details=ConnectionDetails(conn_id="conn_id", team_name="new-team"),
+            user=user,
+        )
+
+    @pytest.mark.db_test
+    @patch.object(Team, "get_name_if_exists")
+    @patch("airflow.api_fastapi.core_api.security.get_auth_manager")
+    async def test_requires_access_connection_post_invalid_team_returns_400(
+        self, mock_get_auth_manager, mock_get_name_if_exists
+    ):
+        """POST with a team_name that doesn't exist raises 400."""
+        mock_get_name_if_exists.return_value = None
+        fastapi_request = Mock()
+        fastapi_request.path_params = {}
+        fastapi_request.json = AsyncMock(return_value={"team_name": 
"nonexistent"})
+        user = Mock()
+
+        with conf_vars({("core", "multi_team"): "True"}):
+            with pytest.raises(HTTPException) as exc_info:
+                await requires_access_connection("POST")(fastapi_request, user)
+
+        assert exc_info.value.status_code == 400
+        assert "nonexistent" in exc_info.value.detail
+
     @patch.object(Connection, "get_conn_id_to_team_name_mapping")
     @patch("airflow.api_fastapi.core_api.security.get_auth_manager")
     def test_requires_access_connection_bulk(
@@ -522,18 +608,24 @@ class TestFastApiSecurity:
         "team_name",
         [None, "team1"],
     )
+    @patch.object(Team, "get_name_if_exists")
     @patch.object(Variable, "get_team_name")
     @patch("airflow.api_fastapi.core_api.security.get_auth_manager")
-    def test_requires_access_variable(self, mock_get_auth_manager, 
mock_get_team_name, team_name):
+    async def test_requires_access_variable(
+        self, mock_get_auth_manager, mock_get_team_name, 
mock_get_name_if_exists, team_name
+    ):
         auth_manager = Mock()
         auth_manager.is_authorized_variable.return_value = True
         mock_get_auth_manager.return_value = auth_manager
+        mock_get_team_name.return_value = team_name
+        mock_get_name_if_exists.return_value = team_name
         fastapi_request = Mock()
         fastapi_request.path_params = {"variable_key": "var_key"}
-        mock_get_team_name.return_value = team_name
+        fastapi_request.json = AsyncMock(return_value={})
         user = Mock()
 
-        requires_access_variable("GET")(fastapi_request, user)
+        with conf_vars({("core", "multi_team"): "True"}):
+            await requires_access_variable("GET")(fastapi_request, user)
 
         auth_manager.is_authorized_variable.assert_called_once_with(
             method="GET",
@@ -542,6 +634,85 @@ class TestFastApiSecurity:
         )
         mock_get_team_name.assert_called_once_with("var_key")
 
+    @pytest.mark.db_test
+    @patch.object(Team, "get_name_if_exists")
+    @patch("airflow.api_fastapi.core_api.security.get_auth_manager")
+    async def test_requires_access_variable_team_from_body(
+        self, mock_get_auth_manager, mock_get_name_if_exists
+    ):
+        """When variable doesn't exist yet (POST), team_name is read from 
request body."""
+        auth_manager = Mock()
+        auth_manager.is_authorized_variable.return_value = True
+        mock_get_auth_manager.return_value = auth_manager
+        mock_get_name_if_exists.return_value = "team1"
+        fastapi_request = Mock()
+        fastapi_request.path_params = {}
+        fastapi_request.json = AsyncMock(return_value={"team_name": "team1"})
+        user = Mock()
+
+        with conf_vars({("core", "multi_team"): "True"}):
+            await requires_access_variable("POST")(fastapi_request, user)
+
+        auth_manager.is_authorized_variable.assert_called_once_with(
+            method="POST",
+            details=VariableDetails(key=None, team_name="team1"),
+            user=user,
+        )
+        mock_get_name_if_exists.assert_called_once_with("team1")
+
+    @pytest.mark.db_test
+    @patch.object(Team, "get_name_if_exists")
+    @patch.object(Variable, "get_team_name")
+    @patch("airflow.api_fastapi.core_api.security.get_auth_manager")
+    async def test_requires_access_variable_put_checks_both_teams(
+        self, mock_get_auth_manager, mock_get_team_name, 
mock_get_name_if_exists
+    ):
+        """PUT checks both existing team (from DB) and new team (from body)."""
+        auth_manager = Mock()
+        auth_manager.is_authorized_variable.return_value = True
+        mock_get_auth_manager.return_value = auth_manager
+        mock_get_team_name.return_value = "old-team"
+        mock_get_name_if_exists.side_effect = lambda name: name
+        fastapi_request = Mock()
+        fastapi_request.path_params = {"variable_key": "var_key"}
+        fastapi_request.json = AsyncMock(return_value={"team_name": 
"new-team"})
+        user = Mock()
+
+        with conf_vars({("core", "multi_team"): "True"}):
+            await requires_access_variable("PUT")(fastapi_request, user)
+
+        assert auth_manager.is_authorized_variable.call_count == 2
+        auth_manager.is_authorized_variable.assert_any_call(
+            method="PUT",
+            details=VariableDetails(key="var_key", team_name="old-team"),
+            user=user,
+        )
+        auth_manager.is_authorized_variable.assert_any_call(
+            method="PUT",
+            details=VariableDetails(key="var_key", team_name="new-team"),
+            user=user,
+        )
+
+    @pytest.mark.db_test
+    @patch.object(Team, "get_name_if_exists")
+    @patch("airflow.api_fastapi.core_api.security.get_auth_manager")
+    async def test_requires_access_variable_post_invalid_team_returns_400(
+        self, mock_get_auth_manager, mock_get_name_if_exists
+    ):
+        """POST with a team_name that doesn't exist raises 400."""
+        mock_get_name_if_exists.return_value = None
+        fastapi_request = Mock()
+        fastapi_request.path_params = {}
+        fastapi_request.json = AsyncMock(return_value={"team_name": 
"nonexistent"})
+        user = Mock()
+
+        with conf_vars({("core", "multi_team"): "True"}):
+            with pytest.raises(HTTPException) as exc_info:
+                await requires_access_variable("POST")(fastapi_request, user)
+
+        assert exc_info.value.status_code == 400
+        assert "nonexistent" in exc_info.value.detail
+
     @patch.object(Variable, "get_key_to_team_name_mapping")
     @patch("airflow.api_fastapi.core_api.security.get_auth_manager")
     def test_requires_access_variable_bulk(self, mock_get_auth_manager, 
mock_get_key_to_team_name_mapping):
@@ -607,18 +778,24 @@ class TestFastApiSecurity:
         "team_name",
         [None, "team1"],
     )
+    @patch.object(Team, "get_name_if_exists")
     @patch.object(Pool, "get_team_name")
     @patch("airflow.api_fastapi.core_api.security.get_auth_manager")
-    def test_requires_access_pool(self, mock_get_auth_manager, 
mock_get_team_name, team_name):
+    async def test_requires_access_pool(
+        self, mock_get_auth_manager, mock_get_team_name, 
mock_get_name_if_exists, team_name
+    ):
         auth_manager = Mock()
         auth_manager.is_authorized_pool.return_value = True
         mock_get_auth_manager.return_value = auth_manager
+        mock_get_team_name.return_value = team_name
+        mock_get_name_if_exists.return_value = team_name
         fastapi_request = Mock()
         fastapi_request.path_params = {"pool_name": "pool"}
-        mock_get_team_name.return_value = team_name
+        fastapi_request.json = AsyncMock(return_value={})
         user = Mock()
 
-        requires_access_pool("GET")(fastapi_request, user)
+        with conf_vars({("core", "multi_team"): "True"}):
+            await requires_access_pool("GET")(fastapi_request, user)
 
         auth_manager.is_authorized_pool.assert_called_once_with(
             method="GET",
@@ -627,6 +804,83 @@ class TestFastApiSecurity:
         )
         mock_get_team_name.assert_called_once_with("pool")
 
+    @pytest.mark.db_test
+    @patch.object(Team, "get_name_if_exists")
+    @patch("airflow.api_fastapi.core_api.security.get_auth_manager")
+    async def test_requires_access_pool_team_from_body(self, 
mock_get_auth_manager, mock_get_name_if_exists):
+        """When pool doesn't exist yet (POST), team_name is read from request 
body."""
+        auth_manager = Mock()
+        auth_manager.is_authorized_pool.return_value = True
+        mock_get_auth_manager.return_value = auth_manager
+        mock_get_name_if_exists.return_value = "team1"
+        fastapi_request = Mock()
+        fastapi_request.path_params = {}
+        fastapi_request.json = AsyncMock(return_value={"team_name": "team1"})
+        user = Mock()
+
+        with conf_vars({("core", "multi_team"): "True"}):
+            await requires_access_pool("POST")(fastapi_request, user)
+
+        auth_manager.is_authorized_pool.assert_called_once_with(
+            method="POST",
+            details=PoolDetails(name=None, team_name="team1"),
+            user=user,
+        )
+        mock_get_name_if_exists.assert_called_once_with("team1")
+
+    @pytest.mark.db_test
+    @patch.object(Team, "get_name_if_exists")
+    @patch.object(Pool, "get_team_name")
+    @patch("airflow.api_fastapi.core_api.security.get_auth_manager")
+    async def test_requires_access_pool_put_checks_both_teams(
+        self, mock_get_auth_manager, mock_get_team_name, 
mock_get_name_if_exists
+    ):
+        """PUT checks both existing team (from DB) and new team (from body)."""
+        auth_manager = Mock()
+        auth_manager.is_authorized_pool.return_value = True
+        mock_get_auth_manager.return_value = auth_manager
+        mock_get_team_name.return_value = "old-team"
+        mock_get_name_if_exists.side_effect = lambda name: name
+        fastapi_request = Mock()
+        fastapi_request.path_params = {"pool_name": "pool"}
+        fastapi_request.json = AsyncMock(return_value={"team_name": 
"new-team"})
+        user = Mock()
+
+        with conf_vars({("core", "multi_team"): "True"}):
+            await requires_access_pool("PUT")(fastapi_request, user)
+
+        assert auth_manager.is_authorized_pool.call_count == 2
+        auth_manager.is_authorized_pool.assert_any_call(
+            method="PUT",
+            details=PoolDetails(name="pool", team_name="old-team"),
+            user=user,
+        )
+        auth_manager.is_authorized_pool.assert_any_call(
+            method="PUT",
+            details=PoolDetails(name="pool", team_name="new-team"),
+            user=user,
+        )
+
+    @pytest.mark.db_test
+    @patch.object(Team, "get_name_if_exists")
+    @patch("airflow.api_fastapi.core_api.security.get_auth_manager")
+    async def test_requires_access_pool_post_invalid_team_returns_400(
+        self, mock_get_auth_manager, mock_get_name_if_exists
+    ):
+        """POST with a team_name that doesn't exist raises 400."""
+        mock_get_name_if_exists.return_value = None
+        fastapi_request = Mock()
+        fastapi_request.path_params = {}
+        fastapi_request.json = AsyncMock(return_value={"team_name": 
"nonexistent"})
+        user = Mock()
+
+        with conf_vars({("core", "multi_team"): "True"}):
+            with pytest.raises(HTTPException) as exc_info:
+                await requires_access_pool("POST")(fastapi_request, user)
+
+        assert exc_info.value.status_code == 400
+        assert "nonexistent" in exc_info.value.detail
+
     @patch.object(Pool, "get_name_to_team_name_mapping")
     @patch("airflow.api_fastapi.core_api.security.get_auth_manager")
     def test_requires_access_pool_bulk(self, mock_get_auth_manager, 
mock_get_name_to_team_name_mapping):
diff --git a/airflow-core/tests/unit/models/test_team.py 
b/airflow-core/tests/unit/models/test_team.py
index 4ac828fce26..dcc4c85cc2c 100644
--- a/airflow-core/tests/unit/models/test_team.py
+++ b/airflow-core/tests/unit/models/test_team.py
@@ -24,6 +24,14 @@ from airflow.models.team import Team
 class TestTeam:
     """Unit tests for Team model class methods."""
 
+    @pytest.mark.db_test
+    def test_get_name_if_exists_returns_name(self, testing_team):
+        assert Team.get_name_if_exists("testing") == "testing"
+
+    @pytest.mark.db_test
+    def test_get_name_if_exists_returns_none(self):
+        assert Team.get_name_if_exists("nonexistent") is None
+
     @pytest.mark.db_test
     def test_get_all_team_names_with_teams(self, testing_team):
         result = Team.get_all_team_names()

Reply via email to