2121import pytest
2222
2323from airflow .api_connexion .exceptions import EXCEPTIONS_LINK_MAP
24+ from airflow .models .dag import DagModel
2425from airflow .models .errors import ImportError
2526from airflow .security import permissions
2627from airflow .utils import timezone
2728from airflow .utils .session import provide_session
2829from tests .test_utils .api_connexion_utils import assert_401 , create_user , delete_user
2930from tests .test_utils .config import conf_vars
30- from tests .test_utils .db import clear_db_import_errors
31+ from tests .test_utils .db import clear_db_dags , clear_db_import_errors
3132
3233pytestmark = pytest .mark .db_test
3334
35+ TEST_DAG_IDS = ["test_dag" , "test_dag2" ]
36+
3437
3538@pytest .fixture (scope = "module" )
3639def configured_app (minimal_app_for_api ):
@@ -39,14 +42,34 @@ def configured_app(minimal_app_for_api):
3942 app , # type:ignore
4043 username = "test" ,
4144 role_name = "Test" ,
42- permissions = [(permissions .ACTION_CAN_READ , permissions .RESOURCE_IMPORT_ERROR )], # type: ignore
45+ permissions = [
46+ (permissions .ACTION_CAN_READ , permissions .RESOURCE_DAG ),
47+ (permissions .ACTION_CAN_READ , permissions .RESOURCE_IMPORT_ERROR ),
48+ ], # type: ignore
4349 )
4450 create_user (app , username = "test_no_permissions" , role_name = "TestNoPermissions" ) # type: ignore
51+ create_user (
52+ app , # type:ignore
53+ username = "test_single_dag" ,
54+ role_name = "TestSingleDAG" ,
55+ permissions = [(permissions .ACTION_CAN_READ , permissions .RESOURCE_IMPORT_ERROR )], # type: ignore
56+ )
57+ # For some reason, DAG level permissions are not synced when in the above list of perms,
58+ # so do it manually here:
59+ app .appbuilder .sm .bulk_sync_roles (
60+ [
61+ {
62+ "role" : "TestSingleDAG" ,
63+ "perms" : [(permissions .ACTION_CAN_READ , permissions .resource_name_for_dag (TEST_DAG_IDS [0 ]))],
64+ }
65+ ]
66+ )
4567
46- yield minimal_app_for_api
68+ yield app
4769
4870 delete_user (app , username = "test" ) # type: ignore
4971 delete_user (app , username = "test_no_permissions" ) # type: ignore
72+ delete_user (app , username = "test_single_dag" ) # type: ignore
5073
5174
5275class TestBaseImportError :
@@ -58,9 +81,11 @@ def setup_attrs(self, configured_app) -> None:
5881 self .client = self .app .test_client () # type:ignore
5982
6083 clear_db_import_errors ()
84+ clear_db_dags ()
6185
6286 def teardown_method (self ) -> None :
6387 clear_db_import_errors ()
88+ clear_db_dags ()
6489
6590 @staticmethod
6691 def _normalize_import_errors (import_errors ):
@@ -121,6 +146,72 @@ def test_should_raise_403_forbidden(self):
121146 )
122147 assert response .status_code == 403
123148
149+ def test_should_raise_403_forbidden_without_dag_read (self , session ):
150+ import_error = ImportError (
151+ filename = "Lorem_ipsum.py" ,
152+ stacktrace = "Lorem ipsum" ,
153+ timestamp = timezone .parse (self .timestamp , timezone = "UTC" ),
154+ )
155+ session .add (import_error )
156+ session .commit ()
157+
158+ response = self .client .get (
159+ f"/api/v1/importErrors/{ import_error .id } " , environ_overrides = {"REMOTE_USER" : "test_single_dag" }
160+ )
161+
162+ assert response .status_code == 403
163+
164+ def test_should_return_200_with_single_dag_read (self , session ):
165+ dag_model = DagModel (dag_id = TEST_DAG_IDS [0 ], fileloc = "Lorem_ipsum.py" )
166+ session .add (dag_model )
167+ import_error = ImportError (
168+ filename = "Lorem_ipsum.py" ,
169+ stacktrace = "Lorem ipsum" ,
170+ timestamp = timezone .parse (self .timestamp , timezone = "UTC" ),
171+ )
172+ session .add (import_error )
173+ session .commit ()
174+
175+ response = self .client .get (
176+ f"/api/v1/importErrors/{ import_error .id } " , environ_overrides = {"REMOTE_USER" : "test_single_dag" }
177+ )
178+
179+ assert response .status_code == 200
180+ response_data = response .json
181+ response_data ["import_error_id" ] = 1
182+ assert {
183+ "filename" : "Lorem_ipsum.py" ,
184+ "import_error_id" : 1 ,
185+ "stack_trace" : "Lorem ipsum" ,
186+ "timestamp" : "2020-06-10T12:00:00+00:00" ,
187+ } == response_data
188+
189+ def test_should_return_200_redacted_with_single_dag_read_in_dagfile (self , session ):
190+ for dag_id in TEST_DAG_IDS :
191+ dag_model = DagModel (dag_id = dag_id , fileloc = "Lorem_ipsum.py" )
192+ session .add (dag_model )
193+ import_error = ImportError (
194+ filename = "Lorem_ipsum.py" ,
195+ stacktrace = "Lorem ipsum" ,
196+ timestamp = timezone .parse (self .timestamp , timezone = "UTC" ),
197+ )
198+ session .add (import_error )
199+ session .commit ()
200+
201+ response = self .client .get (
202+ f"/api/v1/importErrors/{ import_error .id } " , environ_overrides = {"REMOTE_USER" : "test_single_dag" }
203+ )
204+
205+ assert response .status_code == 200
206+ response_data = response .json
207+ response_data ["import_error_id" ] = 1
208+ assert {
209+ "filename" : "Lorem_ipsum.py" ,
210+ "import_error_id" : 1 ,
211+ "stack_trace" : "REDACTED - you do not have read permission on all DAGs in the file" ,
212+ "timestamp" : "2020-06-10T12:00:00+00:00" ,
213+ } == response_data
214+
124215
125216class TestGetImportErrorsEndpoint (TestBaseImportError ):
126217 def test_get_import_errors (self , session ):
@@ -231,6 +322,71 @@ def test_should_raises_401_unauthenticated(self, session):
231322
232323 assert_401 (response )
233324
325+ def test_get_import_errors_single_dag (self , session ):
326+ for dag_id in TEST_DAG_IDS :
327+ fake_filename = f"/tmp/{ dag_id } .py"
328+ dag_model = DagModel (dag_id = dag_id , fileloc = fake_filename )
329+ session .add (dag_model )
330+ importerror = ImportError (
331+ filename = fake_filename ,
332+ stacktrace = "Lorem ipsum" ,
333+ timestamp = timezone .parse (self .timestamp , timezone = "UTC" ),
334+ )
335+ session .add (importerror )
336+ session .commit ()
337+
338+ response = self .client .get (
339+ "/api/v1/importErrors" , environ_overrides = {"REMOTE_USER" : "test_single_dag" }
340+ )
341+
342+ assert response .status_code == 200
343+ response_data = response .json
344+ self ._normalize_import_errors (response_data ["import_errors" ])
345+ assert {
346+ "import_errors" : [
347+ {
348+ "filename" : "/tmp/test_dag.py" ,
349+ "import_error_id" : 1 ,
350+ "stack_trace" : "Lorem ipsum" ,
351+ "timestamp" : "2020-06-10T12:00:00+00:00" ,
352+ },
353+ ],
354+ "total_entries" : 1 ,
355+ } == response_data
356+
357+ def test_get_import_errors_single_dag_in_dagfile (self , session ):
358+ for dag_id in TEST_DAG_IDS :
359+ fake_filename = "/tmp/all_in_one.py"
360+ dag_model = DagModel (dag_id = dag_id , fileloc = fake_filename )
361+ session .add (dag_model )
362+
363+ importerror = ImportError (
364+ filename = "/tmp/all_in_one.py" ,
365+ stacktrace = "Lorem ipsum" ,
366+ timestamp = timezone .parse (self .timestamp , timezone = "UTC" ),
367+ )
368+ session .add (importerror )
369+ session .commit ()
370+
371+ response = self .client .get (
372+ "/api/v1/importErrors" , environ_overrides = {"REMOTE_USER" : "test_single_dag" }
373+ )
374+
375+ assert response .status_code == 200
376+ response_data = response .json
377+ self ._normalize_import_errors (response_data ["import_errors" ])
378+ assert {
379+ "import_errors" : [
380+ {
381+ "filename" : "/tmp/all_in_one.py" ,
382+ "import_error_id" : 1 ,
383+ "stack_trace" : "REDACTED - you do not have read permission on all DAGs in the file" ,
384+ "timestamp" : "2020-06-10T12:00:00+00:00" ,
385+ },
386+ ],
387+ "total_entries" : 1 ,
388+ } == response_data
389+
234390
235391class TestGetImportErrorsEndpointPagination (TestBaseImportError ):
236392 @pytest .mark .parametrize (
0 commit comments