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()