blob: 434511b9e95601e7edcd9ad052d339be90326260 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from __future__ import annotations
import urllib
from datetime import timedelta
from unittest import mock
import pytest
import time_machine
from airflow.api_connexion.exceptions import EXCEPTIONS_LINK_MAP
from airflow.datasets import Dataset
from airflow.models import DAG, DagModel, DagRun
from airflow.models.dataset import DatasetEvent, DatasetModel
from airflow.operators.empty import EmptyOperator
from airflow.security import permissions
from airflow.utils import timezone
from airflow.utils.session import create_session, provide_session
from airflow.utils.state import DagRunState, State
from airflow.utils.types import DagRunType
from tests.test_utils.api_connexion_utils import assert_401, create_user, delete_roles, delete_user
from tests.test_utils.config import conf_vars
from tests.test_utils.db import clear_db_dags, clear_db_runs, clear_db_serialized_dags
from tests.test_utils.www import _check_last_log
@pytest.fixture(scope="module")
def configured_app(minimal_app_for_api):
app = minimal_app_for_api
create_user(
app, # type: ignore
username="test",
role_name="Test",
permissions=[
(permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG),
(permissions.ACTION_CAN_READ, permissions.RESOURCE_DATASET),
(permissions.ACTION_CAN_READ, permissions.RESOURCE_CLUSTER_ACTIVITY),
(permissions.ACTION_CAN_EDIT, permissions.RESOURCE_DAG),
(permissions.ACTION_CAN_CREATE, permissions.RESOURCE_DAG_RUN),
(permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG_RUN),
(permissions.ACTION_CAN_EDIT, permissions.RESOURCE_DAG_RUN),
(permissions.ACTION_CAN_DELETE, permissions.RESOURCE_DAG_RUN),
],
)
create_user(
app, # type: ignore
username="test_dag_view_only",
role_name="TestViewDags",
permissions=[
(permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG),
(permissions.ACTION_CAN_CREATE, permissions.RESOURCE_DAG_RUN),
(permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG_RUN),
(permissions.ACTION_CAN_EDIT, permissions.RESOURCE_DAG_RUN),
(permissions.ACTION_CAN_DELETE, permissions.RESOURCE_DAG_RUN),
],
)
create_user(
app, # type: ignore
username="test_view_dags",
role_name="TestViewDags",
permissions=[
(permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG),
(permissions.ACTION_CAN_CREATE, permissions.RESOURCE_DAG_RUN),
],
)
create_user(
app, # type: ignore
username="test_granular_permissions",
role_name="TestGranularDag",
permissions=[(permissions.ACTION_CAN_READ, permissions.RESOURCE_DAG_RUN)],
)
app.appbuilder.sm.sync_perm_for_dag( # type: ignore
"TEST_DAG_ID",
access_control={"TestGranularDag": [permissions.ACTION_CAN_EDIT, permissions.ACTION_CAN_READ]},
)
create_user(app, username="test_no_permissions", role_name="TestNoPermissions") # type: ignore
yield app
delete_user(app, username="test") # type: ignore
delete_user(app, username="test_dag_view_only") # type: ignore
delete_user(app, username="test_view_dags") # type: ignore
delete_user(app, username="test_granular_permissions") # type: ignore
delete_user(app, username="test_no_permissions") # type: ignore
delete_roles(app)
class TestDagRunEndpoint:
default_time = "2020-06-11T18:00:00+00:00"
default_time_2 = "2020-06-12T18:00:00+00:00"
default_time_3 = "2020-06-13T18:00:00+00:00"
@pytest.fixture(autouse=True)
def setup_attrs(self, configured_app) -> None:
self.app = configured_app
self.client = self.app.test_client() # type:ignore
clear_db_runs()
clear_db_serialized_dags()
clear_db_dags()
def teardown_method(self) -> None:
clear_db_runs()
clear_db_dags()
clear_db_serialized_dags()
def _create_dag(self, dag_id):
dag_instance = DagModel(dag_id=dag_id)
dag_instance.is_active = True
with create_session() as session:
session.add(dag_instance)
dag = DAG(dag_id=dag_id, schedule=None)
self.app.dag_bag.bag_dag(dag, root_dag=dag)
return dag_instance
def _create_test_dag_run(self, state="running", extra_dag=False, commit=True, idx_start=1):
dag_runs = []
dags = []
for i in range(idx_start, idx_start + 2):
if i == 1:
dags.append(DagModel(dag_id="TEST_DAG_ID", is_active=True))
dagrun_model = DagRun(
dag_id="TEST_DAG_ID",
run_id="TEST_DAG_RUN_ID_" + str(i),
run_type=DagRunType.MANUAL,
execution_date=timezone.parse(self.default_time) + timedelta(days=i - 1),
start_date=timezone.parse(self.default_time),
external_trigger=True,
state=state,
)
dag_runs.append(dagrun_model)
if extra_dag:
for i in range(idx_start + 2, idx_start + 4):
dags.append(DagModel(dag_id="TEST_DAG_ID_" + str(i)))
dag_runs.append(
DagRun(
dag_id="TEST_DAG_ID_" + str(i),
run_id="TEST_DAG_RUN_ID_" + str(i),
run_type=DagRunType.MANUAL,
execution_date=timezone.parse(self.default_time_2),
start_date=timezone.parse(self.default_time),
external_trigger=True,
state=state,
)
)
if commit:
with create_session() as session:
session.add_all(dag_runs)
session.add_all(dags)
return dag_runs
class TestDeleteDagRun(TestDagRunEndpoint):
def test_should_respond_204(self, session):
session.add_all(self._create_test_dag_run())
session.commit()
response = self.client.delete(
"api/v1/dags/TEST_DAG_ID/dagRuns/TEST_DAG_RUN_ID_1", environ_overrides={"REMOTE_USER": "test"}
)
assert response.status_code == 204
# Check if the Dag Run is deleted from the database
response = self.client.get(
"api/v1/dags/TEST_DAG_ID/dagRuns/TEST_DAG_RUN_ID_1", environ_overrides={"REMOTE_USER": "test"}
)
assert response.status_code == 404
def test_should_respond_404(self):
response = self.client.delete(
"api/v1/dags/INVALID_DAG_RUN/dagRuns/INVALID_DAG_RUN", environ_overrides={"REMOTE_USER": "test"}
)
assert response.status_code == 404
assert response.json == {
"detail": "DAGRun with DAG ID: 'INVALID_DAG_RUN' and DagRun ID: 'INVALID_DAG_RUN' not found",
"status": 404,
"title": "Not Found",
"type": EXCEPTIONS_LINK_MAP[404],
}
def test_should_raises_401_unauthenticated(self, session):
session.add_all(self._create_test_dag_run())
session.commit()
response = self.client.delete(
"api/v1/dags/TEST_DAG_ID/dagRuns/TEST_DAG_RUN_ID_1",
)
assert_401(response)
def test_should_raise_403_forbidden(self):
response = self.client.get(
"api/v1/dags/TEST_DAG_ID/dagRuns/TEST_DAG_RUN_ID",
environ_overrides={"REMOTE_USER": "test_no_permissions"},
)
assert response.status_code == 403
class TestGetDagRun(TestDagRunEndpoint):
def test_should_respond_200(self, session):
dagrun_model = DagRun(
dag_id="TEST_DAG_ID",
run_id="TEST_DAG_RUN_ID",
run_type=DagRunType.MANUAL,
execution_date=timezone.parse(self.default_time),
start_date=timezone.parse(self.default_time),
external_trigger=True,
state="running",
)
session.add(dagrun_model)
session.commit()
result = session.query(DagRun).all()
assert len(result) == 1
response = self.client.get(
"api/v1/dags/TEST_DAG_ID/dagRuns/TEST_DAG_RUN_ID", environ_overrides={"REMOTE_USER": "test"}
)
assert response.status_code == 200
assert response.json == {
"dag_id": "TEST_DAG_ID",
"dag_run_id": "TEST_DAG_RUN_ID",
"end_date": None,
"state": "running",
"logical_date": self.default_time,
"execution_date": self.default_time,
"external_trigger": True,
"start_date": self.default_time,
"conf": {},
"data_interval_end": None,
"data_interval_start": None,
"last_scheduling_decision": None,
"run_type": "manual",
"note": None,
}
def test_should_respond_404(self):
response = self.client.get(
"api/v1/dags/invalid-id/dagRuns/invalid-id", environ_overrides={"REMOTE_USER": "test"}
)
assert response.status_code == 404
expected_resp = {
"detail": "DAGRun with DAG ID: 'invalid-id' and DagRun ID: 'invalid-id' not found",
"status": 404,
"title": "DAGRun not found",
"type": EXCEPTIONS_LINK_MAP[404],
}
assert expected_resp == response.json
def test_should_raises_401_unauthenticated(self, session):
dagrun_model = DagRun(
dag_id="TEST_DAG_ID",
run_id="TEST_DAG_RUN_ID",
run_type=DagRunType.MANUAL,
execution_date=timezone.parse(self.default_time),
start_date=timezone.parse(self.default_time),
external_trigger=True,
)
session.add(dagrun_model)
session.commit()
response = self.client.get("api/v1/dags/TEST_DAG_ID/dagRuns/TEST_DAG_RUN_ID")
assert_401(response)
class TestGetDagRuns(TestDagRunEndpoint):
def test_should_respond_200(self, session):
self._create_test_dag_run()
result = session.query(DagRun).all()
assert len(result) == 2
response = self.client.get(
"api/v1/dags/TEST_DAG_ID/dagRuns", environ_overrides={"REMOTE_USER": "test"}
)
assert response.status_code == 200
assert response.json == {
"dag_runs": [
{
"dag_id": "TEST_DAG_ID",
"dag_run_id": "TEST_DAG_RUN_ID_1",
"end_date": None,
"state": "running",
"execution_date": self.default_time,
"logical_date": self.default_time,
"external_trigger": True,
"start_date": self.default_time,
"conf": {},
"data_interval_end": None,
"data_interval_start": None,
"last_scheduling_decision": None,
"run_type": "manual",
"note": None,
},
{
"dag_id": "TEST_DAG_ID",
"dag_run_id": "TEST_DAG_RUN_ID_2",
"end_date": None,
"state": "running",
"execution_date": self.default_time_2,
"logical_date": self.default_time_2,
"external_trigger": True,
"start_date": self.default_time,
"conf": {},
"data_interval_end": None,
"data_interval_start": None,
"last_scheduling_decision": None,
"run_type": "manual",
"note": None,
},
],
"total_entries": 2,
}
def test_filter_by_state(self, session):
self._create_test_dag_run()
self._create_test_dag_run(state="queued", idx_start=3)
assert session.query(DagRun).count() == 4
response = self.client.get(
"api/v1/dags/TEST_DAG_ID/dagRuns?state=running,queued", environ_overrides={"REMOTE_USER": "test"}
)
assert response.status_code == 200
assert response.json["total_entries"] == 4
assert response.json["dag_runs"][0]["state"] == response.json["dag_runs"][1]["state"] == "running"
assert response.json["dag_runs"][2]["state"] == response.json["dag_runs"][3]["state"] == "queued"
def test_invalid_order_by_raises_400(self):
self._create_test_dag_run()
response = self.client.get(
"api/v1/dags/TEST_DAG_ID/dagRuns?order_by=invalid", environ_overrides={"REMOTE_USER": "test"}
)
assert response.status_code == 400
msg = "Ordering with 'invalid' is disallowed or the attribute does not exist on the model"
assert response.json["detail"] == msg
def test_return_correct_results_with_order_by(self, session):
self._create_test_dag_run()
result = session.query(DagRun).all()
assert len(result) == 2
response = self.client.get(
"api/v1/dags/TEST_DAG_ID/dagRuns?order_by=-execution_date",
environ_overrides={"REMOTE_USER": "test"},
)
assert response.status_code == 200
assert self.default_time < self.default_time_2
# - means descending
assert response.json == {
"dag_runs": [
{
"dag_id": "TEST_DAG_ID",
"dag_run_id": "TEST_DAG_RUN_ID_2",
"end_date": None,
"state": "running",
"execution_date": self.default_time_2,
"logical_date": self.default_time_2,
"external_trigger": True,
"start_date": self.default_time,
"conf": {},
"data_interval_end": None,
"data_interval_start": None,
"last_scheduling_decision": None,
"run_type": "manual",
"note": None,
},
{
"dag_id": "TEST_DAG_ID",
"dag_run_id": "TEST_DAG_RUN_ID_1",
"end_date": None,
"state": "running",
"execution_date": self.default_time,
"logical_date": self.default_time,
"external_trigger": True,
"start_date": self.default_time,
"conf": {},
"data_interval_end": None,
"data_interval_start": None,
"last_scheduling_decision": None,
"run_type": "manual",
"note": None,
},
],
"total_entries": 2,
}
def test_should_return_all_with_tilde_as_dag_id_and_all_dag_permissions(self):
self._create_test_dag_run(extra_dag=True)
expected_dag_run_ids = ["TEST_DAG_ID", "TEST_DAG_ID", "TEST_DAG_ID_3", "TEST_DAG_ID_4"]
response = self.client.get("api/v1/dags/~/dagRuns", environ_overrides={"REMOTE_USER": "test"})
assert response.status_code == 200
dag_run_ids = [dag_run["dag_id"] for dag_run in response.json["dag_runs"]]
assert dag_run_ids == expected_dag_run_ids
def test_should_return_accessible_with_tilde_as_dag_id_and_dag_level_permissions(self):
self._create_test_dag_run(extra_dag=True)
expected_dag_run_ids = ["TEST_DAG_ID", "TEST_DAG_ID"]
response = self.client.get(
"api/v1/dags/~/dagRuns", environ_overrides={"REMOTE_USER": "test_granular_permissions"}
)
assert response.status_code == 200
dag_run_ids = [dag_run["dag_id"] for dag_run in response.json["dag_runs"]]
assert dag_run_ids == expected_dag_run_ids
def test_should_raises_401_unauthenticated(self):
self._create_test_dag_run()
response = self.client.get("api/v1/dags/TEST_DAG_ID/dagRuns")
assert_401(response)
class TestGetDagRunsPagination(TestDagRunEndpoint):
@pytest.mark.parametrize(
"url, expected_dag_run_ids",
[
("api/v1/dags/TEST_DAG_ID/dagRuns?limit=1", ["TEST_DAG_RUN_ID1"]),
(
"api/v1/dags/TEST_DAG_ID/dagRuns?limit=2",
["TEST_DAG_RUN_ID1", "TEST_DAG_RUN_ID2"],
),
(
"api/v1/dags/TEST_DAG_ID/dagRuns?offset=5",
[
"TEST_DAG_RUN_ID6",
"TEST_DAG_RUN_ID7",
"TEST_DAG_RUN_ID8",
"TEST_DAG_RUN_ID9",
"TEST_DAG_RUN_ID10",
],
),
(
"api/v1/dags/TEST_DAG_ID/dagRuns?offset=0",
[
"TEST_DAG_RUN_ID1",
"TEST_DAG_RUN_ID2",
"TEST_DAG_RUN_ID3",
"TEST_DAG_RUN_ID4",
"TEST_DAG_RUN_ID5",
"TEST_DAG_RUN_ID6",
"TEST_DAG_RUN_ID7",
"TEST_DAG_RUN_ID8",
"TEST_DAG_RUN_ID9",
"TEST_DAG_RUN_ID10",
],
),
("api/v1/dags/TEST_DAG_ID/dagRuns?limit=1&offset=5", ["TEST_DAG_RUN_ID6"]),
("api/v1/dags/TEST_DAG_ID/dagRuns?limit=1&offset=1", ["TEST_DAG_RUN_ID2"]),
(
"api/v1/dags/TEST_DAG_ID/dagRuns?limit=2&offset=2",
["TEST_DAG_RUN_ID3", "TEST_DAG_RUN_ID4"],
),
],
)
def test_handle_limit_and_offset(self, url, expected_dag_run_ids):
self._create_dag_runs(10)
response = self.client.get(url, environ_overrides={"REMOTE_USER": "test"})
assert response.status_code == 200
assert response.json["total_entries"] == 10
dag_run_ids = [dag_run["dag_run_id"] for dag_run in response.json["dag_runs"]]
assert dag_run_ids == expected_dag_run_ids
def test_should_respect_page_size_limit(self):
self._create_dag_runs(200)
response = self.client.get(
"api/v1/dags/TEST_DAG_ID/dagRuns", environ_overrides={"REMOTE_USER": "test"}
)
assert response.status_code == 200
assert response.json["total_entries"] == 200
assert len(response.json["dag_runs"]) == 100 # default is 100
@conf_vars({("api", "maximum_page_limit"): "150"})
def test_should_return_conf_max_if_req_max_above_conf(self):
self._create_dag_runs(200)
response = self.client.get(
"api/v1/dags/TEST_DAG_ID/dagRuns?limit=180", environ_overrides={"REMOTE_USER": "test"}
)
assert response.status_code == 200
assert len(response.json["dag_runs"]) == 150
def _create_dag_runs(self, count):
dag_runs = [
DagRun(
dag_id="TEST_DAG_ID",
run_id="TEST_DAG_RUN_ID" + str(i),
run_type=DagRunType.MANUAL,
execution_date=timezone.parse(self.default_time) + timedelta(minutes=i),
start_date=timezone.parse(self.default_time),
external_trigger=True,
)
for i in range(1, count + 1)
]
dag = DagModel(dag_id="TEST_DAG_ID")
with create_session() as session:
session.add_all(dag_runs)
session.add(dag)
class TestGetDagRunsPaginationFilters(TestDagRunEndpoint):
@pytest.mark.parametrize(
"url, expected_dag_run_ids",
[
(
"api/v1/dags/TEST_DAG_ID/dagRuns?start_date_gte=2020-06-18T18%3A00%3A00%2B00%3A00",
["TEST_START_EXEC_DAY_18", "TEST_START_EXEC_DAY_19"],
),
(
"api/v1/dags/TEST_DAG_ID/dagRuns?start_date_lte=2020-06-11T18%3A00%3A00%2B00%3A00",
["TEST_START_EXEC_DAY_10", "TEST_START_EXEC_DAY_11"],
),
(
"api/v1/dags/TEST_DAG_ID/dagRuns?start_date_lte=2020-06-15T18%3A00%3A00%2B00%3A00"
"&start_date_gte=2020-06-12T18:00:00Z",
[
"TEST_START_EXEC_DAY_12",
"TEST_START_EXEC_DAY_13",
"TEST_START_EXEC_DAY_14",
"TEST_START_EXEC_DAY_15",
],
),
(
"api/v1/dags/TEST_DAG_ID/dagRuns?execution_date_lte=2020-06-13T18%3A00%3A00%2B00%3A00",
[
"TEST_START_EXEC_DAY_10",
"TEST_START_EXEC_DAY_11",
"TEST_START_EXEC_DAY_12",
"TEST_START_EXEC_DAY_13",
],
),
(
"api/v1/dags/TEST_DAG_ID/dagRuns?execution_date_gte=2020-06-16T18%3A00%3A00%2B00%3A00",
[
"TEST_START_EXEC_DAY_16",
"TEST_START_EXEC_DAY_17",
"TEST_START_EXEC_DAY_18",
"TEST_START_EXEC_DAY_19",
],
),
],
)
@provide_session
def test_date_filters_gte_and_lte(self, url, expected_dag_run_ids, session):
dagrun_models = self._create_dag_runs()
session.add_all(dagrun_models)
session.commit()
response = self.client.get(url, environ_overrides={"REMOTE_USER": "test"})
assert response.status_code == 200
assert response.json["total_entries"] == len(expected_dag_run_ids)
dag_run_ids = [dag_run["dag_run_id"] for dag_run in response.json["dag_runs"]]
assert dag_run_ids == expected_dag_run_ids
def _create_dag_runs(self):
dates = [
"2020-06-10T18:00:00+00:00",
"2020-06-11T18:00:00+00:00",
"2020-06-12T18:00:00+00:00",
"2020-06-13T18:00:00+00:00",
"2020-06-14T18:00:00+00:00",
"2020-06-15T18:00:00Z",
"2020-06-16T18:00:00Z",
"2020-06-17T18:00:00Z",
"2020-06-18T18:00:00Z",
"2020-06-19T18:00:00Z",
]
return [
DagRun(
dag_id="TEST_DAG_ID",
run_id="TEST_START_EXEC_DAY_1" + str(i),
run_type=DagRunType.MANUAL,
execution_date=timezone.parse(dates[i]),
start_date=timezone.parse(dates[i]),
external_trigger=True,
state="success",
)
for i in range(len(dates))
]
class TestGetDagRunsEndDateFilters(TestDagRunEndpoint):
@pytest.mark.parametrize(
"url, expected_dag_run_ids",
[
(
f"api/v1/dags/TEST_DAG_ID/dagRuns?end_date_gte="
f"{urllib.parse.quote((timezone.utcnow() + timedelta(days=1)).isoformat())}",
[],
),
(
f"api/v1/dags/TEST_DAG_ID/dagRuns?end_date_lte="
f"{urllib.parse.quote((timezone.utcnow() + timedelta(days=1)).isoformat())}",
["TEST_DAG_RUN_ID_1", "TEST_DAG_RUN_ID_2"],
),
],
)
def test_end_date_gte_lte(self, url, expected_dag_run_ids):
self._create_test_dag_run("success") # state==success, then end date is today
response = self.client.get(url, environ_overrides={"REMOTE_USER": "test"})
assert response.status_code == 200
assert response.json["total_entries"] == len(expected_dag_run_ids)
dag_run_ids = [dag_run["dag_run_id"] for dag_run in response.json["dag_runs"] if dag_run]
assert dag_run_ids == expected_dag_run_ids
class TestGetDagRunBatch(TestDagRunEndpoint):
def test_should_respond_200(self):
self._create_test_dag_run()
response = self.client.post(
"api/v1/dags/~/dagRuns/list",
json={"dag_ids": ["TEST_DAG_ID"]},
environ_overrides={"REMOTE_USER": "test"},
)
assert response.status_code == 200
assert response.json == {
"dag_runs": [
{
"dag_id": "TEST_DAG_ID",
"dag_run_id": "TEST_DAG_RUN_ID_1",
"end_date": None,
"state": "running",
"execution_date": self.default_time,
"logical_date": self.default_time,
"external_trigger": True,
"start_date": self.default_time,
"conf": {},
"data_interval_end": None,
"data_interval_start": None,
"last_scheduling_decision": None,
"run_type": "manual",
"note": None,
},
{
"dag_id": "TEST_DAG_ID",
"dag_run_id": "TEST_DAG_RUN_ID_2",
"end_date": None,
"state": "running",
"execution_date": self.default_time_2,
"logical_date": self.default_time_2,
"external_trigger": True,
"start_date": self.default_time,
"conf": {},
"data_interval_end": None,
"data_interval_start": None,
"last_scheduling_decision": None,
"run_type": "manual",
"note": None,
},
],
"total_entries": 2,
}
def test_filter_by_state(self):
self._create_test_dag_run()
self._create_test_dag_run(state="queued", idx_start=3)
response = self.client.post(
"api/v1/dags/~/dagRuns/list",
json={"dag_ids": ["TEST_DAG_ID"], "states": ["running", "queued"]},
environ_overrides={"REMOTE_USER": "test"},
)
assert response.status_code == 200
assert response.json["total_entries"] == 4
assert response.json["dag_runs"][0]["state"] == response.json["dag_runs"][1]["state"] == "running"
assert response.json["dag_runs"][2]["state"] == response.json["dag_runs"][3]["state"] == "queued"
def test_order_by_descending_works(self):
self._create_test_dag_run()
response = self.client.post(
"api/v1/dags/~/dagRuns/list",
json={"dag_ids": ["TEST_DAG_ID"], "order_by": "-dag_run_id"},
environ_overrides={"REMOTE_USER": "test"},
)
assert response.status_code == 200
assert response.json == {
"dag_runs": [
{
"dag_id": "TEST_DAG_ID",
"dag_run_id": "TEST_DAG_RUN_ID_2",
"end_date": None,
"state": "running",
"execution_date": self.default_time_2,
"logical_date": self.default_time_2,
"external_trigger": True,
"start_date": self.default_time,
"conf": {},
"data_interval_end": None,
"data_interval_start": None,
"last_scheduling_decision": None,
"run_type": "manual",
"note": None,
},
{
"dag_id": "TEST_DAG_ID",
"dag_run_id": "TEST_DAG_RUN_ID_1",
"end_date": None,
"state": "running",
"execution_date": self.default_time,
"logical_date": self.default_time,
"external_trigger": True,
"start_date": self.default_time,
"conf": {},
"data_interval_end": None,
"data_interval_start": None,
"last_scheduling_decision": None,
"run_type": "manual",
"note": None,
},
],
"total_entries": 2,
}
def test_order_by_raises_for_invalid_attr(self):
self._create_test_dag_run()
response = self.client.post(
"api/v1/dags/~/dagRuns/list",
json={"dag_ids": ["TEST_DAG_ID"], "order_by": "-dag_ru"},
environ_overrides={"REMOTE_USER": "test"},
)
assert response.status_code == 400
msg = "Ordering with 'dag_ru' is disallowed or the attribute does not exist on the model"
assert response.json["detail"] == msg
def test_should_return_accessible_with_tilde_as_dag_id_and_dag_level_permissions(self):
self._create_test_dag_run(extra_dag=True)
response = self.client.post(
"api/v1/dags/~/dagRuns/list",
json={"dag_ids": []},
environ_overrides={"REMOTE_USER": "test_granular_permissions"},
)
assert response.status_code == 200
assert response.json == {
"dag_runs": [
{
"dag_id": "TEST_DAG_ID",
"dag_run_id": "TEST_DAG_RUN_ID_1",
"end_date": None,
"state": "running",
"execution_date": self.default_time,
"logical_date": self.default_time,
"external_trigger": True,
"start_date": self.default_time,
"conf": {},
"data_interval_end": None,
"data_interval_start": None,
"last_scheduling_decision": None,
"run_type": "manual",
"note": None,
},
{
"dag_id": "TEST_DAG_ID",
"dag_run_id": "TEST_DAG_RUN_ID_2",
"end_date": None,
"state": "running",
"execution_date": self.default_time_2,
"logical_date": self.default_time_2,
"external_trigger": True,
"start_date": self.default_time,
"conf": {},
"data_interval_end": None,
"data_interval_start": None,
"last_scheduling_decision": None,
"run_type": "manual",
"note": None,
},
],
"total_entries": 2,
}
@pytest.mark.parametrize(
"payload, error",
[
(
{"dag_ids": ["TEST_DAG_ID"], "page_offset": -1},
"-1 is less than the minimum of 0 - 'page_offset'",
),
({"dag_ids": ["TEST_DAG_ID"], "page_limit": 0}, "0 is less than the minimum of 1 - 'page_limit'"),
({"dag_ids": "TEST_DAG_ID"}, "'TEST_DAG_ID' is not of type 'array' - 'dag_ids'"),
({"start_date_gte": "2020-06-12T18"}, "'2020-06-12T18' is not a 'date-time' - 'start_date_gte'"),
],
)
def test_payload_validation(self, payload, error):
self._create_test_dag_run()
response = self.client.post(
"api/v1/dags/~/dagRuns/list", json=payload, environ_overrides={"REMOTE_USER": "test"}
)
assert response.status_code == 400
assert response.json.get("detail") == error
def test_should_raises_401_unauthenticated(self):
self._create_test_dag_run()
response = self.client.post("api/v1/dags/~/dagRuns/list", json={"dag_ids": ["TEST_DAG_ID"]})
assert_401(response)
class TestGetDagRunBatchPagination(TestDagRunEndpoint):
@pytest.mark.parametrize(
"payload, expected_dag_run_ids",
[
({"page_limit": 1}, ["TEST_DAG_RUN_ID1"]),
({"page_limit": 2}, ["TEST_DAG_RUN_ID1", "TEST_DAG_RUN_ID2"]),
(
{"page_offset": 5},
[
"TEST_DAG_RUN_ID6",
"TEST_DAG_RUN_ID7",
"TEST_DAG_RUN_ID8",
"TEST_DAG_RUN_ID9",
"TEST_DAG_RUN_ID10",
],
),
(
{"page_offset": 0},
[
"TEST_DAG_RUN_ID1",
"TEST_DAG_RUN_ID2",
"TEST_DAG_RUN_ID3",
"TEST_DAG_RUN_ID4",
"TEST_DAG_RUN_ID5",
"TEST_DAG_RUN_ID6",
"TEST_DAG_RUN_ID7",
"TEST_DAG_RUN_ID8",
"TEST_DAG_RUN_ID9",
"TEST_DAG_RUN_ID10",
],
),
({"page_offset": 5, "page_limit": 1}, ["TEST_DAG_RUN_ID6"]),
({"page_offset": 1, "page_limit": 1}, ["TEST_DAG_RUN_ID2"]),
(
{"page_offset": 2, "page_limit": 2},
["TEST_DAG_RUN_ID3", "TEST_DAG_RUN_ID4"],
),
],
)
def test_handle_limit_and_offset(self, payload, expected_dag_run_ids):
self._create_dag_runs(10)
response = self.client.post(
"api/v1/dags/~/dagRuns/list", json=payload, environ_overrides={"REMOTE_USER": "test"}
)
assert response.status_code == 200
assert response.json["total_entries"] == 10
dag_run_ids = [dag_run["dag_run_id"] for dag_run in response.json["dag_runs"]]
assert dag_run_ids == expected_dag_run_ids
def test_should_respect_page_size_limit(self):
self._create_dag_runs(200)
response = self.client.post(
"api/v1/dags/~/dagRuns/list", json={}, environ_overrides={"REMOTE_USER": "test"}
)
assert response.status_code == 200
assert response.json["total_entries"] == 200
assert len(response.json["dag_runs"]) == 100 # default is 100
def _create_dag_runs(self, count):
dag_runs = [
DagRun(
dag_id="TEST_DAG_ID",
run_id="TEST_DAG_RUN_ID" + str(i),
state="running",
run_type=DagRunType.MANUAL,
execution_date=timezone.parse(self.default_time) + timedelta(minutes=i),
start_date=timezone.parse(self.default_time),
external_trigger=True,
)
for i in range(1, count + 1)
]
dag = DagModel(dag_id="TEST_DAG_ID")
with create_session() as session:
session.add_all(dag_runs)
session.add(dag)
class TestGetDagRunBatchDateFilters(TestDagRunEndpoint):
@pytest.mark.parametrize(
"payload, expected_dag_run_ids",
[
(
{"start_date_gte": "2020-06-18T18:00:00+00:00"},
["TEST_START_EXEC_DAY_18", "TEST_START_EXEC_DAY_19"],
),
(
{"start_date_lte": "2020-06-11T18:00:00+00:00"},
["TEST_START_EXEC_DAY_10", "TEST_START_EXEC_DAY_11"],
),
(
{"start_date_lte": "2020-06-15T18:00:00+00:00", "start_date_gte": "2020-06-12T18:00:00Z"},
[
"TEST_START_EXEC_DAY_12",
"TEST_START_EXEC_DAY_13",
"TEST_START_EXEC_DAY_14",
"TEST_START_EXEC_DAY_15",
],
),
(
{"execution_date_lte": "2020-06-13T18:00:00+00:00"},
[
"TEST_START_EXEC_DAY_10",
"TEST_START_EXEC_DAY_11",
"TEST_START_EXEC_DAY_12",
"TEST_START_EXEC_DAY_13",
],
),
(
{"execution_date_gte": "2020-06-16T18:00:00+00:00"},
[
"TEST_START_EXEC_DAY_16",
"TEST_START_EXEC_DAY_17",
"TEST_START_EXEC_DAY_18",
"TEST_START_EXEC_DAY_19",
],
),
],
)
def test_date_filters_gte_and_lte(self, payload, expected_dag_run_ids):
self._create_dag_runs()
response = self.client.post(
"api/v1/dags/~/dagRuns/list", json=payload, environ_overrides={"REMOTE_USER": "test"}
)
assert response.status_code == 200
assert response.json["total_entries"] == len(expected_dag_run_ids)
dag_run_ids = [dag_run["dag_run_id"] for dag_run in response.json["dag_runs"]]
assert dag_run_ids == expected_dag_run_ids
def _create_dag_runs(self):
dates = [
"2020-06-10T18:00:00+00:00",
"2020-06-11T18:00:00+00:00",
"2020-06-12T18:00:00+00:00",
"2020-06-13T18:00:00+00:00",
"2020-06-14T18:00:00+00:00",
"2020-06-15T18:00:00Z",
"2020-06-16T18:00:00Z",
"2020-06-17T18:00:00Z",
"2020-06-18T18:00:00Z",
"2020-06-19T18:00:00Z",
]
dag = DagModel(dag_id="TEST_DAG_ID")
dag_runs = [
DagRun(
dag_id="TEST_DAG_ID",
run_id="TEST_START_EXEC_DAY_1" + str(i),
run_type=DagRunType.MANUAL,
execution_date=timezone.parse(dates[i]),
start_date=timezone.parse(dates[i]),
external_trigger=True,
state="success",
)
for i in range(len(dates))
]
with create_session() as session:
session.add_all(dag_runs)
session.add(dag)
return dag_runs
@pytest.mark.parametrize(
"payload, expected_response",
[
(
{"execution_date_gte": "2020-11-09T16:25:56.939143"},
"'2020-11-09T16:25:56.939143' is not a 'date-time' - 'execution_date_gte'",
),
(
{"start_date_gte": "2020-06-18T16:25:56.939143"},
"'2020-06-18T16:25:56.939143' is not a 'date-time' - 'start_date_gte'",
),
(
{"start_date_lte": "2020-06-18T18:00:00.564434"},
"'2020-06-18T18:00:00.564434' is not a 'date-time' - 'start_date_lte'",
),
(
{"start_date_lte": "2020-06-15T18:00:00.653434", "start_date_gte": "2020-06-12T18:00.343534"},
"'2020-06-12T18:00.343534' is not a 'date-time' - 'start_date_gte'",
),
(
{"execution_date_lte": "2020-06-13T18:00:00.353454"},
"'2020-06-13T18:00:00.353454' is not a 'date-time' - 'execution_date_lte'",
),
(
{"execution_date_gte": "2020-06-16T18:00:00.676443"},
"'2020-06-16T18:00:00.676443' is not a 'date-time' - 'execution_date_gte'",
),
],
)
def test_naive_date_filters_raises_400(self, payload, expected_response):
self._create_dag_runs()
response = self.client.post(
"api/v1/dags/~/dagRuns/list", json=payload, environ_overrides={"REMOTE_USER": "test"}
)
assert response.status_code == 400
assert response.json["detail"] == expected_response
@pytest.mark.parametrize(
"payload, expected_dag_run_ids",
[
(
{"end_date_gte": f"{(timezone.utcnow() + timedelta(days=1)).isoformat()}"},
[],
),
(
{"end_date_lte": f"{(timezone.utcnow() + timedelta(days=1)).isoformat()}"},
["TEST_DAG_RUN_ID_1", "TEST_DAG_RUN_ID_2"],
),
],
)
def test_end_date_gte_lte(self, payload, expected_dag_run_ids):
self._create_test_dag_run("success") # state==success, then end date is today
response = self.client.post(
"api/v1/dags/~/dagRuns/list", json=payload, environ_overrides={"REMOTE_USER": "test"}
)
assert response.status_code == 200
assert response.json["total_entries"] == len(expected_dag_run_ids)
dag_run_ids = [dag_run["dag_run_id"] for dag_run in response.json["dag_runs"] if dag_run]
assert dag_run_ids == expected_dag_run_ids
class TestPostDagRun(TestDagRunEndpoint):
@pytest.mark.parametrize("logical_date_field_name", ["execution_date", "logical_date"])
@pytest.mark.parametrize(
"dag_run_id, logical_date, note",
[
pytest.param("TEST_DAG_RUN", "2020-06-11T18:00:00+00:00", "test-note", id="all-present"),
pytest.param(None, "2020-06-11T18:00:00+00:00", None, id="only-date"),
pytest.param(None, None, None, id="all-missing"),
],
)
def test_should_respond_200(self, session, logical_date_field_name, dag_run_id, logical_date, note):
self._create_dag("TEST_DAG_ID")
# We'll patch airflow.utils.timezone.utcnow to always return this so we
# can check the returned dates.
fixed_now = timezone.utcnow()
request_json = {}
if logical_date is not None:
request_json[logical_date_field_name] = logical_date
if dag_run_id is not None:
request_json["dag_run_id"] = dag_run_id
request_json["note"] = note
with mock.patch("airflow.utils.timezone.utcnow", lambda: fixed_now):
response = self.client.post(
"api/v1/dags/TEST_DAG_ID/dagRuns",
json=request_json,
environ_overrides={"REMOTE_USER": "test"},
)
assert response.status_code == 200
if logical_date is None:
expected_logical_date = fixed_now.isoformat()
else:
expected_logical_date = logical_date
if dag_run_id is None:
expected_dag_run_id = f"manual__{expected_logical_date}"
else:
expected_dag_run_id = dag_run_id
assert response.json == {
"conf": {},
"dag_id": "TEST_DAG_ID",
"dag_run_id": expected_dag_run_id,
"end_date": None,
"execution_date": expected_logical_date,
"logical_date": expected_logical_date,
"external_trigger": True,
"start_date": None,
"state": "queued",
"data_interval_end": expected_logical_date,
"data_interval_start": expected_logical_date,
"last_scheduling_decision": None,
"run_type": "manual",
"note": note,
}
_check_last_log(session, dag_id="TEST_DAG_ID", event="dag_run.create", execution_date=None)
def test_should_respond_404_if_a_dag_is_inactive(self, session):
dm = self._create_dag("TEST_INACTIVE_DAG_ID")
dm.is_active = False
session.add(dm)
session.flush()
response = self.client.post(
"api/v1/dags/TEST_INACTIVE_DAG_ID/dagRuns",
json={},
environ_overrides={"REMOTE_USER": "test"},
)
assert response.status_code == 404
def test_should_respond_400_if_a_dag_has_import_errors(self, session):
"""Test that if a dagmodel has import errors, dags won't be triggered"""
dm = self._create_dag("TEST_DAG_ID")
dm.has_import_errors = True
session.add(dm)
session.flush()
response = self.client.post(
"api/v1/dags/TEST_DAG_ID/dagRuns",
json={},
environ_overrides={"REMOTE_USER": "test"},
)
assert {
"detail": "DAG with dag_id: 'TEST_DAG_ID' has import errors",
"status": 400,
"title": "DAG cannot be triggered",
"type": EXCEPTIONS_LINK_MAP[400],
} == response.json
def test_should_response_200_for_matching_execution_date_logical_date(self):
execution_date = "2020-11-10T08:25:56.939143+00:00"
logical_date = "2020-11-10T08:25:56.939143+00:00"
self._create_dag("TEST_DAG_ID")
response = self.client.post(
"api/v1/dags/TEST_DAG_ID/dagRuns",
json={
"execution_date": execution_date,
"logical_date": logical_date,
},
environ_overrides={"REMOTE_USER": "test"},
)
dag_run_id = f"manual__{logical_date}"
assert response.status_code == 200
assert response.json == {
"conf": {},
"dag_id": "TEST_DAG_ID",
"dag_run_id": dag_run_id,
"end_date": None,
"execution_date": execution_date,
"logical_date": logical_date,
"external_trigger": True,
"start_date": None,
"state": "queued",
"data_interval_end": logical_date,
"data_interval_start": logical_date,
"last_scheduling_decision": None,
"run_type": "manual",
"note": None,
}
def test_should_response_400_for_conflicting_execution_date_logical_date(self):
execution_date = "2020-11-10T08:25:56.939143+00:00"
logical_date = "2020-11-11T08:25:56.939143+00:00"
self._create_dag("TEST_DAG_ID")
response = self.client.post(
"api/v1/dags/TEST_DAG_ID/dagRuns",
json={"execution_date": execution_date, "logical_date": logical_date},
environ_overrides={"REMOTE_USER": "test"},
)
assert response.status_code == 400
assert response.json["title"] == "logical_date conflicts with execution_date"
assert response.json["detail"] == (f"'{logical_date}' != '{execution_date}'")
@pytest.mark.parametrize(
"data, expected",
[
(
{"execution_date": "2020-11-10T08:25:56.939143"},
"'2020-11-10T08:25:56.939143' is not a 'date-time' - 'execution_date'",
),
(
{"execution_date": "2020-11-10T08:25:56P"},
"'2020-11-10T08:25:56P' is not a 'date-time' - 'execution_date'",
),
(
{"logical_date": "2020-11-10T08:25:56.939143"},
"'2020-11-10T08:25:56.939143' is not a 'date-time' - 'logical_date'",
),
(
{"logical_date": "2020-11-10T08:25:56P"},
"'2020-11-10T08:25:56P' is not a 'date-time' - 'logical_date'",
),
],
)
def test_should_response_400_for_naive_datetime_and_bad_datetime(self, data, expected):
self._create_dag("TEST_DAG_ID")
response = self.client.post(
"api/v1/dags/TEST_DAG_ID/dagRuns", json=data, environ_overrides={"REMOTE_USER": "test"}
)
assert response.status_code == 400
assert response.json["detail"] == expected
@pytest.mark.parametrize(
"data, expected",
[
(
{
"dag_run_id": "TEST_DAG_RUN",
"execution_date": "2020-06-11T18:00:00+00:00",
"conf": "some string",
},
"'some string' is not of type 'object' - 'conf'",
)
],
)
def test_should_response_400_for_non_dict_dagrun_conf(self, data, expected):
self._create_dag("TEST_DAG_ID")
response = self.client.post(
"api/v1/dags/TEST_DAG_ID/dagRuns", json=data, environ_overrides={"REMOTE_USER": "test"}
)
assert response.status_code == 400
assert response.json["detail"] == expected
def test_response_404(self):
response = self.client.post(
"api/v1/dags/TEST_DAG_ID/dagRuns",
json={"dag_run_id": "TEST_DAG_RUN", "execution_date": self.default_time},
environ_overrides={"REMOTE_USER": "test"},
)
assert response.status_code == 404
assert {
"detail": "DAG with dag_id: 'TEST_DAG_ID' not found",
"status": 404,
"title": "DAG not found",
"type": EXCEPTIONS_LINK_MAP[404],
} == response.json
@pytest.mark.parametrize(
"url, request_json, expected_response",
[
pytest.param(
"api/v1/dags/TEST_DAG_ID/dagRuns",
{
"start_date": "2020-06-11T18:00:00+00:00",
"execution_date": "2020-06-12T18:00:00+00:00",
},
{
"detail": "Property is read-only - 'start_date'",
"status": 400,
"title": "Bad Request",
"type": EXCEPTIONS_LINK_MAP[400],
},
id="start_date in request json",
),
pytest.param(
"api/v1/dags/TEST_DAG_ID/dagRuns",
{"state": "failed", "execution_date": "2020-06-12T18:00:00+00:00"},
{
"detail": "Property is read-only - 'state'",
"status": 400,
"title": "Bad Request",
"type": EXCEPTIONS_LINK_MAP[400],
},
id="state in request json",
),
],
)
def test_response_400(self, url, request_json, expected_response):
self._create_dag("TEST_DAG_ID")
response = self.client.post(url, json=request_json, environ_overrides={"REMOTE_USER": "test"})
assert response.status_code == 400, response.data
assert expected_response == response.json
def test_response_409(self):
self._create_test_dag_run()
response = self.client.post(
"api/v1/dags/TEST_DAG_ID/dagRuns",
json={
"dag_run_id": "TEST_DAG_RUN_ID_1",
"execution_date": self.default_time_3,
},
environ_overrides={"REMOTE_USER": "test"},
)
assert response.status_code == 409, response.data
assert response.json == {
"detail": "DAGRun with DAG ID: 'TEST_DAG_ID' and "
"DAGRun ID: 'TEST_DAG_RUN_ID_1' already exists",
"status": 409,
"title": "Conflict",
"type": EXCEPTIONS_LINK_MAP[409],
}
def test_response_409_when_execution_date_is_same(self):
self._create_test_dag_run()
response = self.client.post(
"api/v1/dags/TEST_DAG_ID/dagRuns",
json={
"dag_run_id": "TEST_DAG_RUN_ID_6",
"execution_date": self.default_time,
},
environ_overrides={"REMOTE_USER": "test"},
)
assert response.status_code == 409, response.data
assert response.json == {
"detail": "DAGRun with DAG ID: 'TEST_DAG_ID' and "
"DAGRun logical date: '2020-06-11 18:00:00+00:00' already exists",
"status": 409,
"title": "Conflict",
"type": EXCEPTIONS_LINK_MAP[409],
}
def test_should_raises_401_unauthenticated(self):
response = self.client.post(
"api/v1/dags/TEST_DAG_ID/dagRuns",
json={
"dag_run_id": "TEST_DAG_RUN_ID_1",
"execution_date": self.default_time,
},
)
assert_401(response)
@pytest.mark.parametrize(
"username",
["test_dag_view_only", "test_view_dags", "test_granular_permissions", "test_no_permissions"],
)
def test_should_raises_403_unauthorized(self, username):
self._create_dag("TEST_DAG_ID")
response = self.client.post(
"api/v1/dags/TEST_DAG_ID/dagRuns",
json={
"dag_run_id": "TEST_DAG_RUN_ID_1",
"execution_date": self.default_time,
},
environ_overrides={"REMOTE_USER": username},
)
assert response.status_code == 403
class TestPatchDagRunState(TestDagRunEndpoint):
@pytest.mark.parametrize("state", ["failed", "success", "queued"])
@pytest.mark.parametrize("run_type", [state.value for state in DagRunType])
def test_should_respond_200(self, state, run_type, dag_maker, session):
dag_id = "TEST_DAG_ID"
dag_run_id = "TEST_DAG_RUN_ID"
with dag_maker(dag_id) as dag:
task = EmptyOperator(task_id="task_id", dag=dag)
self.app.dag_bag.bag_dag(dag, root_dag=dag)
dr = dag_maker.create_dagrun(run_id=dag_run_id, run_type=run_type)
ti = dr.get_task_instance(task_id="task_id")
ti.task = task
ti.state = State.RUNNING
session.merge(ti)
session.commit()
request_json = {"state": state}
response = self.client.patch(
f"api/v1/dags/{dag_id}/dagRuns/{dag_run_id}",
json=request_json,
environ_overrides={"REMOTE_USER": "test"},
)
if state != "queued":
ti.refresh_from_db()
assert ti.state == state
dr = session.query(DagRun).filter(DagRun.run_id == dr.run_id).first()
assert response.status_code == 200
assert response.json == {
"conf": {},
"dag_id": dag_id,
"dag_run_id": dag_run_id,
"end_date": dr.end_date.isoformat(),
"execution_date": dr.execution_date.isoformat(),
"external_trigger": False,
"logical_date": dr.execution_date.isoformat(),
"start_date": dr.start_date.isoformat(),
"state": state,
"data_interval_start": dr.data_interval_start.isoformat(),
"data_interval_end": dr.data_interval_end.isoformat(),
"last_scheduling_decision": None,
"run_type": run_type,
"note": None,
}
@pytest.mark.parametrize("invalid_state", ["running"])
@time_machine.travel(TestDagRunEndpoint.default_time)
def test_should_response_400_for_non_existing_dag_run_state(self, invalid_state, dag_maker):
dag_id = "TEST_DAG_ID"
dag_run_id = "TEST_DAG_RUN_ID"
with dag_maker(dag_id):
EmptyOperator(task_id="task_id")
dag_maker.create_dagrun(run_id=dag_run_id)
request_json = {"state": invalid_state}
response = self.client.patch(
"api/v1/dags/TEST_DAG_ID/dagRuns/TEST_DAG_RUN_ID_1",
json=request_json,
environ_overrides={"REMOTE_USER": "test"},
)
assert response.status_code == 400
assert response.json == {
"detail": f"'{invalid_state}' is not one of ['success', 'failed', 'queued'] - 'state'",
"status": 400,
"title": "Bad Request",
"type": EXCEPTIONS_LINK_MAP[400],
}
def test_should_raises_401_unauthenticated(self, session):
response = self.client.patch(
"api/v1/dags/TEST_DAG_ID/dagRuns/TEST_DAG_RUN_ID_1",
json={
"state": "success",
},
)
assert_401(response)
def test_should_raise_403_forbidden(self):
response = self.client.patch(
"api/v1/dags/TEST_DAG_ID/dagRuns/TEST_DAG_RUN_ID_1",
json={
"state": "success",
},
environ_overrides={"REMOTE_USER": "test_no_permissions"},
)
assert response.status_code == 403
def test_should_respond_404(self):
response = self.client.patch(
"api/v1/dags/INVALID_DAG_ID/dagRuns/TEST_DAG_RUN_ID_1",
json={
"state": "success",
},
environ_overrides={"REMOTE_USER": "test"},
)
assert response.status_code == 404
class TestClearDagRun(TestDagRunEndpoint):
def test_should_respond_200(self, dag_maker, session):
dag_id = "TEST_DAG_ID"
dag_run_id = "TEST_DAG_RUN_ID"
with dag_maker(dag_id) as dag:
task = EmptyOperator(task_id="task_id", dag=dag)
self.app.dag_bag.bag_dag(dag, root_dag=dag)
dr = dag_maker.create_dagrun(run_id=dag_run_id, state=DagRunState.FAILED)
ti = dr.get_task_instance(task_id="task_id")
ti.task = task
ti.state = State.SUCCESS
session.merge(ti)
session.commit()
request_json = {"dry_run": False}
response = self.client.post(
f"api/v1/dags/{dag_id}/dagRuns/{dag_run_id}/clear",
json=request_json,
environ_overrides={"REMOTE_USER": "test"},
)
dr = session.query(DagRun).filter(DagRun.run_id == dr.run_id).first()
assert response.status_code == 200
assert response.json == {
"conf": {},
"dag_id": dag_id,
"dag_run_id": dag_run_id,
"end_date": None,
"execution_date": dr.execution_date.isoformat(),
"external_trigger": False,
"logical_date": dr.logical_date.isoformat(),
"start_date": None,
"state": "queued",
"data_interval_start": dr.data_interval_start.isoformat(),
"data_interval_end": dr.data_interval_end.isoformat(),
"last_scheduling_decision": None,
"run_type": dr.run_type,
"note": None,
}
ti.refresh_from_db()
assert ti.state is None
def test_dry_run(self, dag_maker, session):
"""Test that dry_run being True returns TaskInstances without clearing DagRun"""
dag_id = "TEST_DAG_ID"
dag_run_id = "TEST_DAG_RUN_ID"
with dag_maker(dag_id) as dag:
task = EmptyOperator(task_id="task_id", dag=dag)
self.app.dag_bag.bag_dag(dag, root_dag=dag)
dr = dag_maker.create_dagrun(run_id=dag_run_id)
ti = dr.get_task_instance(task_id="task_id")
ti.task = task
ti.state = State.SUCCESS
session.merge(ti)
session.commit()
request_json = {"dry_run": True}
response = self.client.post(
f"api/v1/dags/{dag_id}/dagRuns/{dag_run_id}/clear",
json=request_json,
environ_overrides={"REMOTE_USER": "test"},
)
assert response.status_code == 200
assert response.json == {
"task_instances": [
{
"dag_id": dag_id,
"dag_run_id": dag_run_id,
"execution_date": dr.execution_date.isoformat(),
"task_id": "task_id",
}
]
}
ti.refresh_from_db()
assert ti.state == State.SUCCESS
dr = session.query(DagRun).filter(DagRun.run_id == dr.run_id).first()
assert dr.state == "running"
def test_should_raises_401_unauthenticated(self, session):
response = self.client.post(
"api/v1/dags/TEST_DAG_ID/dagRuns/TEST_DAG_RUN_ID_1/clear",
json={
"dry_run": True,
},
)
assert_401(response)
def test_should_raise_403_forbidden(self):
response = self.client.post(
"api/v1/dags/TEST_DAG_ID/dagRuns/TEST_DAG_RUN_ID_1/clear",
json={
"dry_run": True,
},
environ_overrides={"REMOTE_USER": "test_no_permissions"},
)
assert response.status_code == 403
def test_should_respond_404(self):
response = self.client.post(
"api/v1/dags/INVALID_DAG_ID/dagRuns/TEST_DAG_RUN_ID_1/clear",
json={
"dry_run": True,
},
environ_overrides={"REMOTE_USER": "test"},
)
assert response.status_code == 404
@pytest.mark.need_serialized_dag
class TestGetDagRunDatasetTriggerEvents(TestDagRunEndpoint):
def test_should_respond_200(self, dag_maker, session):
dataset1 = Dataset(uri="ds1")
with dag_maker(dag_id="source_dag", start_date=timezone.utcnow(), session=session):
EmptyOperator(task_id="task", outlets=[dataset1])
dr = dag_maker.create_dagrun()
ti = dr.task_instances[0]
ds1_id = session.query(DatasetModel.id).filter_by(uri=dataset1.uri).scalar()
event = DatasetEvent(
dataset_id=ds1_id,
source_task_id=ti.task_id,
source_dag_id=ti.dag_id,
source_run_id=ti.run_id,
source_map_index=ti.map_index,
)
session.add(event)
with dag_maker(dag_id="TEST_DAG_ID", start_date=timezone.utcnow(), session=session):
pass
dr = dag_maker.create_dagrun(run_id="TEST_DAG_RUN_ID", run_type=DagRunType.DATASET_TRIGGERED)
dr.consumed_dataset_events.append(event)
session.commit()
assert event.timestamp
response = self.client.get(
"api/v1/dags/TEST_DAG_ID/dagRuns/TEST_DAG_RUN_ID/upstreamDatasetEvents",
environ_overrides={"REMOTE_USER": "test"},
)
assert response.status_code == 200
expected_response = {
"dataset_events": [
{
"timestamp": event.timestamp.isoformat(),
"dataset_id": ds1_id,
"dataset_uri": dataset1.uri,
"extra": {},
"id": event.id,
"source_dag_id": ti.dag_id,
"source_map_index": ti.map_index,
"source_run_id": ti.run_id,
"source_task_id": ti.task_id,
"created_dagruns": [
{
"dag_id": "TEST_DAG_ID",
"dag_run_id": "TEST_DAG_RUN_ID",
"data_interval_end": dr.data_interval_end.isoformat(),
"data_interval_start": dr.data_interval_start.isoformat(),
"end_date": None,
"logical_date": dr.logical_date.isoformat(),
"start_date": dr.start_date.isoformat(),
"state": "running",
}
],
}
],
"total_entries": 1,
}
assert response.json == expected_response
def test_should_respond_404(self):
response = self.client.get(
"api/v1/dags/invalid-id/dagRuns/invalid-id/upstreamDatasetEvents",
environ_overrides={"REMOTE_USER": "test"},
)
assert response.status_code == 404
expected_resp = {
"detail": "DAGRun with DAG ID: 'invalid-id' and DagRun ID: 'invalid-id' not found",
"status": 404,
"title": "DAGRun not found",
"type": EXCEPTIONS_LINK_MAP[404],
}
assert expected_resp == response.json
def test_should_raises_401_unauthenticated(self, session):
dagrun_model = DagRun(
dag_id="TEST_DAG_ID",
run_id="TEST_DAG_RUN_ID",
run_type=DagRunType.MANUAL,
execution_date=timezone.parse(self.default_time),
start_date=timezone.parse(self.default_time),
external_trigger=True,
)
session.add(dagrun_model)
session.commit()
response = self.client.get("api/v1/dags/TEST_DAG_ID/dagRuns/TEST_DAG_RUN_ID/upstreamDatasetEvents")
assert_401(response)
class TestSetDagRunNote(TestDagRunEndpoint):
def test_should_respond_200(self, dag_maker, session):
dag_runs: list[DagRun] = self._create_test_dag_run("success")
session.add_all(dag_runs)
session.commit()
created_dr: DagRun = dag_runs[0]
new_note_value = "My super cool DagRun notes"
response = self.client.patch(
f"api/v1/dags/{created_dr.dag_id}/dagRuns/{created_dr.run_id}/setNote",
json={"note": new_note_value},
environ_overrides={"REMOTE_USER": "test"},
)
dr = session.query(DagRun).filter(DagRun.run_id == created_dr.run_id).first()
assert response.status_code == 200, response.text
assert dr.note == new_note_value
assert response.json == {
"conf": {},
"dag_id": dr.dag_id,
"dag_run_id": dr.run_id,
"end_date": dr.end_date.isoformat(),
"execution_date": self.default_time,
"external_trigger": True,
"logical_date": self.default_time,
"start_date": self.default_time,
"state": "success",
"data_interval_start": None,
"data_interval_end": None,
"last_scheduling_decision": None,
"run_type": dr.run_type,
"note": new_note_value,
}
assert dr.dag_run_note.user_id is not None
def test_should_raises_401_unauthenticated(self, session):
response = self.client.patch(
"api/v1/dags/TEST_DAG_ID/dagRuns/TEST_DAG_RUN_ID_1/setNote",
json={"note": "I am setting a note while being unauthenticated."},
)
assert_401(response)
def test_should_raise_403_forbidden(self):
response = self.client.patch(
"api/v1/dags/TEST_DAG_ID/dagRuns/TEST_DAG_RUN_ID_1/setNote",
json={"note": "I am setting a note without the proper permissions."},
environ_overrides={"REMOTE_USER": "test_no_permissions"},
)
assert response.status_code == 403
def test_should_respond_404(self):
response = self.client.patch(
"api/v1/dags/INVALID_DAG_ID/dagRuns/TEST_DAG_RUN_ID_1/setNote",
json={"note": "I am setting a note on a DAG that doesn't exist."},
environ_overrides={"REMOTE_USER": "test"},
)
assert response.status_code == 404