jason810496 commented on code in PR #54827:
URL: https://github.com/apache/airflow/pull/54827#discussion_r2300787789


##########
providers/standard/src/airflow/providers/standard/operators/hitl.py:
##########
@@ -87,12 +89,33 @@ def __init__(
         self.respondents = [respondents] if isinstance(respondents, str) else 
respondents
 
         self.validate_options()
+        self.validate_params()
         self.validate_defaults()
 
     def validate_options(self) -> None:
+        """
+        Validate the `options` attribute of the instance.
+
+        Raises:
+            ValueError: If `options` is empty.
+            ValueError: If any option contains a comma (`,`), which is not 
allowed.
+        """
         if not self.options:
             raise ValueError('"options" cannot be empty.')
 
+        if any("," in option for option in self.options):

Review Comment:
   The unit test for `validate_options` needs update:
   
https://github.com/apache/airflow/blob/26515d3ef28212b5164779205cdf1f6495665f32/providers/standard/tests/unit/standard/operators/test_hitl.py#L58



##########
providers/standard/src/airflow/providers/standard/operators/hitl.py:
##########
@@ -87,12 +89,33 @@ def __init__(
         self.respondents = [respondents] if isinstance(respondents, str) else 
respondents
 
         self.validate_options()
+        self.validate_params()
         self.validate_defaults()
 
     def validate_options(self) -> None:
+        """
+        Validate the `options` attribute of the instance.
+
+        Raises:
+            ValueError: If `options` is empty.
+            ValueError: If any option contains a comma (`,`), which is not 
allowed.
+        """
         if not self.options:
             raise ValueError('"options" cannot be empty.')
 
+        if any("," in option for option in self.options):
+            raise ValueError('"," is not allowed in option')
+
+    def validate_params(self) -> None:

Review Comment:
   The unit test for `validate_params` is missing.



##########
providers/standard/tests/unit/standard/operators/test_hitl.py:
##########
@@ -238,6 +264,170 @@ def test_validate_params_input_with_invalid_input(self) 
-> None:
                 },
             )
 
