potiuk commented on code in PR #67998:
URL: https://github.com/apache/airflow/pull/67998#discussion_r3680764936


##########
airflow-core/tests/unit/api_fastapi/execution_api/test_security.py:
##########
@@ -273,3 +275,36 @@ async def 
test_returns_none_without_session_when_multi_team_disabled(self):
 
         assert result is None
         mock_create_session.assert_not_called()
+
+
+class TestGetTeamNameForTI:
+    @pytest.mark.parametrize(
+        ("multi_team", "expected_result"),
+        [
+            (True, "team_a"),
+            (False, None),
+        ],
+    )
+    def test_get_team_name_for_ti_respects_multi_team(self, multi_team, 
expected_result):
+        session = MagicMock()
+        session.scalar.return_value = "team_a"
+
+        with patch("airflow.configuration.conf.getboolean", 
return_value=multi_team):
+            result = 
get_team_name_for_ti(UUID("d9edb890-fa95-4049-b66f-b6469d4fbc32"), session)
+
+        assert result == expected_result
+        if multi_team:
+            session.scalar.assert_called_once()
+        else:
+            session.scalar.assert_not_called()
+
+
+class TestTeamNameStatement:
+    def test_team_name_statement_checks_task_instance_and_history(self):

Review Comment:
   Asserting `"UNION ALL" in rendered` and `"FROM task_instance_history" in 
rendered` tests the SQL text rather than the behaviour — it would pass on a 
statement that compiles correctly but resolves the wrong team, and it'll break 
on any cosmetic change to how SQLAlchemy renders the query.
   
   What would actually protect this path is a DB-backed test: create a Dag on a 
bundle owned by a team, archive a try into `task_instance_history`, then assert 
`get_team_name_for_ti(...)` returns that team — and returns `None` for a TI 
whose bundle has no team. Given this decides authorization scope, behavioural 
coverage seems worth the extra setup.
   
   ---
   Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting



##########
airflow-core/tests/unit/api_fastapi/execution_api/test_security.py:
##########
@@ -273,3 +275,36 @@ async def 
test_returns_none_without_session_when_multi_team_disabled(self):
 
         assert result is None
         mock_create_session.assert_not_called()
+
+
+class TestGetTeamNameForTI:
+    @pytest.mark.parametrize(
+        ("multi_team", "expected_result"),
+        [
+            (True, "team_a"),
+            (False, None),
+        ],
+    )
+    def test_get_team_name_for_ti_respects_multi_team(self, multi_team, 
expected_result):
+        session = MagicMock()
+        session.scalar.return_value = "team_a"
+
+        with patch("airflow.configuration.conf.getboolean", 
return_value=multi_team):

Review Comment:
   `patch("airflow.configuration.conf.getboolean", return_value=multi_team)` 
replaces *every* `getboolean` call for the duration, not just `core/multi_team` 
— so anything else consulting a boolean config inside this block silently gets 
the parametrized value too.
   
   Airflow's convention here is `conf_vars` from 
`tests_common.test_utils.config`, which scopes the override to the specific 
key. Since the value varies with `@pytest.mark.parametrize`, use it as a 
context manager:
   
   ```python
   with conf_vars({("core", "multi_team"): str(multi_team)}):
       result = get_team_name_for_ti(UUID(...), session)
   ```
   
   ---
   Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting



##########
airflow-core/src/airflow/api_fastapi/execution_api/security.py:
##########
@@ -275,6 +276,17 @@ def _team_name_for_ti_stmt(ti_id):
         .where(TaskInstance.id == ti_id)
     )
 
+    task_instance_history_stmt = (
+        select(Team.name)
+        .select_from(TaskInstanceHistory)
+        .join(DagModel, DagModel.dag_id == TaskInstanceHistory.dag_id)
+        .join(DagBundleModel, DagBundleModel.name == DagModel.bundle_name)
+        .join(DagBundleModel.teams)
+        .where(TaskInstanceHistory.task_instance_id == ti_id)
+    )
+
+    return union_all(task_instance_stmt, task_instance_history_stmt)

Review Comment:
   Worth a thought on `union_all` vs `union`. `task_instance_id` is the sole 
primary key of `task_instance_history`, so this is at most two rows — one live, 
one archived — rather than one per try, which is much better than I first 
assumed.
   
   But for a retried TI both branches do match, and the caller uses 
`session.scalar()`, which takes the first row with no `ORDER BY`. Normally both 
resolve to the same team, so it's benign; if a Dag ever moved between bundles 
owned by different teams, the answer becomes whichever row the backend happened 
to return first. `union` would collapse identical values and make the duplicate 
case explicit if it ever isn't.
   
   ---
   Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to