16
16
# under the License.
17
17
from __future__ import annotations
18
18
19
- from datetime import datetime
20
-
21
19
import pytest
22
- import time_machine
23
- from uuid6 import uuid7
20
+ from httpx import Client
24
21
25
22
from tests_common .test_utils .db import AIRFLOW_V_3_1_PLUS
26
23
27
24
if not AIRFLOW_V_3_1_PLUS :
28
25
pytest .skip ("Human in the loop public API compatible with Airflow >= 3.0.1" , allow_module_level = True )
29
26
27
+ from datetime import datetime
30
28
from typing import TYPE_CHECKING , Any
31
29
30
+ import time_machine
31
+
32
+ from airflow ._shared .timezones .timezone import convert_to_utc
32
33
from airflow .models .hitl import HITLDetail
33
34
34
35
if TYPE_CHECKING :
36
+ from fastapi .testclient import TestClient
37
+ from sqlalchemy .orm import Session
38
+
35
39
from airflow .models .taskinstance import TaskInstance
36
40
41
+ from tests_common .pytest_plugin import CreateTaskInstance
42
+
37
43
pytestmark = pytest .mark .db_test
38
- TI_ID = uuid7 ()
44
+
45
+ default_hitl_detail_request_kwargs : dict [str , Any ] = {
46
+ # ti_id decided at a later stage
47
+ "subject" : "This is subject" ,
48
+ "body" : "this is body" ,
49
+ "options" : ["Approve" , "Reject" ],
50
+ "defaults" : ["Approve" ],
51
+ "multiple" : False ,
52
+ "params" : {"input_1" : 1 },
53
+ }
54
+ expected_empty_hitl_detail_response_part : dict [str , Any ] = {
55
+ "response_at" : None ,
56
+ "chosen_options" : None ,
57
+ "user_id" : None ,
58
+ "params_input" : {},
59
+ "response_received" : False ,
60
+ }
39
61
40
62
41
63
@pytest .fixture
42
- def sample_ti (create_task_instance ) -> TaskInstance :
64
+ def sample_ti (create_task_instance : CreateTaskInstance ) -> TaskInstance :
43
65
return create_task_instance ()
44
66
45
67
46
68
@pytest .fixture
47
- def sample_hitl_detail (session , sample_ti ) -> HITLDetail :
69
+ def sample_hitl_detail (session : Session , sample_ti : TaskInstance ) -> HITLDetail :
48
70
hitl_detail_model = HITLDetail (
49
71
ti_id = sample_ti .id ,
50
- options = ["Approve" , "Reject" ],
51
- subject = "This is subject" ,
52
- body = "this is body" ,
53
- defaults = ["Approve" ],
54
- multiple = False ,
55
- params = {"input_1" : 1 },
72
+ ** default_hitl_detail_request_kwargs ,
56
73
)
57
74
session .add (hitl_detail_model )
58
75
session .commit ()
@@ -61,54 +78,65 @@ def sample_hitl_detail(session, sample_ti) -> HITLDetail:
61
78
62
79
63
80
@pytest .fixture
64
- def expected_sample_hitl_detail_dict (sample_ti ) -> dict [str , Any ]:
81
+ def expected_sample_hitl_detail_dict (sample_ti : TaskInstance ) -> dict [str , Any ]:
65
82
return {
66
- "body" : "this is body" ,
67
- "defaults" : ["Approve" ],
68
- "multiple" : False ,
69
- "options" : ["Approve" , "Reject" ],
70
- "params" : {"input_1" : 1 },
71
- "params_input" : {},
72
- "response_at" : None ,
73
- "chosen_options" : None ,
74
- "response_received" : False ,
75
- "subject" : "This is subject" ,
76
83
"ti_id" : sample_ti .id ,
77
- "user_id" : None ,
84
+ ** default_hitl_detail_request_kwargs ,
85
+ ** expected_empty_hitl_detail_response_part ,
78
86
}
79
87
80
88
81
- def test_add_hitl_detail (client , create_task_instance , session ) -> None :
89
+ @pytest .mark .parametrize (
90
+ "existing_hitl_detail_args" ,
91
+ [
92
+ None ,
93
+ default_hitl_detail_request_kwargs ,
94
+ {
95
+ ** default_hitl_detail_request_kwargs ,
96
+ ** {
97
+ "params_input" : {"input_1" : 2 },
98
+ "response_at" : convert_to_utc (datetime (2025 , 7 , 3 , 0 , 0 , 0 )),
99
+ "chosen_options" : ["Reject" ],
100
+ "user_id" : "Fallback to defaults" ,
101
+ },
102
+ },
103
+ ],
104
+ ids = [
105
+ "no existing hitl detail" ,
106
+ "existing hitl detail without response" ,
107
+ "existing hitl detail with response" ,
108
+ ],
109
+ )
110
+ def test_upsert_hitl_detail (
111
+ client : TestClient ,
112
+ create_task_instance : CreateTaskInstance ,
113
+ session : Session ,
114
+ existing_hitl_detail_args : dict [str , Any ],
115
+ ) -> None :
82
116
ti = create_task_instance ()
83
117
session .commit ()
84
118
119
+ if existing_hitl_detail_args :
120
+ session .add (HITLDetail (ti_id = ti .id , ** existing_hitl_detail_args ))
121
+ session .commit ()
122
+
85
123
response = client .post (
86
124
f"/execution/hitl-details/{ ti .id } " ,
87
125
json = {
88
126
"ti_id" : ti .id ,
89
- "options" : ["Approve" , "Reject" ],
90
- "subject" : "This is subject" ,
91
- "body" : "this is body" ,
92
- "defaults" : ["Approve" ],
93
- "multiple" : False ,
94
- "params" : {"input_1" : 1 },
127
+ ** default_hitl_detail_request_kwargs ,
95
128
},
96
129
)
97
130
assert response .status_code == 201
98
131
assert response .json () == {
99
132
"ti_id" : ti .id ,
100
- "options" : ["Approve" , "Reject" ],
101
- "subject" : "This is subject" ,
102
- "body" : "this is body" ,
103
- "defaults" : ["Approve" ],
104
- "multiple" : False ,
105
- "params" : {"input_1" : 1 },
133
+ ** default_hitl_detail_request_kwargs ,
106
134
}
107
135
108
136
109
137
@time_machine .travel (datetime (2025 , 7 , 3 , 0 , 0 , 0 ), tick = False )
110
138
@pytest .mark .usefixtures ("sample_hitl_detail" )
111
- def test_update_hitl_detail (client , sample_ti ) -> None :
139
+ def test_update_hitl_detail (client : Client , sample_ti : TaskInstance ) -> None :
112
140
response = client .patch (
113
141
f"/execution/hitl-details/{ sample_ti .id } " ,
114
142
json = {
@@ -128,13 +156,7 @@ def test_update_hitl_detail(client, sample_ti) -> None:
128
156
129
157
130
158
@pytest .mark .usefixtures ("sample_hitl_detail" )
131
- def test_get_hitl_detail (client , sample_ti ) -> None :
159
+ def test_get_hitl_detail (client : Client , sample_ti : TaskInstance ) -> None :
132
160
response = client .get (f"/execution/hitl-details/{ sample_ti .id } " )
133
161
assert response .status_code == 200
134
- assert response .json () == {
135
- "params_input" : {},
136
- "response_at" : None ,
137
- "chosen_options" : None ,
138
- "response_received" : False ,
139
- "user_id" : None ,
140
- }
162
+ assert response .json () == expected_empty_hitl_detail_response_part
0 commit comments