+    @pytest.mark.parametrize(
+        "options, params_input, expected_query_string",
+        [
+            (None, None, "?map_index=-1"),
+            ("1", None, "?_options=1&map_index=-1"),
+            (["1", "2"], None, "?_options=1%2C2&map_index=-1"),
+            (None, {"input_1": 123}, "?input_1=123&map_index=-1"),
+            (
+                ["3", "4", "5"],
+                {"input_1": 123123, "input_2": 345345},
+                
"?_options=3%2C4%2C5&input_1=123123&input_2=345345&map_index=-1",
+            ),
+        ],
+        ids=[
+            "empty",
+            "single-option",
+            "multiple-options",
+            "single-param-input",
+            "multiple-options-and-param-inputs",
+        ],
+    )
+    @pytest.mark.parametrize("base_url", ["http://test";, "http://test_2:8080";])
+    def test_generate_link_to_ui(
+        self,
+        dag_maker: DagMaker,
+        base_url: str,
+        options: list[str] | None,
+        params_input: dict[str, Any] | None,
+        expected_query_string: str,
+    ) -> None:
+        with dag_maker("test_dag"):
+            task = HITLOperator(
+                task_id="hitl_test",
+                subject="This is subject",
+                options=["1", "2", "3", "4", "5"],
+                body="This is body",
+                defaults=["1"],
+                respondents="test",
+                multiple=True,
+                params=ParamsDict({"input_1": 1, "input_2": 2, "input_3": 3}),
+            )
+        dr = dag_maker.create_dagrun()
+        ti = dag_maker.run_ti(task.task_id, dr)
+
+        expected_url = (
+            
f"{base_url}/dags/test_dag/runs/test/tasks/hitl_test/required_actions{expected_query_string}"
+        )
+
+        url = task.generate_link_to_ui(
+            task_instance=ti,
+            base_url=base_url,
+            options=options,
+            params_input=params_input,
+        )
+        assert url == expected_url
+
+    @pytest.mark.parametrize(
+        "options, params_input, expected_query_string",
+        [
+            (None, None, "?map_index=-1"),
+            ("1", None, "?_options=1&map_index=-1"),
+            (["1", "2"], None, "?_options=1%2C2&map_index=-1"),
+            (None, {"input_1": 123}, "?input_1=123&map_index=-1"),
+            (
+                ["3", "4", "5"],
+                {"input_1": 123123, "input_2": 345345},
+                
"?_options=3%2C4%2C5&input_1=123123&input_2=345345&map_index=-1",
+            ),
+        ],
+        ids=[
+            "empty",
+            "single-option",
+            "multiple-options",
+            "single-param-input",
+            "multiple-options-and-param-inputs",
+        ],
+    )
+    @conf_vars({("api", "base_url"): "http://localhost:8080/"})
+    def test_generate_link_fall_back_to_conf_api_base_url(
+        self,
+        dag_maker: DagMaker,
+        options: list[str] | None,
+        params_input: dict[str, Any] | None,
+        expected_query_string: str,
+    ) -> None:
+        with dag_maker("test_dag"):
+            task = HITLOperator(
+                task_id="hitl_test",
+                subject="This is subject",
+                options=["1", "2", "3", "4", "5"],
+                body="This is body",
+                defaults=["1"],
+                respondents="test",
+                multiple=True,
+                params=ParamsDict({"input_1": 1, "input_2": 2, "input_3": 3}),
+            )
+        dr = dag_maker.create_dagrun()
+        ti = dag_maker.run_ti(task.task_id, dr)
+
+        expected_url = 
f"http://localhost:8080/dags/test_dag/runs/test/tasks/hitl_test/required_actions{expected_query_string}";
+
+        url = task.generate_link_to_ui(
+            task_instance=ti,
+            options=options,
+            params_input=params_input,
+        )
+        assert url == expected_url
+
+    @pytest.mark.parametrize(
+        "options, params_input, expected_err_msg",
+        [
+            ([100, "2", 30000], None, "options {.*} are not valid options"),
+            (
+                None,
+                {"input_not_exist": 123, "no_such_key": 123},
+                "params {.*} are not valid params",
+            ),
+        ],
+    )
+    def test_generate_link_to_ui_with_invalid_input(
+        self,
+        dag_maker: DagMaker,
+        options: list[str] | None,
+        params_input: dict[str, Any] | None,
+        expected_err_msg: str,
+    ) -> None:
+        with dag_maker("test_dag"):
+            task = HITLOperator(
+                task_id="hitl_test",
+                subject="This is subject",
+                options=["1", "2", "3", "4", "5"],
+                body="This is body",
+                defaults=["1"],
+                respondents="test",
+                multiple=True,
+                params=ParamsDict({"input_1": 1, "input_2": 2, "input_3": 3}),
+            )
+        dr = dag_maker.create_dagrun()
+        ti = dag_maker.run_ti(task.task_id, dr)
+
+        with pytest.raises(ValueError, match=expected_err_msg):
+            task.generate_link_to_ui(task_instance=ti, options=options, 
params_input=params_input)
+
+    def test_generate_link_to_ui_without_base_url(
+        self,
+        dag_maker: DagMaker,
+    ) -> None:
+        with dag_maker("test_dag"):
+            task = HITLOperator(
+                task_id="hitl_test",
+                subject="This is subject",
+                options=["1", "2", "3", "4", "5"],
+                body="This is body",
+                defaults=["1"],
+                respondents="test",
+                multiple=True,
+                params=ParamsDict({"input_1": 1, "input_2": 2, "input_3": 3}),
+            )
+        dr = dag_maker.create_dagrun()
+        ti = dag_maker.run_ti(task.task_id, dr)

Review Comment:
   How about having another fixture to setup the Dag for generating link for UI 
and yield the `HITLOperator  instance`, `DagRun` and `TaskInstance`?
   It seems the Dags added in this PR are same.



-- 
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: commits-unsubscr...@airflow.apache.org

For queries about this service, please contact Infrastructure at:
us...@infra.apache.org

Reply via email to