diff --git a/diracx-routers/tests/jobs/conftest.py b/diracx-routers/tests/jobs/conftest.py new file mode 100644 index 00000000..bb430756 --- /dev/null +++ b/diracx-routers/tests/jobs/conftest.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +import pytest +from fastapi.testclient import TestClient + +TEST_JDL = """ + Arguments = "jobDescription.xml -o LogLevel=INFO"; + Executable = "dirac-jobexec"; + JobGroup = jobGroup; + JobName = jobName; + JobType = User; + LogLevel = INFO; + OutputSandbox = + { + Script1_CodeOutput.log, + std.err, + std.out + }; + Priority = 1; + Site = ANY; + StdError = std.err; + StdOutput = std.out; +""" + +TEST_PARAMETRIC_JDL = """ +Arguments = "jobDescription.xml -o LogLevel=DEBUG -p JOB_ID=%(JOB_ID)s -p InputData=%(InputData)s"; + Executable = "dirac-jobexec"; + InputData = %(InputData)s; + InputSandbox = jobDescription.xml; + JOB_ID = %(JOB_ID)s; + JobName = Name; + JobType = User; + LogLevel = DEBUG; + OutputSandbox = + { + Script1_CodeOutput.log, + std.err, + std.out + }; + Parameters = 3; + Parameters.InputData = + { + {/lhcb/data/data1, + /lhcb/data/data2}, + {/lhcb/data/data3, + /lhcb/data/data4}, + {/lhcb/data/data5, + /lhcb/data/data6} + }; + Parameters.JOB_ID = + { + 1, + 2, + 3 + }; + Priority = 1; + StdError = std.err; + StdOutput = std.out; +""" + + +@pytest.fixture +def normal_user_client(client_factory): + with client_factory.normal_user() as client: + yield client + + +@pytest.fixture +def admin_user_client(client_factory): + with client_factory.admin_user() as client: + yield client + + +@pytest.fixture +def valid_job_id(normal_user_client: TestClient): + job_definitions = [TEST_JDL] + r = normal_user_client.post("/api/jobs/jdl", json=job_definitions) + assert r.status_code == 200, r.json() + assert len(r.json()) == 1 + return r.json()[0]["JobID"] + + +@pytest.fixture +def valid_job_ids(normal_user_client: TestClient): + job_definitions = [TEST_PARAMETRIC_JDL] + r = normal_user_client.post("/api/jobs/jdl", json=job_definitions) + assert r.status_code == 200, r.json() + assert len(r.json()) == 3 + return sorted([job_dict["JobID"] for job_dict in r.json()]) + + +@pytest.fixture +def invalid_job_id(): + return 999999996 + + +@pytest.fixture +def invalid_job_ids(): + return [999999997, 999999998, 999999999] diff --git a/diracx-routers/tests/jobs/test_query.py b/diracx-routers/tests/jobs/test_query.py new file mode 100644 index 00000000..abc924af --- /dev/null +++ b/diracx-routers/tests/jobs/test_query.py @@ -0,0 +1,819 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from http import HTTPStatus + +import pytest +from fastapi.testclient import TestClient +from freezegun import freeze_time + +from diracx.core.models import JobStatus + +from .conftest import TEST_JDL, TEST_PARAMETRIC_JDL + +TEST_LARGE_PARAMETRIC_JDL = """ + Executable = "echo"; + Arguments = "%s"; + JobName = "Test_%n"; + Parameters = 100; + ParameterStart = 1; +""" + +TEST_MALFORMED_JDL = """ +[ + 'Executable = "echo";' +] +""" + +pytestmark = pytest.mark.enabled_dependencies( + [ + "AuthSettings", + "JobDB", + "JobLoggingDB", + "ConfigSource", + "TaskQueueDB", + "SandboxMetadataDB", + "WMSAccessPolicy", + "DevelopmentSettings", + "JobParametersDB", + ] +) + + +def test_insert_and_list_parametric_jobs(normal_user_client): + job_definitions = [TEST_PARAMETRIC_JDL] + r = normal_user_client.post("/api/jobs/jdl", json=job_definitions) + assert r.status_code == 200, r.json() + assert len(r.json()) == 3 # Parameters.JOB_ID is 3 + + submitted_job_ids = sorted([job_dict["JobID"] for job_dict in r.json()]) + + r = normal_user_client.post("/api/jobs/search") + assert r.status_code == 200, r.json() + + listed_jobs = r.json() + + assert "Content-Range" not in r.headers + + assert len(listed_jobs) == 3 # Parameters.JOB_ID is 3 + + assert submitted_job_ids == sorted([job_dict["JobID"] for job_dict in listed_jobs]) + + +@pytest.mark.parametrize( + "job_definitions", + [ + [TEST_JDL], + [TEST_JDL for _ in range(2)], + [TEST_JDL for _ in range(10)], + ], +) +def test_insert_and_list_bulk_jobs(job_definitions, normal_user_client): + r = normal_user_client.post("/api/jobs/jdl", json=job_definitions) + assert r.status_code == 200, r.json() + assert len(r.json()) == len(job_definitions) + + submitted_job_ids = sorted([job_dict["JobID"] for job_dict in r.json()]) + + r = normal_user_client.post("/api/jobs/search") + assert r.status_code == 200, r.json() + + listed_jobs = r.json() + + assert "Content-Range" not in r.headers + + assert len(listed_jobs) == len(job_definitions) + + assert submitted_job_ids == sorted([job_dict["JobID"] for job_dict in listed_jobs]) + + +def test_insert_and_search(normal_user_client): + """Test inserting a job and then searching for it.""" + # job_definitions = [TEST_JDL%(normal_user_client.dirac_token_payload)] + job_definitions = [TEST_JDL] + r = normal_user_client.post("/api/jobs/jdl", json=job_definitions) + listed_jobs = r.json() + assert r.status_code == 200, listed_jobs + assert len(listed_jobs) == len(job_definitions) + + submitted_job_ids = sorted([job_dict["JobID"] for job_dict in r.json()]) + + # Test /jobs/search + # 1. Search for all jobs + r = normal_user_client.post("/api/jobs/search") + listed_jobs = r.json() + assert r.status_code == 200, listed_jobs + assert [x["JobID"] for x in listed_jobs] == submitted_job_ids + assert {x["VerifiedFlag"] for x in listed_jobs} == {True} + + # 2. Search for all jobs with status NEW: should return an empty list + r = normal_user_client.post( + "/api/jobs/search", + json={"search": [{"parameter": "Status", "operator": "eq", "value": "NEW"}]}, + ) + listed_jobs = r.json() + assert r.status_code == 200, listed_jobs + assert listed_jobs == [] + + assert "Content-Range" not in r.headers + + # 3. Search for all jobs with status RECEIVED: should return the submitted jobs + r = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "Status", + "operator": "eq", + "value": JobStatus.RECEIVED.value, + } + ] + }, + ) + listed_jobs = r.json() + assert r.status_code == 200, listed_jobs + assert [x["JobID"] for x in listed_jobs] == submitted_job_ids + + assert "Content-Range" not in r.headers + + # 4. Search for all jobs but just return the JobID and the Status + r = normal_user_client.post( + "/api/jobs/search", json={"parameters": ["JobID", "Status"]} + ) + listed_jobs = r.json() + assert r.status_code == 200, listed_jobs + assert listed_jobs == [ + {"JobID": jid, "Status": JobStatus.RECEIVED.value} for jid in submitted_job_ids + ] + + assert "Content-Range" not in r.headers + + # Test /jobs/summary + r = normal_user_client.post( + "/api/jobs/summary", json={"grouping": ["Status", "OwnerGroup"]} + ) + assert r.status_code == 200, r.json() + + assert r.json() == [ + {"Status": JobStatus.RECEIVED.value, "OwnerGroup": "test_group", "count": 1} + ] + + r = normal_user_client.post( + "/api/jobs/summary", + json={ + "grouping": ["Status"], + "search": [ + { + "parameter": "Status", + "operator": "eq", + "value": JobStatus.RECEIVED.value, + } + ], + }, + ) + assert r.status_code == 200, r.json() + assert r.json() == [{"Status": JobStatus.RECEIVED.value, "count": 1}] + + r = normal_user_client.post( + "/api/jobs/summary", + json={ + "grouping": ["Status"], + "search": [{"parameter": "Status", "operator": "eq", "value": "NEW"}], + }, + ) + assert r.status_code == 200, r.json() + assert r.json() == [] + + +def test_insert_malformed_jdl(normal_user_client): + job_definitions = [TEST_MALFORMED_JDL] + r = normal_user_client.post("/api/jobs/jdl", json=job_definitions) + assert r.status_code == 400, r.json() + + +@freeze_time("2024-01-01T00:00:00.123456Z") +def test_insert_and_search_by_datetime(normal_user_client): + """Test inserting a job and then searching for it. + + Focus on the SubmissionTime parameter. + """ + # job_definitions = [TEST_JDL%(normal_user_client.dirac_token_payload)] + job_definitions = [TEST_JDL] + r = normal_user_client.post("/api/jobs/jdl", json=job_definitions) + listed_jobs = r.json() + assert r.status_code == 200, listed_jobs + assert len(listed_jobs) == len(job_definitions) + + # 1.1 Search for all jobs submitted in 2024 + r = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "SubmissionTime", + "operator": "eq", + "value": "2024", + } + ] + }, + ) + assert r.status_code == 200, r.json() + assert len(r.json()) == 1 + + # 1.2 Search for all jobs submitted before 2024 + r = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "SubmissionTime", + "operator": "lt", + "value": "2024", + } + ] + }, + ) + assert r.status_code == 200, r.json() + assert len(r.json()) == 0 + + # 2.1 Search for all jobs submitted after 2024-01 + r = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "SubmissionTime", + "operator": "gt", + "value": "2024-01", + } + ] + }, + ) + assert r.status_code == 200, r.json() + assert len(r.json()) == 0 + + # 2.2 Search for all jobs submitted before 2024-02 + r = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "SubmissionTime", + "operator": "lt", + "value": "2024-02", + } + ] + }, + ) + assert r.status_code == 200, r.json() + assert len(r.json()) == 1 + + # 3 Search for all jobs submitted during 2024-01-01 + r = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "SubmissionTime", + "operator": "eq", + "value": "2024-01-01", + } + ] + }, + ) + assert r.status_code == 200, r.json() + assert len(r.json()) == 1 + + # 4.1 Search for all jobs submitted during 2024-01-01 00 + r = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "SubmissionTime", + "operator": "eq", + "value": "2024-01-01 00", + } + ] + }, + ) + assert r.status_code == 200, r.json() + assert len(r.json()) == 1 + + # 4.2 Search for all jobs submitted during 2024-01-01T00 (with the 'T' separator) + r = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "SubmissionTime", + "operator": "eq", + "value": "2024-01-01T00", + } + ] + }, + ) + assert r.status_code == 200, r.json() + assert len(r.json()) == 1 + + # 4.3 Search for all jobs not submitted during 2024-01-01 01 + r = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "SubmissionTime", + "operator": "neq", + "value": "2024-01-01 01", + } + ] + }, + ) + assert r.status_code == 200, r.json() + assert len(r.json()) == 1 + + # 5.1 Search for all jobs submitted after 2024-01-01 00:00:00 + r = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "SubmissionTime", + "operator": "gt", + "value": "2024-01-01 00:00:00", + } + ] + }, + ) + assert r.status_code == 200, r.json() + assert len(r.json()) == 0 + + # 5.2 Search for all jobs not submitted on 2024-01-01 00:00:00 + r = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "SubmissionTime", + "operator": "neq", + "value": "2024-01-01 00:00:00", + } + ] + }, + ) + assert r.status_code == 200, r.json() + assert len(r.json()) == 0 + + # 5.3 Search for all jobs submitted on 2024-01-01 00:00:00 + r = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "SubmissionTime", + "operator": "eq", + "value": "2024-01-01 00:00:00", + } + ] + }, + ) + assert r.status_code == 200, r.json() + assert len(r.json()) == 1 + + # 6.1 Search for all jobs submitted on 2024-01-01 00:00:00.123456 + r = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "SubmissionTime", + "operator": "eq", + "value": "2024-01-01 00:00:00.123456", + } + ] + }, + ) + assert r.status_code == 200, r.json() + assert len(r.json()) == 1 + + # 6.2 Search for all jobs submitted on 2024-01-01 00:00:00.123456Z + r = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "SubmissionTime", + "operator": "eq", + "value": "2024-01-01 00:00:00.123456Z", + } + ] + }, + ) + assert r.status_code == 200, r.json() + assert len(r.json()) == 1 + + # 6.3 Search for all jobs submitted on 2024-01-01 00:00:00.123Z + r = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "SubmissionTime", + "operator": "eq", + "value": "2024-01-01 00:00:00.123Z", + } + ] + }, + ) + assert r.status_code == 200, r.json() + assert len(r.json()) == 1 + + +def test_search_distinct(normal_user_client): + """Test that the distinct parameter works as expected.""" + job_definitions = [TEST_JDL, TEST_JDL, TEST_JDL] + r = normal_user_client.post("/api/jobs/jdl", json=job_definitions) + listed_jobs = r.json() + assert r.status_code == 200, listed_jobs + assert len(listed_jobs) == len(job_definitions) + + # Check that distinct collapses identical records when true + r = normal_user_client.post( + "/api/jobs/search", json={"parameters": ["Status"], "distinct": False} + ) + listed_jobs = r.json() + assert r.status_code == 200, listed_jobs + assert len(listed_jobs) > 1 + + assert "Content-Range" not in r.headers + + r = normal_user_client.post( + "/api/jobs/search", json={"parameters": ["Status"], "distinct": True} + ) + listed_jobs = r.json() + assert r.status_code == 200, listed_jobs + assert len(listed_jobs) == 1 + + assert "Content-Range" not in r.headers + + +def test_search_pagination(normal_user_client): + """Test that the pagination works as expected.""" + job_definitions = [TEST_JDL] * 20 + r = normal_user_client.post("/api/jobs/jdl", json=job_definitions) + listed_jobs = r.json() + assert r.status_code == 200, listed_jobs + assert len(listed_jobs) == len(job_definitions) + + # Get the first 20 jobs (all of them) + r = normal_user_client.post("/api/jobs/search", params={"page": 1, "per_page": 20}) + listed_jobs = r.json() + assert r.status_code == 200, listed_jobs + assert len(listed_jobs) == 20 + + assert "Content-Range" not in r.headers + + # Get the first 10 jobs + r = normal_user_client.post("/api/jobs/search", params={"page": 1, "per_page": 10}) + listed_jobs = r.json() + assert r.status_code == 206, listed_jobs + assert len(listed_jobs) == 10 + + assert "Content-Range" in r.headers + assert ( + r.headers["Content-Range"] + == f"jobs 0-{len(listed_jobs) -1}/{len(job_definitions)}" + ) + + # Get the next 10 jobs + r = normal_user_client.post("/api/jobs/search", params={"page": 2, "per_page": 10}) + listed_jobs = r.json() + assert r.status_code == 206, listed_jobs + assert len(listed_jobs) == 10 + + assert "Content-Range" in r.headers + assert ( + r.headers["Content-Range"] + == f"jobs 10-{len(listed_jobs) + 10 - 1}/{len(job_definitions)}" + ) + + # Get an unknown page + r = normal_user_client.post("/api/jobs/search", params={"page": 3, "per_page": 10}) + listed_jobs = r.json() + assert r.status_code == 416, listed_jobs + assert len(listed_jobs) == 0 + + assert "Content-Range" in r.headers + assert r.headers["Content-Range"] == f"jobs */{len(job_definitions)}" + + # Set the per_page parameter to 0 + r = normal_user_client.post("/api/jobs/search", params={"page": 1, "per_page": 0}) + assert r.status_code == 400, r.json() + + # Set the per_page parameter to a negative number + r = normal_user_client.post("/api/jobs/search", params={"page": 1, "per_page": -1}) + assert r.status_code == 400, r.json() + + # Set the page parameter to 0 + r = normal_user_client.post("/api/jobs/search", params={"page": 0, "per_page": 10}) + assert r.status_code == 400, r.json() + + # Set the page parameter to a negative number + r = normal_user_client.post("/api/jobs/search", params={"page": -1, "per_page": 10}) + assert r.status_code == 400, r.json() + + +def test_user_cannot_submit_parametric_jdl_greater_than_max_parametric_jobs( + normal_user_client, +): + """Test that a user cannot submit a parametric JDL greater than the max parametric jobs.""" + job_definitions = [TEST_LARGE_PARAMETRIC_JDL] + res = normal_user_client.post("/api/jobs/jdl", json=job_definitions) + assert res.status_code == HTTPStatus.BAD_REQUEST, res.json() + + +def test_user_cannot_submit_list_of_jdl_greater_than_max_number_of_jobs( + normal_user_client, +): + """Test that a user cannot submit a list of JDL greater than the max number of jobs.""" + job_definitions = [TEST_JDL for _ in range(100)] + res = normal_user_client.post("/api/jobs/jdl", json=job_definitions) + assert res.status_code == HTTPStatus.BAD_REQUEST, res.json() + + +@pytest.mark.parametrize( + "job_definitions", + [[TEST_PARAMETRIC_JDL, TEST_JDL], [TEST_PARAMETRIC_JDL, TEST_PARAMETRIC_JDL]], +) +def test_user_cannot_submit_multiple_jdl_if_at_least_one_of_them_is_parametric( + normal_user_client, job_definitions +): + res = normal_user_client.post("/api/jobs/jdl", json=job_definitions) + assert res.status_code == HTTPStatus.BAD_REQUEST, res.json() + + +def test_user_without_the_normal_user_property_cannot_submit_job(admin_user_client): + pytest.skip( + "AlwaysAllowAccessPolicyCallable is forced in testing, so this test can not actually test this access policy." + ) + res = admin_user_client.post("/api/jobs/jdl", json=[TEST_JDL]) + assert res.status_code == HTTPStatus.FORBIDDEN, res.json() + + +def test_get_job_status(normal_user_client: TestClient, valid_job_id: int): + """Test that the job status is returned correctly.""" + # Act + r = normal_user_client.post( + "/api/jobs/search", + json={ + "parameters": ["JobID", "Status", "MinorStatus", "ApplicationStatus"], + "search": [{"parameter": "JobID", "operator": "eq", "value": valid_job_id}], + }, + ) + + # Assert + assert r.status_code == 200, r.json() + assert len(r.json()) == 1, f"Should only return length-1 list: {r.json()}" + assert r.json()[0]["JobID"] == valid_job_id, "Returned wrong job id" + # TODO: should we return camel case here (and everywhere else) ? + assert r.json()[0]["Status"] == JobStatus.RECEIVED.value + assert r.json()[0]["MinorStatus"] == "Job accepted" + assert r.json()[0]["ApplicationStatus"] == "Unknown" + + +def test_get_status_of_nonexistent_job( + normal_user_client: TestClient, invalid_job_id: int +): + """Test that the job status is returned correctly.""" + # Act + r = normal_user_client.post( + "/api/jobs/search", + json={ + "parameters": ["Status"], + "search": [ + {"parameter": "JobID", "operator": "eq", "value": invalid_job_id} + ], + }, + ) + + # Assert + assert r.status_code == HTTPStatus.OK, r.json() + assert r.json() == [] + + +def test_get_job_status_in_bulk(normal_user_client: TestClient, valid_job_ids: list): + """Test that we can get the status of multiple jobs in one request.""" + # Act + + r = normal_user_client.post( + "/api/jobs/search", + json={ + "parameters": ["JobID", "Status", "MinorStatus", "ApplicationStatus"], + "search": [ + {"parameter": "JobID", "operator": "in", "values": valid_job_ids} + ], + }, + ) + + # Assert + assert r.status_code == 200, r.json() + assert len(r.json()) == 3 # Parameters.JOB_ID is 3 + assert {j["JobID"] for j in r.json()} == set(valid_job_ids) + for job in r.json(): + assert job["JobID"] in valid_job_ids + assert job["Status"] == JobStatus.SUBMITTING.value + assert job["MinorStatus"] == "Bulk transaction confirmation" + assert job["ApplicationStatus"] == "Unknown" + + +async def test_get_job_status_history( + normal_user_client: TestClient, valid_job_id: int +): + # Arrange + r = normal_user_client.post( + "/api/jobs/search", + json={ + "parameters": ["JobID", "Status", "MinorStatus", "ApplicationStatus"], + "search": [{"parameter": "JobID", "operator": "eq", "value": valid_job_id}], + }, + ) + assert r.status_code == 200, r.json() + assert r.json()[0]["Status"] == JobStatus.RECEIVED.value + assert r.json()[0]["MinorStatus"] == "Job accepted" + assert r.json()[0]["ApplicationStatus"] == "Unknown" + + new_status = JobStatus.CHECKING.value + new_minor_status = "JobPath" + before = datetime.now(timezone.utc) + + r = normal_user_client.patch( + "/api/jobs/status", + json={ + valid_job_id: { + datetime.now(tz=timezone.utc).isoformat(): { + "Status": new_status, + "MinorStatus": new_minor_status, + } + } + }, + ) + + after = datetime.now(timezone.utc) + + assert r.status_code == 200, r.json() + assert r.json()["success"][str(valid_job_id)]["Status"] == new_status + assert r.json()["success"][str(valid_job_id)]["MinorStatus"] == new_minor_status + + # Act + r = normal_user_client.post( + "/api/jobs/search", + json={ + "parameters": [ + "JobID", + "Status", + "MinorStatus", + "ApplicationStatus", + "LoggingInfo", + ], + "search": [{"parameter": "JobID", "operator": "eq", "value": valid_job_id}], + }, + ) + # Assert + assert r.status_code == 200, r.json() + assert len(r.json()) == 1 + assert len(r.json()[0]["LoggingInfo"]) == 2 + assert r.json()[0]["LoggingInfo"][0]["Status"] == JobStatus.RECEIVED.value + assert r.json()[0]["LoggingInfo"][0]["MinorStatus"] == "Job accepted" + assert r.json()[0]["LoggingInfo"][0]["ApplicationStatus"] == "Unknown" + assert r.json()[0]["LoggingInfo"][0]["Source"] == "JobManager" + + assert r.json()[0]["LoggingInfo"][1]["Status"] == JobStatus.CHECKING.value + assert r.json()[0]["LoggingInfo"][1]["MinorStatus"] == "JobPath" + assert r.json()[0]["LoggingInfo"][1]["ApplicationStatus"] == "Unknown" + assert ( + before + < datetime.fromisoformat(r.json()[0]["LoggingInfo"][1]["StatusTime"]) + < after + ) + assert r.json()[0]["LoggingInfo"][1]["Source"] == "Unknown" + + +# Test setting job properties + + +def test_set_single_job_properties(normal_user_client: TestClient, valid_job_id: int): + pytest.skip("There seems to be a missing route for this - TODO") + + job_id = str(valid_job_id) + + initial_job_state = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "JobID", + "operator": "eq", + "value": job_id, + } + ] + }, + ).json()[0] + + initial_user_priority = initial_job_state["UserPriority"] + initial_application_status = initial_job_state["ApplicationStatus"] + initial_last_update_time = initial_job_state["LastUpdateTime"] + + # Update just one property + res = normal_user_client.patch( + "/api/jobs/", + json={valid_job_id: {"UserPriority": 2}}, + ) + assert res.status_code == 200, res.json() + + new_job_state = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "JobID", + "operator": "eq", + "value": job_id, + } + ] + }, + ).json()[0] + new_user_priority = new_job_state["UserPriority"] + new_application_status = new_job_state["ApplicationStatus"] + + assert initial_application_status == new_application_status + assert initial_user_priority != new_user_priority + assert new_user_priority == 2 + assert new_job_state["LastUpdateTime"] == initial_last_update_time + + # Update two properties + res = normal_user_client.patch( + f"/api/jobs/{job_id}", + json={"UserPriority": initial_user_priority, "ApplicationStatus": "Crack"}, + params={"update_timestamp": True}, + ) + assert res.status_code == 200, res.json() + + new_job_state = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "JobID", + "operator": "eq", + "value": job_id, + } + ] + }, + ).json()[0] + new_user_priority = new_job_state["UserPriority"] + new_application_status = new_job_state["ApplicationStatus"] + + assert initial_application_status != new_application_status + assert new_application_status == "Crack" + assert initial_user_priority == new_user_priority + assert new_job_state["LastUpdateTime"] != initial_last_update_time + + +def test_set_single_job_properties_non_existing_job( + normal_user_client: TestClient, invalid_job_id: int +): + pytest.skip("There seems to be a missing route for this - TODO") + job_id = str(invalid_job_id) + + res = normal_user_client.patch( + "/api/jobs/", + json={job_id: {"UserPriority": 2}}, + ) + assert res.status_code == HTTPStatus.NOT_FOUND, res.json() + + +def test_get_job_status_history_in_bulk( + normal_user_client: TestClient, valid_job_id: int +): + pytest.skip("TODO: decide whether to keep this") + + # Arrange + r = normal_user_client.get(f"/api/jobs/{valid_job_id}/status") + assert r.status_code == 200, r.json() + assert r.json()[str(valid_job_id)]["Status"] == JobStatus.RECEIVED.value + assert r.json()[str(valid_job_id)]["MinorStatus"] == "Job accepted" + assert r.json()[str(valid_job_id)]["ApplicationStatus"] == "Unknown" + + # Act + r = normal_user_client.get( + "/api/jobs/status/history", params={"job_ids": [valid_job_id]} + ) + + # Assert + assert r.status_code == 200, r.json() + assert len(r.json()) == 1 + assert r.json()[str(valid_job_id)][0]["Status"] == JobStatus.RECEIVED.value + assert r.json()[str(valid_job_id)][0]["MinorStatus"] == "Job accepted" + assert r.json()[str(valid_job_id)][0]["ApplicationStatus"] == "Unknown" + assert datetime.fromisoformat(r.json()[str(valid_job_id)][0]["StatusTime"]) + assert r.json()[str(valid_job_id)][0]["Source"] == "JobManager" diff --git a/diracx-routers/tests/jobs/test_sandboxes.py b/diracx-routers/tests/jobs/test_sandboxes.py index 7a30ba48..eb5aded6 100644 --- a/diracx-routers/tests/jobs/test_sandboxes.py +++ b/diracx-routers/tests/jobs/test_sandboxes.py @@ -26,12 +26,6 @@ ) -@pytest.fixture -def normal_user_client(client_factory): - with client_factory.normal_user() as client: - yield client - - def test_upload_then_download( normal_user_client: TestClient, test_auth_settings: AuthSettings ): diff --git a/diracx-routers/tests/jobs/test_status.py b/diracx-routers/tests/jobs/test_status.py new file mode 100644 index 00000000..0d539ee5 --- /dev/null +++ b/diracx-routers/tests/jobs/test_status.py @@ -0,0 +1,943 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from http import HTTPStatus + +import pytest +from fastapi.testclient import TestClient + +from diracx.core.models import JobStatus + +from .conftest import TEST_JDL + +pytestmark = pytest.mark.enabled_dependencies( + [ + "AuthSettings", + "JobDB", + "JobLoggingDB", + "ConfigSource", + "TaskQueueDB", + "SandboxMetadataDB", + "WMSAccessPolicy", + "DevelopmentSettings", + "JobParametersDB", + ] +) + + +def test_set_job_status(normal_user_client: TestClient, valid_job_id: int): + # Arrange + r = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "JobID", + "operator": "eq", + "value": valid_job_id, + } + ] + }, + ) + + assert r.status_code == 200, r.json() + for j in r.json(): + assert j["JobID"] == valid_job_id + assert j["Status"] == JobStatus.RECEIVED.value + assert j["MinorStatus"] == "Job accepted" + assert j["ApplicationStatus"] == "Unknown" + + # Act + new_status = JobStatus.CHECKING.value + new_minor_status = "JobPath" + r = normal_user_client.patch( + "/api/jobs/status", + json={ + valid_job_id: { + datetime.now(tz=timezone.utc).isoformat(): { + "Status": new_status, + "MinorStatus": new_minor_status, + } + } + }, + ) + + # Assert + assert r.status_code == 200, r.json() + assert r.json()["success"][str(valid_job_id)]["Status"] == new_status + assert r.json()["success"][str(valid_job_id)]["MinorStatus"] == new_minor_status + + r = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "JobID", + "operator": "eq", + "value": valid_job_id, + } + ] + }, + ) + assert r.status_code == 200, r.json() + assert r.json()[0]["JobID"] == valid_job_id + assert r.json()[0]["Status"] == new_status + assert r.json()[0]["MinorStatus"] == new_minor_status + assert r.json()[0]["ApplicationStatus"] == "Unknown" + + +def test_set_job_status_invalid_job( + normal_user_client: TestClient, invalid_job_id: int +): + # Act + r = normal_user_client.patch( + "/api/jobs/status", + json={ + invalid_job_id: { + datetime.now(tz=timezone.utc).isoformat(): { + "Status": JobStatus.CHECKING.value, + "MinorStatus": "JobPath", + } + } + }, + ) + + # Assert + assert r.status_code == HTTPStatus.NOT_FOUND, r.json() + assert r.json() == { + "detail": { + "success": {}, + "failed": {str(invalid_job_id): {"detail": "Not found"}}, + } + } + + +def test_set_job_status_offset_naive_datetime_return_bad_request( + normal_user_client: TestClient, + valid_job_id: int, +): + # Act + date = datetime.now(tz=timezone.utc).isoformat(sep=" ").split("+")[0] + r = normal_user_client.patch( + "/api/jobs/status", + json={ + valid_job_id: { + date: { + "Status": JobStatus.CHECKING.value, + "MinorStatus": "JobPath", + } + } + }, + ) + + # Assert + assert r.status_code == HTTPStatus.BAD_REQUEST, r.json() + assert r.json() == { + "detail": f"Timestamp {date} is not timezone aware for job {valid_job_id}" + } + + +def test_set_job_status_cannot_make_impossible_transitions( + normal_user_client: TestClient, valid_job_id: int +): + # Arrange + r = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "JobID", + "operator": "eq", + "value": valid_job_id, + } + ] + }, + ) + assert r.status_code == 200, r.json() + assert r.json()[0]["JobID"] == valid_job_id + assert r.json()[0]["Status"] == JobStatus.RECEIVED.value + assert r.json()[0]["MinorStatus"] == "Job accepted" + assert r.json()[0]["ApplicationStatus"] == "Unknown" + + # Act + new_status = JobStatus.RUNNING.value + new_minor_status = "JobPath" + r = normal_user_client.patch( + "/api/jobs/status", + json={ + valid_job_id: { + datetime.now(tz=timezone.utc).isoformat(): { + "Status": new_status, + "MinorStatus": new_minor_status, + } + } + }, + ) + + # Assert + assert r.status_code == 200, r.json() + success = r.json()["success"] + assert len(success) == 1, r.json() + assert success[str(valid_job_id)]["Status"] != new_status + assert success[str(valid_job_id)]["MinorStatus"] == new_minor_status + + r = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "JobID", + "operator": "eq", + "value": valid_job_id, + } + ] + }, + ) + assert r.status_code == 200, r.json() + assert r.json()[0]["Status"] != new_status + assert r.json()[0]["MinorStatus"] == new_minor_status + assert r.json()[0]["ApplicationStatus"] == "Unknown" + + +def test_set_job_status_force(normal_user_client: TestClient, valid_job_id: int): + # Arrange + r = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "JobID", + "operator": "eq", + "value": valid_job_id, + } + ] + }, + ) + assert r.status_code == 200, r.json() + assert r.json()[0]["JobID"] == valid_job_id + assert r.json()[0]["Status"] == JobStatus.RECEIVED.value + assert r.json()[0]["MinorStatus"] == "Job accepted" + assert r.json()[0]["ApplicationStatus"] == "Unknown" + + # Act + new_status = JobStatus.RUNNING.value + new_minor_status = "JobPath" + r = normal_user_client.patch( + "/api/jobs/status", + json={ + valid_job_id: { + datetime.now(tz=timezone.utc).isoformat(): { + "Status": new_status, + "MinorStatus": new_minor_status, + } + } + }, + params={"force": True}, + ) + + success = r.json()["success"] + + # Assert + assert r.status_code == 200, r.json() + assert success[str(valid_job_id)]["Status"] == new_status + assert success[str(valid_job_id)]["MinorStatus"] == new_minor_status + + r = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "JobID", + "operator": "eq", + "value": valid_job_id, + } + ] + }, + ) + assert r.status_code == 200, r.json() + assert r.json()[0]["JobID"] == valid_job_id + assert r.json()[0]["Status"] == new_status + assert r.json()[0]["MinorStatus"] == new_minor_status + assert r.json()[0]["ApplicationStatus"] == "Unknown" + + +def test_set_job_status_bulk(normal_user_client: TestClient, valid_job_ids): + # Arrange + for job_id in valid_job_ids: + r = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "JobID", + "operator": "eq", + "value": job_id, + } + ] + }, + ) + assert r.status_code == 200, r.json() + assert r.json()[0]["JobID"] == job_id + assert r.json()[0]["Status"] == JobStatus.SUBMITTING.value + assert r.json()[0]["MinorStatus"] == "Bulk transaction confirmation" + + # Act + new_status = JobStatus.CHECKING.value + new_minor_status = "JobPath" + r = normal_user_client.patch( + "/api/jobs/status", + json={ + job_id: { + datetime.now(timezone.utc).isoformat(): { + "Status": new_status, + "MinorStatus": new_minor_status, + } + } + for job_id in valid_job_ids + }, + ) + + success = r.json()["success"] + + # Assert + assert r.status_code == 200, r.json() + for job_id in valid_job_ids: + assert success[str(job_id)]["Status"] == new_status + assert success[str(job_id)]["MinorStatus"] == new_minor_status + + r_get = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "JobID", + "operator": "eq", + "value": job_id, + } + ] + }, + ) + assert r_get.status_code == 200, r_get.json() + assert r_get.json()[0]["JobID"] == job_id + assert r_get.json()[0]["Status"] == new_status + assert r_get.json()[0]["MinorStatus"] == new_minor_status + assert r_get.json()[0]["ApplicationStatus"] == "Unknown" + + +def test_set_job_status_with_invalid_job_id( + normal_user_client: TestClient, invalid_job_id: int +): + # Act + r = normal_user_client.patch( + "/api/jobs/status", + json={ + invalid_job_id: { + datetime.now(tz=timezone.utc).isoformat(): { + "Status": JobStatus.CHECKING.value, + "MinorStatus": "JobPath", + } + }, + }, + ) + + # Assert + assert r.status_code == HTTPStatus.NOT_FOUND, r.json() + assert r.json()["detail"] == { + "success": {}, + "failed": {str(invalid_job_id): {"detail": "Not found"}}, + } + + +def test_insert_and_reschedule(normal_user_client: TestClient): + job_definitions = [TEST_JDL] + r = normal_user_client.post("/api/jobs/jdl", json=job_definitions) + assert r.status_code == 200, r.json() + assert len(r.json()) == len(job_definitions) + + submitted_job_ids = sorted([job_dict["JobID"] for job_dict in r.json()]) + + # Test /jobs/reschedule and + # test max_reschedule + + max_resched = 3 + jid = str(submitted_job_ids[0]) + + for i in range(max_resched): + r = normal_user_client.post( + "/api/jobs/reschedule", + params={"job_ids": submitted_job_ids}, + ) + assert r.status_code == 200, r.json() + result = r.json() + successful_results = result["success"] + assert jid in successful_results, result + assert successful_results[jid]["Status"] == JobStatus.RECEIVED + assert successful_results[jid]["MinorStatus"] == "Job Rescheduled" + assert successful_results[jid]["RescheduleCounter"] == i + 1 + + r = normal_user_client.post( + "/api/jobs/reschedule", + params={"job_ids": submitted_job_ids}, + ) + assert ( + r.status_code != 200 + ), f"Rescheduling more than {max_resched} times should have failed by now {r.json()}" + assert r.json() == { + "detail": { + "success": [], + "failed": { + "1": { + "detail": f"Maximum number of reschedules exceeded ({max_resched})" + } + }, + } + } + + +## test edge case for rescheduling + + +def test_reschedule_job_attr_update(normal_user_client: TestClient): + job_definitions = [TEST_JDL] * 15 + + r = normal_user_client.post("/api/jobs/jdl", json=job_definitions) + assert r.status_code == 200, r.json() + assert len(r.json()) == len(job_definitions) + + submitted_job_ids = sorted([job_dict["JobID"] for job_dict in r.json()]) + + # Test /jobs/reschedule and + # test max_reschedule + + max_resched = 3 + + fail_resched_ids = submitted_job_ids[0:5] + good_resched_ids = list(set(submitted_job_ids) - set(fail_resched_ids)) + + for i in range(max_resched): + r = normal_user_client.post( + "/api/jobs/reschedule", + params={"job_ids": fail_resched_ids}, + ) + assert r.status_code == 200, r.json() + result = r.json() + successful_results = result["success"] + for jid in fail_resched_ids: + assert str(jid) in successful_results, result + assert successful_results[str(jid)]["Status"] == JobStatus.RECEIVED + assert successful_results[str(jid)]["MinorStatus"] == "Job Rescheduled" + assert successful_results[str(jid)]["RescheduleCounter"] == i + 1 + + for i in range(max_resched): + r = normal_user_client.post( + "/api/jobs/reschedule", + params={"job_ids": submitted_job_ids}, + ) + assert r.status_code == 200, r.json() + result = r.json() + successful_results = result["success"] + failed_results = result["failed"] + for jid in good_resched_ids: + assert str(jid) in successful_results, result + assert successful_results[str(jid)]["Status"] == JobStatus.RECEIVED + assert successful_results[str(jid)]["MinorStatus"] == "Job Rescheduled" + assert successful_results[str(jid)]["RescheduleCounter"] == i + 1 + for jid in fail_resched_ids: + assert str(jid) in failed_results, result + # assert successful_results[jid]["Status"] == JobStatus.RECEIVED + # assert successful_results[jid]["MinorStatus"] == "Job Rescheduled" + # assert successful_results[jid]["RescheduleCounter"] == i + 1 + + r = normal_user_client.post( + "/api/jobs/reschedule", + params={"job_ids": submitted_job_ids}, + ) + assert ( + r.status_code != 200 + ), f"Rescheduling more than {max_resched} times should have failed by now {r.json()}" + assert r.json() == { + "detail": { + "success": [], + "failed": { + str(i): { + "detail": f"Maximum number of reschedules exceeded ({max_resched})" + } + for i in good_resched_ids + fail_resched_ids + }, + } + } + + +# Test delete job + + +def test_delete_job_valid_job_id(normal_user_client: TestClient, valid_job_id: int): + # Act + r = normal_user_client.patch( + "/api/jobs/status", + json={ + valid_job_id: { + str(datetime.now(tz=timezone.utc)): { + "Status": JobStatus.DELETED, + "MinorStatus": "Checking accounting", + } + } + }, + ) + + # Assert + assert r.status_code == 200, r.json() + r = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "JobID", + "operator": "eq", + "value": valid_job_id, + } + ] + }, + ) + assert r.status_code == 200, r.json() + assert r.json()[0]["JobID"] == valid_job_id + assert r.json()[0]["Status"] == JobStatus.DELETED + assert r.json()[0]["MinorStatus"] == "Checking accounting" + assert r.json()[0]["ApplicationStatus"] == "Unknown" + + +def test_delete_job_invalid_job_id(normal_user_client: TestClient, invalid_job_id: int): + # Act + r = normal_user_client.patch( + "/api/jobs/status", + json={ + invalid_job_id: { + str(datetime.now(tz=timezone.utc)): { + "Status": JobStatus.DELETED, + "MinorStatus": "Checking accounting", + } + } + }, + ) + # Assert + assert r.status_code == HTTPStatus.NOT_FOUND, r.json() + assert r.json()["detail"]["failed"] == { + str(invalid_job_id): {"detail": "Not found"} + } + + +def test_delete_bulk_jobs_valid_job_ids( + normal_user_client: TestClient, valid_job_ids: list[int] +): + # Act + r = normal_user_client.patch( + "/api/jobs/status", + json={ + job_id: { + str(datetime.now(tz=timezone.utc)): { + "Status": JobStatus.DELETED, + "MinorStatus": "Checking accounting", + } + } + for job_id in valid_job_ids + }, + ) + req = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "JobID", + "operator": "in", + "values": valid_job_ids, + } + ] + }, + ) + assert req.status_code == 200, req.json() + + r = {i["JobID"]: i for i in req.json()} + for valid_job_id in valid_job_ids: + assert r[valid_job_id]["Status"] == JobStatus.DELETED + assert r[valid_job_id]["MinorStatus"] == "Checking accounting" + assert r[valid_job_id]["ApplicationStatus"] == "Unknown" + + +def test_delete_bulk_jobs_invalid_job_ids( + normal_user_client: TestClient, invalid_job_ids: list[int] +): + # Act + r = normal_user_client.patch( + "/api/jobs/status", + json={ + job_id: { + str(datetime.now(tz=timezone.utc)): { + "Status": JobStatus.DELETED, + "MinorStatus": "Checking accounting", + } + } + for job_id in invalid_job_ids + }, + ) + + # Assert + assert r.status_code == HTTPStatus.NOT_FOUND, r.json() + assert r.json() == { + "detail": { + "success": {}, + "failed": {str(jid): {"detail": "Not found"} for jid in invalid_job_ids}, + } + } + + +def test_delete_bulk_jobs_mix_of_valid_and_invalid_job_ids( + normal_user_client: TestClient, valid_job_ids: list[int], invalid_job_ids: list[int] +): + # Arrange + job_ids = valid_job_ids + invalid_job_ids + + # Act + r = normal_user_client.patch( + "/api/jobs/status", + json={ + job_id: { + str(datetime.now(tz=timezone.utc)): { + "Status": JobStatus.DELETED, + "MinorStatus": "Checking accounting", + } + } + for job_id in job_ids + }, + ) + + # Assert + assert r.status_code == HTTPStatus.OK, r.json() + resp = r.json() + + assert len(resp["success"]) == len(valid_job_ids) + assert resp["failed"] == { + "999999997": {"detail": "Not found"}, + "999999998": {"detail": "Not found"}, + "999999999": {"detail": "Not found"}, + } + + req = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "JobID", + "operator": "in", + "values": valid_job_ids, + } + ] + }, + ) + assert req.status_code == 200, req.json() + + r = req.json() + assert len(r) == len(valid_job_ids), r + for job in r: + assert job["Status"] == JobStatus.DELETED + assert job["MinorStatus"] == "Checking accounting" + + +# Test kill job +def test_kill_job_valid_job_id(normal_user_client: TestClient, valid_job_id: int): + # Act + r = normal_user_client.patch( + "/api/jobs/status", + json={ + valid_job_id: { + str(datetime.now(timezone.utc)): { + "Status": JobStatus.KILLED, + "MinorStatus": "Marked for termination", + } + } + }, + ) + + # Assert + assert r.status_code == 200, r.json() + + successful = r.json()["success"] + assert successful[str(valid_job_id)]["Status"] == JobStatus.KILLED + req = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "JobID", + "operator": "eq", + "value": valid_job_id, + } + ] + }, + ) + assert req.status_code == 200, successful + assert req.json()[0]["JobID"] == valid_job_id + assert req.json()[0]["Status"] == JobStatus.KILLED + assert req.json()[0]["MinorStatus"] == "Marked for termination" + assert req.json()[0]["ApplicationStatus"] == "Unknown" + + +def test_kill_job_invalid_job_id(normal_user_client: TestClient, invalid_job_id: int): + # Act + r = normal_user_client.patch( + "/api/jobs/status", + json={ + int(invalid_job_id): { + str(datetime.now(timezone.utc)): { + "Status": JobStatus.KILLED, + "MinorStatus": "Marked for termination", + } + } + }, + ) + + # Assert + assert r.status_code == HTTPStatus.NOT_FOUND, r.json() + assert r.json()["detail"] == { + "success": {}, + "failed": {str(invalid_job_id): {"detail": "Not found"}}, + } + + +def test_kill_bulk_jobs_valid_job_ids( + normal_user_client: TestClient, valid_job_ids: list[int] +): + # Act + r = normal_user_client.patch( + "/api/jobs/status", + json={ + job_id: { + str(datetime.now(timezone.utc)): { + "Status": JobStatus.KILLED, + "MinorStatus": "Marked for termination", + } + } + for job_id in valid_job_ids + }, + ) + result = r.json() + + # Assert + assert r.status_code == 200, result + req = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "JobID", + "operator": "in", + "values": valid_job_ids, + } + ] + }, + ) + assert req.status_code == 200, req.json() + + r = req.json() + assert len(r) == len(valid_job_ids), r + for job in r: + assert job["Status"] == JobStatus.KILLED + assert job["MinorStatus"] == "Marked for termination" + assert job["ApplicationStatus"] == "Unknown" + + +def test_kill_bulk_jobs_invalid_job_ids( + normal_user_client: TestClient, invalid_job_ids: list[int] +): + # Act + r = normal_user_client.patch( + "/api/jobs/status", + json={ + job_id: { + str(datetime.now(timezone.utc)): { + "Status": JobStatus.KILLED, + "MinorStatus": "Marked for termination", + } + } + for job_id in invalid_job_ids + }, + ) + # Assert + assert r.status_code == HTTPStatus.NOT_FOUND, r.json() + + assert r.json()["detail"] == { + "success": {}, + "failed": { + "999999997": {"detail": "Not found"}, + "999999998": {"detail": "Not found"}, + "999999999": {"detail": "Not found"}, + }, + } + + +def test_kill_bulk_jobs_mix_of_valid_and_invalid_job_ids( + normal_user_client: TestClient, valid_job_ids: list[int], invalid_job_ids: list[int] +): + # Arrange + job_ids = valid_job_ids + invalid_job_ids + + # Act + r = normal_user_client.patch( + "/api/jobs/status", + json={ + job_id: { + str(datetime.now(timezone.utc)): { + "Status": JobStatus.KILLED, + "MinorStatus": "Marked for termination", + } + } + for job_id in job_ids + }, + ) + # Assert + assert r.status_code == HTTPStatus.OK, r.json() + resp = r.json() + + assert len(resp["success"]) == len(valid_job_ids) + assert resp["failed"] == { + "999999997": {"detail": "Not found"}, + "999999998": {"detail": "Not found"}, + "999999999": {"detail": "Not found"}, + } + + req = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "JobID", + "operator": "in", + "values": valid_job_ids, + } + ] + }, + ) + assert req.status_code == 200, req.json() + + r = req.json() + assert len(r) == len(valid_job_ids), r + for job in r: + assert job["Status"] == JobStatus.KILLED + assert job["MinorStatus"] == "Marked for termination" + assert job["ApplicationStatus"] == "Unknown" + + +# Test remove job + + +def test_remove_job_valid_job_id(normal_user_client: TestClient, valid_job_id: int): + r = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "JobID", + "operator": "eq", + "value": valid_job_id, + } + ] + }, + ) + assert r.status_code == HTTPStatus.OK, r.json() + assert r.json() != [] + + # Act + r = normal_user_client.delete( + "/api/jobs/", + params={ + "job_ids": [valid_job_id], + }, + ) + + assert r.status_code == HTTPStatus.OK, r.json() + + # Assert + assert r.status_code == 200, r.json() + r = normal_user_client.post( + "/api/jobs/search", + json={ + "search": [ + { + "parameter": "JobID", + "operator": "eq", + "value": valid_job_id, + } + ] + }, + ) + assert r.status_code == HTTPStatus.OK, r.json() + assert r.json() == [] + + +def test_remove_job_invalid_job_id(normal_user_client: TestClient, invalid_job_id: int): + # Act + r = normal_user_client.delete( + "/api/jobs/", + params={ + "job_ids": [invalid_job_id], + }, + ) + + # Assert + assert r.status_code == 200, r.json() + + +def test_remove_jobs_valid_job_ids( + normal_user_client: TestClient, valid_job_ids: list[int] +): + # Act + r = normal_user_client.delete("/api/jobs/", params={"job_ids": valid_job_ids}) + + # Assert + assert r.status_code == 200, r.json() + for job_id in valid_job_ids: + r = normal_user_client.get(f"/api/jobs/{job_id}/status") + assert r.status_code == HTTPStatus.NOT_FOUND, r.json() + + +# def test_remove_jobs_invalid_job_ids( +# normal_user_client: TestClient, invalid_job_ids: list[int] +# ): +# # Act +# r = normal_user_client.post("/api/jobs/remove", params={"job_ids": invalid_job_ids}) + +# # Assert +# assert r.status_code == HTTPStatus.NOT_FOUND, r.json() +# assert r.json() == { +# "detail": { +# "message": f"Failed to remove {len(invalid_job_ids)} jobs out of {len(invalid_job_ids)}", +# "failed_ids": { +# str(invalid_job_id): f"Job {invalid_job_id} not found" +# for invalid_job_id in invalid_job_ids +# }, +# } +# } + + +# def test_remove_jobs_mix_of_valid_and_invalid_job_ids( +# normal_user_client: TestClient, valid_job_ids: list[int], invalid_job_ids: list[int] +# ): +# # Arrange +# job_ids = valid_job_ids + invalid_job_ids + +# # Act +# r = normal_user_client.post("/api/jobs/remove", params={"job_ids": job_ids}) + +# # Assert +# assert r.status_code == HTTPStatus.NOT_FOUND, r.json() +# assert r.json() == { +# "detail": { +# "message": f"Failed to remove {len(invalid_job_ids)} jobs out of {len(job_ids)}", +# "failed_ids": { +# str(invalid_job_id): f"Job {invalid_job_id} not found" +# for invalid_job_id in invalid_job_ids +# }, +# } +# } +# for job_id in valid_job_ids: +# r = normal_user_client.get(f"/api/jobs/{job_id}/status") +# assert r.status_code == HTTPStatus.NOT_FOUND, r.json() diff --git a/diracx-routers/tests/test_job_manager.py b/diracx-routers/tests/test_job_manager.py deleted file mode 100644 index 16aadfb4..00000000 --- a/diracx-routers/tests/test_job_manager.py +++ /dev/null @@ -1,1830 +0,0 @@ -from __future__ import annotations - -from datetime import datetime, timezone -from http import HTTPStatus - -import pytest -from fastapi.testclient import TestClient -from freezegun import freeze_time - -from diracx.core.models import JobStatus - -TEST_JDL = """ - Arguments = "jobDescription.xml -o LogLevel=INFO"; - Executable = "dirac-jobexec"; - JobGroup = jobGroup; - JobName = jobName; - JobType = User; - LogLevel = INFO; - OutputSandbox = - { - Script1_CodeOutput.log, - std.err, - std.out - }; - Priority = 1; - Site = ANY; - StdError = std.err; - StdOutput = std.out; -""" - -TEST_PARAMETRIC_JDL = """ -Arguments = "jobDescription.xml -o LogLevel=DEBUG -p JOB_ID=%(JOB_ID)s -p InputData=%(InputData)s"; - Executable = "dirac-jobexec"; - InputData = %(InputData)s; - InputSandbox = jobDescription.xml; - JOB_ID = %(JOB_ID)s; - JobName = Name; - JobType = User; - LogLevel = DEBUG; - OutputSandbox = - { - Script1_CodeOutput.log, - std.err, - std.out - }; - Parameters = 3; - Parameters.InputData = - { - {/lhcb/data/data1, - /lhcb/data/data2}, - {/lhcb/data/data3, - /lhcb/data/data4}, - {/lhcb/data/data5, - /lhcb/data/data6} - }; - Parameters.JOB_ID = - { - 1, - 2, - 3 - }; - Priority = 1; - StdError = std.err; - StdOutput = std.out; -""" - -TEST_LARGE_PARAMETRIC_JDL = """ - Executable = "echo"; - Arguments = "%s"; - JobName = "Test_%n"; - Parameters = 100; - ParameterStart = 1; -""" - -TEST_MALFORMED_JDL = """ -[ - 'Executable = "echo";' -] -""" - -pytestmark = pytest.mark.enabled_dependencies( - [ - "AuthSettings", - "JobDB", - "JobLoggingDB", - "ConfigSource", - "TaskQueueDB", - "SandboxMetadataDB", - "WMSAccessPolicy", - "DevelopmentSettings", - "JobParametersDB", - ] -) - - -@pytest.fixture -def normal_user_client(client_factory): - with client_factory.normal_user() as client: - yield client - - -@pytest.fixture -def admin_user_client(client_factory): - with client_factory.admin_user() as client: - yield client - - -def test_insert_and_list_parametric_jobs(normal_user_client): - job_definitions = [TEST_PARAMETRIC_JDL] - r = normal_user_client.post("/api/jobs/jdl", json=job_definitions) - assert r.status_code == 200, r.json() - assert len(r.json()) == 3 # Parameters.JOB_ID is 3 - - submitted_job_ids = sorted([job_dict["JobID"] for job_dict in r.json()]) - - r = normal_user_client.post("/api/jobs/search") - assert r.status_code == 200, r.json() - - listed_jobs = r.json() - - assert "Content-Range" not in r.headers - - assert len(listed_jobs) == 3 # Parameters.JOB_ID is 3 - - assert submitted_job_ids == sorted([job_dict["JobID"] for job_dict in listed_jobs]) - - -@pytest.mark.parametrize( - "job_definitions", - [ - [TEST_JDL], - [TEST_JDL for _ in range(2)], - [TEST_JDL for _ in range(10)], - ], -) -def test_insert_and_list_bulk_jobs(job_definitions, normal_user_client): - r = normal_user_client.post("/api/jobs/jdl", json=job_definitions) - assert r.status_code == 200, r.json() - assert len(r.json()) == len(job_definitions) - - submitted_job_ids = sorted([job_dict["JobID"] for job_dict in r.json()]) - - r = normal_user_client.post("/api/jobs/search") - assert r.status_code == 200, r.json() - - listed_jobs = r.json() - - assert "Content-Range" not in r.headers - - assert len(listed_jobs) == len(job_definitions) - - assert submitted_job_ids == sorted([job_dict["JobID"] for job_dict in listed_jobs]) - - -def test_insert_and_search(normal_user_client): - """Test inserting a job and then searching for it.""" - # job_definitions = [TEST_JDL%(normal_user_client.dirac_token_payload)] - job_definitions = [TEST_JDL] - r = normal_user_client.post("/api/jobs/jdl", json=job_definitions) - listed_jobs = r.json() - assert r.status_code == 200, listed_jobs - assert len(listed_jobs) == len(job_definitions) - - submitted_job_ids = sorted([job_dict["JobID"] for job_dict in r.json()]) - - # Test /jobs/search - # 1. Search for all jobs - r = normal_user_client.post("/api/jobs/search") - listed_jobs = r.json() - assert r.status_code == 200, listed_jobs - assert [x["JobID"] for x in listed_jobs] == submitted_job_ids - assert {x["VerifiedFlag"] for x in listed_jobs} == {True} - - # 2. Search for all jobs with status NEW: should return an empty list - r = normal_user_client.post( - "/api/jobs/search", - json={"search": [{"parameter": "Status", "operator": "eq", "value": "NEW"}]}, - ) - listed_jobs = r.json() - assert r.status_code == 200, listed_jobs - assert listed_jobs == [] - - assert "Content-Range" not in r.headers - - # 3. Search for all jobs with status RECEIVED: should return the submitted jobs - r = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "Status", - "operator": "eq", - "value": JobStatus.RECEIVED.value, - } - ] - }, - ) - listed_jobs = r.json() - assert r.status_code == 200, listed_jobs - assert [x["JobID"] for x in listed_jobs] == submitted_job_ids - - assert "Content-Range" not in r.headers - - # 4. Search for all jobs but just return the JobID and the Status - r = normal_user_client.post( - "/api/jobs/search", json={"parameters": ["JobID", "Status"]} - ) - listed_jobs = r.json() - assert r.status_code == 200, listed_jobs - assert listed_jobs == [ - {"JobID": jid, "Status": JobStatus.RECEIVED.value} for jid in submitted_job_ids - ] - - assert "Content-Range" not in r.headers - - # Test /jobs/summary - r = normal_user_client.post( - "/api/jobs/summary", json={"grouping": ["Status", "OwnerGroup"]} - ) - assert r.status_code == 200, r.json() - - assert r.json() == [ - {"Status": JobStatus.RECEIVED.value, "OwnerGroup": "test_group", "count": 1} - ] - - r = normal_user_client.post( - "/api/jobs/summary", - json={ - "grouping": ["Status"], - "search": [ - { - "parameter": "Status", - "operator": "eq", - "value": JobStatus.RECEIVED.value, - } - ], - }, - ) - assert r.status_code == 200, r.json() - assert r.json() == [{"Status": JobStatus.RECEIVED.value, "count": 1}] - - r = normal_user_client.post( - "/api/jobs/summary", - json={ - "grouping": ["Status"], - "search": [{"parameter": "Status", "operator": "eq", "value": "NEW"}], - }, - ) - assert r.status_code == 200, r.json() - assert r.json() == [] - - -def test_insert_malformed_jdl(normal_user_client): - job_definitions = [TEST_MALFORMED_JDL] - r = normal_user_client.post("/api/jobs/jdl", json=job_definitions) - assert r.status_code == 400, r.json() - - -@freeze_time("2024-01-01T00:00:00.123456Z") -def test_insert_and_search_by_datetime(normal_user_client): - """Test inserting a job and then searching for it. - - Focus on the SubmissionTime parameter. - """ - # job_definitions = [TEST_JDL%(normal_user_client.dirac_token_payload)] - job_definitions = [TEST_JDL] - r = normal_user_client.post("/api/jobs/jdl", json=job_definitions) - listed_jobs = r.json() - assert r.status_code == 200, listed_jobs - assert len(listed_jobs) == len(job_definitions) - - # 1.1 Search for all jobs submitted in 2024 - r = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "SubmissionTime", - "operator": "eq", - "value": "2024", - } - ] - }, - ) - assert r.status_code == 200, r.json() - assert len(r.json()) == 1 - - # 1.2 Search for all jobs submitted before 2024 - r = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "SubmissionTime", - "operator": "lt", - "value": "2024", - } - ] - }, - ) - assert r.status_code == 200, r.json() - assert len(r.json()) == 0 - - # 2.1 Search for all jobs submitted after 2024-01 - r = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "SubmissionTime", - "operator": "gt", - "value": "2024-01", - } - ] - }, - ) - assert r.status_code == 200, r.json() - assert len(r.json()) == 0 - - # 2.2 Search for all jobs submitted before 2024-02 - r = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "SubmissionTime", - "operator": "lt", - "value": "2024-02", - } - ] - }, - ) - assert r.status_code == 200, r.json() - assert len(r.json()) == 1 - - # 3 Search for all jobs submitted during 2024-01-01 - r = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "SubmissionTime", - "operator": "eq", - "value": "2024-01-01", - } - ] - }, - ) - assert r.status_code == 200, r.json() - assert len(r.json()) == 1 - - # 4.1 Search for all jobs submitted during 2024-01-01 00 - r = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "SubmissionTime", - "operator": "eq", - "value": "2024-01-01 00", - } - ] - }, - ) - assert r.status_code == 200, r.json() - assert len(r.json()) == 1 - - # 4.2 Search for all jobs submitted during 2024-01-01T00 (with the 'T' separator) - r = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "SubmissionTime", - "operator": "eq", - "value": "2024-01-01T00", - } - ] - }, - ) - assert r.status_code == 200, r.json() - assert len(r.json()) == 1 - - # 4.3 Search for all jobs not submitted during 2024-01-01 01 - r = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "SubmissionTime", - "operator": "neq", - "value": "2024-01-01 01", - } - ] - }, - ) - assert r.status_code == 200, r.json() - assert len(r.json()) == 1 - - # 5.1 Search for all jobs submitted after 2024-01-01 00:00:00 - r = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "SubmissionTime", - "operator": "gt", - "value": "2024-01-01 00:00:00", - } - ] - }, - ) - assert r.status_code == 200, r.json() - assert len(r.json()) == 0 - - # 5.2 Search for all jobs not submitted on 2024-01-01 00:00:00 - r = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "SubmissionTime", - "operator": "neq", - "value": "2024-01-01 00:00:00", - } - ] - }, - ) - assert r.status_code == 200, r.json() - assert len(r.json()) == 0 - - # 5.3 Search for all jobs submitted on 2024-01-01 00:00:00 - r = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "SubmissionTime", - "operator": "eq", - "value": "2024-01-01 00:00:00", - } - ] - }, - ) - assert r.status_code == 200, r.json() - assert len(r.json()) == 1 - - # 6.1 Search for all jobs submitted on 2024-01-01 00:00:00.123456 - r = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "SubmissionTime", - "operator": "eq", - "value": "2024-01-01 00:00:00.123456", - } - ] - }, - ) - assert r.status_code == 200, r.json() - assert len(r.json()) == 1 - - # 6.2 Search for all jobs submitted on 2024-01-01 00:00:00.123456Z - r = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "SubmissionTime", - "operator": "eq", - "value": "2024-01-01 00:00:00.123456Z", - } - ] - }, - ) - assert r.status_code == 200, r.json() - assert len(r.json()) == 1 - - # 6.3 Search for all jobs submitted on 2024-01-01 00:00:00.123Z - r = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "SubmissionTime", - "operator": "eq", - "value": "2024-01-01 00:00:00.123Z", - } - ] - }, - ) - assert r.status_code == 200, r.json() - assert len(r.json()) == 1 - - -def test_search_distinct(normal_user_client): - """Test that the distinct parameter works as expected.""" - job_definitions = [TEST_JDL, TEST_JDL, TEST_JDL] - r = normal_user_client.post("/api/jobs/jdl", json=job_definitions) - listed_jobs = r.json() - assert r.status_code == 200, listed_jobs - assert len(listed_jobs) == len(job_definitions) - - # Check that distinct collapses identical records when true - r = normal_user_client.post( - "/api/jobs/search", json={"parameters": ["Status"], "distinct": False} - ) - listed_jobs = r.json() - assert r.status_code == 200, listed_jobs - assert len(listed_jobs) > 1 - - assert "Content-Range" not in r.headers - - r = normal_user_client.post( - "/api/jobs/search", json={"parameters": ["Status"], "distinct": True} - ) - listed_jobs = r.json() - assert r.status_code == 200, listed_jobs - assert len(listed_jobs) == 1 - - assert "Content-Range" not in r.headers - - -def test_search_pagination(normal_user_client): - """Test that the pagination works as expected.""" - job_definitions = [TEST_JDL] * 20 - r = normal_user_client.post("/api/jobs/jdl", json=job_definitions) - listed_jobs = r.json() - assert r.status_code == 200, listed_jobs - assert len(listed_jobs) == len(job_definitions) - - # Get the first 20 jobs (all of them) - r = normal_user_client.post("/api/jobs/search", params={"page": 1, "per_page": 20}) - listed_jobs = r.json() - assert r.status_code == 200, listed_jobs - assert len(listed_jobs) == 20 - - assert "Content-Range" not in r.headers - - # Get the first 10 jobs - r = normal_user_client.post("/api/jobs/search", params={"page": 1, "per_page": 10}) - listed_jobs = r.json() - assert r.status_code == 206, listed_jobs - assert len(listed_jobs) == 10 - - assert "Content-Range" in r.headers - assert ( - r.headers["Content-Range"] - == f"jobs 0-{len(listed_jobs) -1}/{len(job_definitions)}" - ) - - # Get the next 10 jobs - r = normal_user_client.post("/api/jobs/search", params={"page": 2, "per_page": 10}) - listed_jobs = r.json() - assert r.status_code == 206, listed_jobs - assert len(listed_jobs) == 10 - - assert "Content-Range" in r.headers - assert ( - r.headers["Content-Range"] - == f"jobs 10-{len(listed_jobs) + 10 - 1}/{len(job_definitions)}" - ) - - # Get an unknown page - r = normal_user_client.post("/api/jobs/search", params={"page": 3, "per_page": 10}) - listed_jobs = r.json() - assert r.status_code == 416, listed_jobs - assert len(listed_jobs) == 0 - - assert "Content-Range" in r.headers - assert r.headers["Content-Range"] == f"jobs */{len(job_definitions)}" - - # Set the per_page parameter to 0 - r = normal_user_client.post("/api/jobs/search", params={"page": 1, "per_page": 0}) - assert r.status_code == 400, r.json() - - # Set the per_page parameter to a negative number - r = normal_user_client.post("/api/jobs/search", params={"page": 1, "per_page": -1}) - assert r.status_code == 400, r.json() - - # Set the page parameter to 0 - r = normal_user_client.post("/api/jobs/search", params={"page": 0, "per_page": 10}) - assert r.status_code == 400, r.json() - - # Set the page parameter to a negative number - r = normal_user_client.post("/api/jobs/search", params={"page": -1, "per_page": 10}) - assert r.status_code == 400, r.json() - - -def test_user_cannot_submit_parametric_jdl_greater_than_max_parametric_jobs( - normal_user_client, -): - """Test that a user cannot submit a parametric JDL greater than the max parametric jobs.""" - job_definitions = [TEST_LARGE_PARAMETRIC_JDL] - res = normal_user_client.post("/api/jobs/jdl", json=job_definitions) - assert res.status_code == HTTPStatus.BAD_REQUEST, res.json() - - -def test_user_cannot_submit_list_of_jdl_greater_than_max_number_of_jobs( - normal_user_client, -): - """Test that a user cannot submit a list of JDL greater than the max number of jobs.""" - job_definitions = [TEST_JDL for _ in range(100)] - res = normal_user_client.post("/api/jobs/jdl", json=job_definitions) - assert res.status_code == HTTPStatus.BAD_REQUEST, res.json() - - -@pytest.mark.parametrize( - "job_definitions", - [[TEST_PARAMETRIC_JDL, TEST_JDL], [TEST_PARAMETRIC_JDL, TEST_PARAMETRIC_JDL]], -) -def test_user_cannot_submit_multiple_jdl_if_at_least_one_of_them_is_parametric( - normal_user_client, job_definitions -): - res = normal_user_client.post("/api/jobs/jdl", json=job_definitions) - assert res.status_code == HTTPStatus.BAD_REQUEST, res.json() - - -def test_user_without_the_normal_user_property_cannot_submit_job(admin_user_client): - pytest.skip( - "AlwaysAllowAccessPolicyCallable is forced in testing, so this test can not actually test this access policy." - ) - res = admin_user_client.post("/api/jobs/jdl", json=[TEST_JDL]) - assert res.status_code == HTTPStatus.FORBIDDEN, res.json() - - -@pytest.fixture -def valid_job_id(normal_user_client: TestClient): - job_definitions = [TEST_JDL] - r = normal_user_client.post("/api/jobs/jdl", json=job_definitions) - assert r.status_code == 200, r.json() - assert len(r.json()) == 1 - return r.json()[0]["JobID"] - - -@pytest.fixture -def valid_job_ids(normal_user_client: TestClient): - job_definitions = [TEST_PARAMETRIC_JDL] - r = normal_user_client.post("/api/jobs/jdl", json=job_definitions) - assert r.status_code == 200, r.json() - assert len(r.json()) == 3 - return sorted([job_dict["JobID"] for job_dict in r.json()]) - - -@pytest.fixture -def invalid_job_id(): - return 999999996 - - -@pytest.fixture -def invalid_job_ids(): - return [999999997, 999999998, 999999999] - - -def test_get_job_status(normal_user_client: TestClient, valid_job_id: int): - """Test that the job status is returned correctly.""" - # Act - r = normal_user_client.post( - "/api/jobs/search", - json={ - "parameters": ["JobID", "Status", "MinorStatus", "ApplicationStatus"], - "search": [{"parameter": "JobID", "operator": "eq", "value": valid_job_id}], - }, - ) - - # Assert - assert r.status_code == 200, r.json() - assert len(r.json()) == 1, f"Should only return length-1 list: {r.json()}" - assert r.json()[0]["JobID"] == valid_job_id, "Returned wrong job id" - # TODO: should we return camel case here (and everywhere else) ? - assert r.json()[0]["Status"] == JobStatus.RECEIVED.value - assert r.json()[0]["MinorStatus"] == "Job accepted" - assert r.json()[0]["ApplicationStatus"] == "Unknown" - - -def test_get_status_of_nonexistent_job( - normal_user_client: TestClient, invalid_job_id: int -): - """Test that the job status is returned correctly.""" - # Act - r = normal_user_client.post( - "/api/jobs/search", - json={ - "parameters": ["Status"], - "search": [ - {"parameter": "JobID", "operator": "eq", "value": invalid_job_id} - ], - }, - ) - - # Assert - assert r.status_code == HTTPStatus.OK, r.json() - assert r.json() == [] - - -def test_get_job_status_in_bulk(normal_user_client: TestClient, valid_job_ids: list): - """Test that we can get the status of multiple jobs in one request.""" - # Act - - r = normal_user_client.post( - "/api/jobs/search", - json={ - "parameters": ["JobID", "Status", "MinorStatus", "ApplicationStatus"], - "search": [ - {"parameter": "JobID", "operator": "in", "values": valid_job_ids} - ], - }, - ) - - # Assert - assert r.status_code == 200, r.json() - assert len(r.json()) == 3 # Parameters.JOB_ID is 3 - assert {j["JobID"] for j in r.json()} == set(valid_job_ids) - for job in r.json(): - assert job["JobID"] in valid_job_ids - assert job["Status"] == JobStatus.SUBMITTING.value - assert job["MinorStatus"] == "Bulk transaction confirmation" - assert job["ApplicationStatus"] == "Unknown" - - -async def test_get_job_status_history( - normal_user_client: TestClient, valid_job_id: int -): - # Arrange - r = normal_user_client.post( - "/api/jobs/search", - json={ - "parameters": ["JobID", "Status", "MinorStatus", "ApplicationStatus"], - "search": [{"parameter": "JobID", "operator": "eq", "value": valid_job_id}], - }, - ) - assert r.status_code == 200, r.json() - assert r.json()[0]["Status"] == JobStatus.RECEIVED.value - assert r.json()[0]["MinorStatus"] == "Job accepted" - assert r.json()[0]["ApplicationStatus"] == "Unknown" - - new_status = JobStatus.CHECKING.value - new_minor_status = "JobPath" - before = datetime.now(timezone.utc) - - r = normal_user_client.patch( - "/api/jobs/status", - json={ - valid_job_id: { - datetime.now(tz=timezone.utc).isoformat(): { - "Status": new_status, - "MinorStatus": new_minor_status, - } - } - }, - ) - - after = datetime.now(timezone.utc) - - assert r.status_code == 200, r.json() - assert r.json()["success"][str(valid_job_id)]["Status"] == new_status - assert r.json()["success"][str(valid_job_id)]["MinorStatus"] == new_minor_status - - # Act - r = normal_user_client.post( - "/api/jobs/search", - json={ - "parameters": [ - "JobID", - "Status", - "MinorStatus", - "ApplicationStatus", - "LoggingInfo", - ], - "search": [{"parameter": "JobID", "operator": "eq", "value": valid_job_id}], - }, - ) - # Assert - assert r.status_code == 200, r.json() - assert len(r.json()) == 1 - assert len(r.json()[0]["LoggingInfo"]) == 2 - assert r.json()[0]["LoggingInfo"][0]["Status"] == JobStatus.RECEIVED.value - assert r.json()[0]["LoggingInfo"][0]["MinorStatus"] == "Job accepted" - assert r.json()[0]["LoggingInfo"][0]["ApplicationStatus"] == "Unknown" - assert r.json()[0]["LoggingInfo"][0]["Source"] == "JobManager" - - assert r.json()[0]["LoggingInfo"][1]["Status"] == JobStatus.CHECKING.value - assert r.json()[0]["LoggingInfo"][1]["MinorStatus"] == "JobPath" - assert r.json()[0]["LoggingInfo"][1]["ApplicationStatus"] == "Unknown" - assert ( - before - < datetime.fromisoformat(r.json()[0]["LoggingInfo"][1]["StatusTime"]) - < after - ) - assert r.json()[0]["LoggingInfo"][1]["Source"] == "Unknown" - - -def test_get_job_status_history_in_bulk( - normal_user_client: TestClient, valid_job_id: int -): - pytest.skip("TODO: decide whether to keep this") - - # Arrange - r = normal_user_client.get(f"/api/jobs/{valid_job_id}/status") - assert r.status_code == 200, r.json() - assert r.json()[str(valid_job_id)]["Status"] == JobStatus.RECEIVED.value - assert r.json()[str(valid_job_id)]["MinorStatus"] == "Job accepted" - assert r.json()[str(valid_job_id)]["ApplicationStatus"] == "Unknown" - - # Act - r = normal_user_client.get( - "/api/jobs/status/history", params={"job_ids": [valid_job_id]} - ) - - # Assert - assert r.status_code == 200, r.json() - assert len(r.json()) == 1 - assert r.json()[str(valid_job_id)][0]["Status"] == JobStatus.RECEIVED.value - assert r.json()[str(valid_job_id)][0]["MinorStatus"] == "Job accepted" - assert r.json()[str(valid_job_id)][0]["ApplicationStatus"] == "Unknown" - assert datetime.fromisoformat(r.json()[str(valid_job_id)][0]["StatusTime"]) - assert r.json()[str(valid_job_id)][0]["Source"] == "JobManager" - - -def test_set_job_status(normal_user_client: TestClient, valid_job_id: int): - # Arrange - r = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "JobID", - "operator": "eq", - "value": valid_job_id, - } - ] - }, - ) - - assert r.status_code == 200, r.json() - for j in r.json(): - assert j["JobID"] == valid_job_id - assert j["Status"] == JobStatus.RECEIVED.value - assert j["MinorStatus"] == "Job accepted" - assert j["ApplicationStatus"] == "Unknown" - - # Act - new_status = JobStatus.CHECKING.value - new_minor_status = "JobPath" - r = normal_user_client.patch( - "/api/jobs/status", - json={ - valid_job_id: { - datetime.now(tz=timezone.utc).isoformat(): { - "Status": new_status, - "MinorStatus": new_minor_status, - } - } - }, - ) - - # Assert - assert r.status_code == 200, r.json() - assert r.json()["success"][str(valid_job_id)]["Status"] == new_status - assert r.json()["success"][str(valid_job_id)]["MinorStatus"] == new_minor_status - - r = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "JobID", - "operator": "eq", - "value": valid_job_id, - } - ] - }, - ) - assert r.status_code == 200, r.json() - assert r.json()[0]["JobID"] == valid_job_id - assert r.json()[0]["Status"] == new_status - assert r.json()[0]["MinorStatus"] == new_minor_status - assert r.json()[0]["ApplicationStatus"] == "Unknown" - - -def test_set_job_status_invalid_job( - normal_user_client: TestClient, invalid_job_id: int -): - # Act - r = normal_user_client.patch( - "/api/jobs/status", - json={ - invalid_job_id: { - datetime.now(tz=timezone.utc).isoformat(): { - "Status": JobStatus.CHECKING.value, - "MinorStatus": "JobPath", - } - } - }, - ) - - # Assert - assert r.status_code == HTTPStatus.NOT_FOUND, r.json() - assert r.json() == { - "detail": { - "success": {}, - "failed": {str(invalid_job_id): {"detail": "Not found"}}, - } - } - - -def test_set_job_status_offset_naive_datetime_return_bad_request( - normal_user_client: TestClient, - valid_job_id: int, -): - # Act - date = datetime.now(tz=timezone.utc).isoformat(sep=" ").split("+")[0] - r = normal_user_client.patch( - "/api/jobs/status", - json={ - valid_job_id: { - date: { - "Status": JobStatus.CHECKING.value, - "MinorStatus": "JobPath", - } - } - }, - ) - - # Assert - assert r.status_code == HTTPStatus.BAD_REQUEST, r.json() - assert r.json() == { - "detail": f"Timestamp {date} is not timezone aware for job {valid_job_id}" - } - - -def test_set_job_status_cannot_make_impossible_transitions( - normal_user_client: TestClient, valid_job_id: int -): - # Arrange - r = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "JobID", - "operator": "eq", - "value": valid_job_id, - } - ] - }, - ) - assert r.status_code == 200, r.json() - assert r.json()[0]["JobID"] == valid_job_id - assert r.json()[0]["Status"] == JobStatus.RECEIVED.value - assert r.json()[0]["MinorStatus"] == "Job accepted" - assert r.json()[0]["ApplicationStatus"] == "Unknown" - - # Act - new_status = JobStatus.RUNNING.value - new_minor_status = "JobPath" - r = normal_user_client.patch( - "/api/jobs/status", - json={ - valid_job_id: { - datetime.now(tz=timezone.utc).isoformat(): { - "Status": new_status, - "MinorStatus": new_minor_status, - } - } - }, - ) - - # Assert - assert r.status_code == 200, r.json() - success = r.json()["success"] - assert len(success) == 1, r.json() - assert success[str(valid_job_id)]["Status"] != new_status - assert success[str(valid_job_id)]["MinorStatus"] == new_minor_status - - r = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "JobID", - "operator": "eq", - "value": valid_job_id, - } - ] - }, - ) - assert r.status_code == 200, r.json() - assert r.json()[0]["Status"] != new_status - assert r.json()[0]["MinorStatus"] == new_minor_status - assert r.json()[0]["ApplicationStatus"] == "Unknown" - - -def test_set_job_status_force(normal_user_client: TestClient, valid_job_id: int): - # Arrange - r = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "JobID", - "operator": "eq", - "value": valid_job_id, - } - ] - }, - ) - assert r.status_code == 200, r.json() - assert r.json()[0]["JobID"] == valid_job_id - assert r.json()[0]["Status"] == JobStatus.RECEIVED.value - assert r.json()[0]["MinorStatus"] == "Job accepted" - assert r.json()[0]["ApplicationStatus"] == "Unknown" - - # Act - new_status = JobStatus.RUNNING.value - new_minor_status = "JobPath" - r = normal_user_client.patch( - "/api/jobs/status", - json={ - valid_job_id: { - datetime.now(tz=timezone.utc).isoformat(): { - "Status": new_status, - "MinorStatus": new_minor_status, - } - } - }, - params={"force": True}, - ) - - success = r.json()["success"] - - # Assert - assert r.status_code == 200, r.json() - assert success[str(valid_job_id)]["Status"] == new_status - assert success[str(valid_job_id)]["MinorStatus"] == new_minor_status - - r = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "JobID", - "operator": "eq", - "value": valid_job_id, - } - ] - }, - ) - assert r.status_code == 200, r.json() - assert r.json()[0]["JobID"] == valid_job_id - assert r.json()[0]["Status"] == new_status - assert r.json()[0]["MinorStatus"] == new_minor_status - assert r.json()[0]["ApplicationStatus"] == "Unknown" - - -def test_set_job_status_bulk(normal_user_client: TestClient, valid_job_ids): - # Arrange - for job_id in valid_job_ids: - r = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "JobID", - "operator": "eq", - "value": job_id, - } - ] - }, - ) - assert r.status_code == 200, r.json() - assert r.json()[0]["JobID"] == job_id - assert r.json()[0]["Status"] == JobStatus.SUBMITTING.value - assert r.json()[0]["MinorStatus"] == "Bulk transaction confirmation" - - # Act - new_status = JobStatus.CHECKING.value - new_minor_status = "JobPath" - r = normal_user_client.patch( - "/api/jobs/status", - json={ - job_id: { - datetime.now(timezone.utc).isoformat(): { - "Status": new_status, - "MinorStatus": new_minor_status, - } - } - for job_id in valid_job_ids - }, - ) - - success = r.json()["success"] - - # Assert - assert r.status_code == 200, r.json() - for job_id in valid_job_ids: - assert success[str(job_id)]["Status"] == new_status - assert success[str(job_id)]["MinorStatus"] == new_minor_status - - r_get = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "JobID", - "operator": "eq", - "value": job_id, - } - ] - }, - ) - assert r_get.status_code == 200, r_get.json() - assert r_get.json()[0]["JobID"] == job_id - assert r_get.json()[0]["Status"] == new_status - assert r_get.json()[0]["MinorStatus"] == new_minor_status - assert r_get.json()[0]["ApplicationStatus"] == "Unknown" - - -def test_set_job_status_with_invalid_job_id( - normal_user_client: TestClient, invalid_job_id: int -): - # Act - r = normal_user_client.patch( - "/api/jobs/status", - json={ - invalid_job_id: { - datetime.now(tz=timezone.utc).isoformat(): { - "Status": JobStatus.CHECKING.value, - "MinorStatus": "JobPath", - } - }, - }, - ) - - # Assert - assert r.status_code == HTTPStatus.NOT_FOUND, r.json() - assert r.json()["detail"] == { - "success": {}, - "failed": {str(invalid_job_id): {"detail": "Not found"}}, - } - - -def test_insert_and_reschedule(normal_user_client: TestClient): - job_definitions = [TEST_JDL] - r = normal_user_client.post("/api/jobs/jdl", json=job_definitions) - assert r.status_code == 200, r.json() - assert len(r.json()) == len(job_definitions) - - submitted_job_ids = sorted([job_dict["JobID"] for job_dict in r.json()]) - - # Test /jobs/reschedule and - # test max_reschedule - - max_resched = 3 - jid = str(submitted_job_ids[0]) - - for i in range(max_resched): - r = normal_user_client.post( - "/api/jobs/reschedule", - params={"job_ids": submitted_job_ids}, - ) - assert r.status_code == 200, r.json() - result = r.json() - successful_results = result["success"] - assert jid in successful_results, result - assert successful_results[jid]["Status"] == JobStatus.RECEIVED - assert successful_results[jid]["MinorStatus"] == "Job Rescheduled" - assert successful_results[jid]["RescheduleCounter"] == i + 1 - - r = normal_user_client.post( - "/api/jobs/reschedule", - params={"job_ids": submitted_job_ids}, - ) - assert ( - r.status_code != 200 - ), f"Rescheduling more than {max_resched} times should have failed by now {r.json()}" - assert r.json() == { - "detail": { - "success": [], - "failed": { - "1": { - "detail": f"Maximum number of reschedules exceeded ({max_resched})" - } - }, - } - } - - -## test edge case for rescheduling - - -def test_reschedule_job_attr_update(normal_user_client: TestClient): - job_definitions = [TEST_JDL] * 15 - - r = normal_user_client.post("/api/jobs/jdl", json=job_definitions) - assert r.status_code == 200, r.json() - assert len(r.json()) == len(job_definitions) - - submitted_job_ids = sorted([job_dict["JobID"] for job_dict in r.json()]) - - # Test /jobs/reschedule and - # test max_reschedule - - max_resched = 3 - - fail_resched_ids = submitted_job_ids[0:5] - good_resched_ids = list(set(submitted_job_ids) - set(fail_resched_ids)) - - for i in range(max_resched): - r = normal_user_client.post( - "/api/jobs/reschedule", - params={"job_ids": fail_resched_ids}, - ) - assert r.status_code == 200, r.json() - result = r.json() - successful_results = result["success"] - for jid in fail_resched_ids: - assert str(jid) in successful_results, result - assert successful_results[str(jid)]["Status"] == JobStatus.RECEIVED - assert successful_results[str(jid)]["MinorStatus"] == "Job Rescheduled" - assert successful_results[str(jid)]["RescheduleCounter"] == i + 1 - - for i in range(max_resched): - r = normal_user_client.post( - "/api/jobs/reschedule", - params={"job_ids": submitted_job_ids}, - ) - assert r.status_code == 200, r.json() - result = r.json() - successful_results = result["success"] - failed_results = result["failed"] - for jid in good_resched_ids: - assert str(jid) in successful_results, result - assert successful_results[str(jid)]["Status"] == JobStatus.RECEIVED - assert successful_results[str(jid)]["MinorStatus"] == "Job Rescheduled" - assert successful_results[str(jid)]["RescheduleCounter"] == i + 1 - for jid in fail_resched_ids: - assert str(jid) in failed_results, result - # assert successful_results[jid]["Status"] == JobStatus.RECEIVED - # assert successful_results[jid]["MinorStatus"] == "Job Rescheduled" - # assert successful_results[jid]["RescheduleCounter"] == i + 1 - - r = normal_user_client.post( - "/api/jobs/reschedule", - params={"job_ids": submitted_job_ids}, - ) - assert ( - r.status_code != 200 - ), f"Rescheduling more than {max_resched} times should have failed by now {r.json()}" - assert r.json() == { - "detail": { - "success": [], - "failed": { - str(i): { - "detail": f"Maximum number of reschedules exceeded ({max_resched})" - } - for i in good_resched_ids + fail_resched_ids - }, - } - } - - -# Test delete job - - -def test_delete_job_valid_job_id(normal_user_client: TestClient, valid_job_id: int): - # Act - r = normal_user_client.patch( - "/api/jobs/status", - json={ - valid_job_id: { - str(datetime.now(tz=timezone.utc)): { - "Status": JobStatus.DELETED, - "MinorStatus": "Checking accounting", - } - } - }, - ) - - # Assert - assert r.status_code == 200, r.json() - r = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "JobID", - "operator": "eq", - "value": valid_job_id, - } - ] - }, - ) - assert r.status_code == 200, r.json() - assert r.json()[0]["JobID"] == valid_job_id - assert r.json()[0]["Status"] == JobStatus.DELETED - assert r.json()[0]["MinorStatus"] == "Checking accounting" - assert r.json()[0]["ApplicationStatus"] == "Unknown" - - -def test_delete_job_invalid_job_id(normal_user_client: TestClient, invalid_job_id: int): - # Act - r = normal_user_client.patch( - "/api/jobs/status", - json={ - invalid_job_id: { - str(datetime.now(tz=timezone.utc)): { - "Status": JobStatus.DELETED, - "MinorStatus": "Checking accounting", - } - } - }, - ) - # Assert - assert r.status_code == HTTPStatus.NOT_FOUND, r.json() - assert r.json()["detail"]["failed"] == { - str(invalid_job_id): {"detail": "Not found"} - } - - -def test_delete_bulk_jobs_valid_job_ids( - normal_user_client: TestClient, valid_job_ids: list[int] -): - # Act - r = normal_user_client.patch( - "/api/jobs/status", - json={ - job_id: { - str(datetime.now(tz=timezone.utc)): { - "Status": JobStatus.DELETED, - "MinorStatus": "Checking accounting", - } - } - for job_id in valid_job_ids - }, - ) - req = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "JobID", - "operator": "in", - "values": valid_job_ids, - } - ] - }, - ) - assert req.status_code == 200, req.json() - - r = {i["JobID"]: i for i in req.json()} - for valid_job_id in valid_job_ids: - assert r[valid_job_id]["Status"] == JobStatus.DELETED - assert r[valid_job_id]["MinorStatus"] == "Checking accounting" - assert r[valid_job_id]["ApplicationStatus"] == "Unknown" - - -def test_delete_bulk_jobs_invalid_job_ids( - normal_user_client: TestClient, invalid_job_ids: list[int] -): - # Act - r = normal_user_client.patch( - "/api/jobs/status", - json={ - job_id: { - str(datetime.now(tz=timezone.utc)): { - "Status": JobStatus.DELETED, - "MinorStatus": "Checking accounting", - } - } - for job_id in invalid_job_ids - }, - ) - - # Assert - assert r.status_code == HTTPStatus.NOT_FOUND, r.json() - assert r.json() == { - "detail": { - "success": {}, - "failed": {str(jid): {"detail": "Not found"} for jid in invalid_job_ids}, - } - } - - -def test_delete_bulk_jobs_mix_of_valid_and_invalid_job_ids( - normal_user_client: TestClient, valid_job_ids: list[int], invalid_job_ids: list[int] -): - # Arrange - job_ids = valid_job_ids + invalid_job_ids - - # Act - r = normal_user_client.patch( - "/api/jobs/status", - json={ - job_id: { - str(datetime.now(tz=timezone.utc)): { - "Status": JobStatus.DELETED, - "MinorStatus": "Checking accounting", - } - } - for job_id in job_ids - }, - ) - - # Assert - assert r.status_code == HTTPStatus.OK, r.json() - resp = r.json() - - assert len(resp["success"]) == len(valid_job_ids) - assert resp["failed"] == { - "999999997": {"detail": "Not found"}, - "999999998": {"detail": "Not found"}, - "999999999": {"detail": "Not found"}, - } - - req = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "JobID", - "operator": "in", - "values": valid_job_ids, - } - ] - }, - ) - assert req.status_code == 200, req.json() - - r = req.json() - assert len(r) == len(valid_job_ids), r - for job in r: - assert job["Status"] == JobStatus.DELETED - assert job["MinorStatus"] == "Checking accounting" - - -# Test kill job -def test_kill_job_valid_job_id(normal_user_client: TestClient, valid_job_id: int): - # Act - r = normal_user_client.patch( - "/api/jobs/status", - json={ - valid_job_id: { - str(datetime.now(timezone.utc)): { - "Status": JobStatus.KILLED, - "MinorStatus": "Marked for termination", - } - } - }, - ) - - # Assert - assert r.status_code == 200, r.json() - - successful = r.json()["success"] - assert successful[str(valid_job_id)]["Status"] == JobStatus.KILLED - req = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "JobID", - "operator": "eq", - "value": valid_job_id, - } - ] - }, - ) - assert req.status_code == 200, successful - assert req.json()[0]["JobID"] == valid_job_id - assert req.json()[0]["Status"] == JobStatus.KILLED - assert req.json()[0]["MinorStatus"] == "Marked for termination" - assert req.json()[0]["ApplicationStatus"] == "Unknown" - - -def test_kill_job_invalid_job_id(normal_user_client: TestClient, invalid_job_id: int): - # Act - r = normal_user_client.patch( - "/api/jobs/status", - json={ - int(invalid_job_id): { - str(datetime.now(timezone.utc)): { - "Status": JobStatus.KILLED, - "MinorStatus": "Marked for termination", - } - } - }, - ) - - # Assert - assert r.status_code == HTTPStatus.NOT_FOUND, r.json() - assert r.json()["detail"] == { - "success": {}, - "failed": {str(invalid_job_id): {"detail": "Not found"}}, - } - - -def test_kill_bulk_jobs_valid_job_ids( - normal_user_client: TestClient, valid_job_ids: list[int] -): - # Act - r = normal_user_client.patch( - "/api/jobs/status", - json={ - job_id: { - str(datetime.now(timezone.utc)): { - "Status": JobStatus.KILLED, - "MinorStatus": "Marked for termination", - } - } - for job_id in valid_job_ids - }, - ) - result = r.json() - - # Assert - assert r.status_code == 200, result - req = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "JobID", - "operator": "in", - "values": valid_job_ids, - } - ] - }, - ) - assert req.status_code == 200, req.json() - - r = req.json() - assert len(r) == len(valid_job_ids), r - for job in r: - assert job["Status"] == JobStatus.KILLED - assert job["MinorStatus"] == "Marked for termination" - assert job["ApplicationStatus"] == "Unknown" - - -def test_kill_bulk_jobs_invalid_job_ids( - normal_user_client: TestClient, invalid_job_ids: list[int] -): - # Act - r = normal_user_client.patch( - "/api/jobs/status", - json={ - job_id: { - str(datetime.now(timezone.utc)): { - "Status": JobStatus.KILLED, - "MinorStatus": "Marked for termination", - } - } - for job_id in invalid_job_ids - }, - ) - # Assert - assert r.status_code == HTTPStatus.NOT_FOUND, r.json() - - assert r.json()["detail"] == { - "success": {}, - "failed": { - "999999997": {"detail": "Not found"}, - "999999998": {"detail": "Not found"}, - "999999999": {"detail": "Not found"}, - }, - } - - -def test_kill_bulk_jobs_mix_of_valid_and_invalid_job_ids( - normal_user_client: TestClient, valid_job_ids: list[int], invalid_job_ids: list[int] -): - # Arrange - job_ids = valid_job_ids + invalid_job_ids - - # Act - r = normal_user_client.patch( - "/api/jobs/status", - json={ - job_id: { - str(datetime.now(timezone.utc)): { - "Status": JobStatus.KILLED, - "MinorStatus": "Marked for termination", - } - } - for job_id in job_ids - }, - ) - # Assert - assert r.status_code == HTTPStatus.OK, r.json() - resp = r.json() - - assert len(resp["success"]) == len(valid_job_ids) - assert resp["failed"] == { - "999999997": {"detail": "Not found"}, - "999999998": {"detail": "Not found"}, - "999999999": {"detail": "Not found"}, - } - - req = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "JobID", - "operator": "in", - "values": valid_job_ids, - } - ] - }, - ) - assert req.status_code == 200, req.json() - - r = req.json() - assert len(r) == len(valid_job_ids), r - for job in r: - assert job["Status"] == JobStatus.KILLED - assert job["MinorStatus"] == "Marked for termination" - assert job["ApplicationStatus"] == "Unknown" - - -# Test remove job - - -def test_remove_job_valid_job_id(normal_user_client: TestClient, valid_job_id: int): - r = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "JobID", - "operator": "eq", - "value": valid_job_id, - } - ] - }, - ) - assert r.status_code == HTTPStatus.OK, r.json() - assert r.json() != [] - - # Act - r = normal_user_client.delete( - "/api/jobs/", - params={ - "job_ids": [valid_job_id], - }, - ) - - assert r.status_code == HTTPStatus.OK, r.json() - - # Assert - assert r.status_code == 200, r.json() - r = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "JobID", - "operator": "eq", - "value": valid_job_id, - } - ] - }, - ) - assert r.status_code == HTTPStatus.OK, r.json() - assert r.json() == [] - - -def test_remove_job_invalid_job_id(normal_user_client: TestClient, invalid_job_id: int): - # Act - r = normal_user_client.delete( - "/api/jobs/", - params={ - "job_ids": [invalid_job_id], - }, - ) - - # Assert - assert r.status_code == 200, r.json() - - -def test_remove_jobs_valid_job_ids( - normal_user_client: TestClient, valid_job_ids: list[int] -): - # Act - r = normal_user_client.delete("/api/jobs/", params={"job_ids": valid_job_ids}) - - # Assert - assert r.status_code == 200, r.json() - for job_id in valid_job_ids: - r = normal_user_client.get(f"/api/jobs/{job_id}/status") - assert r.status_code == HTTPStatus.NOT_FOUND, r.json() - - -# Test setting job properties - - -def test_set_single_job_properties(normal_user_client: TestClient, valid_job_id: int): - pytest.skip("There seems to be a missing route for this - TODO") - - job_id = str(valid_job_id) - - initial_job_state = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "JobID", - "operator": "eq", - "value": job_id, - } - ] - }, - ).json()[0] - - initial_user_priority = initial_job_state["UserPriority"] - initial_application_status = initial_job_state["ApplicationStatus"] - initial_last_update_time = initial_job_state["LastUpdateTime"] - - # Update just one property - res = normal_user_client.patch( - "/api/jobs/", - json={valid_job_id: {"UserPriority": 2}}, - ) - assert res.status_code == 200, res.json() - - new_job_state = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "JobID", - "operator": "eq", - "value": job_id, - } - ] - }, - ).json()[0] - new_user_priority = new_job_state["UserPriority"] - new_application_status = new_job_state["ApplicationStatus"] - - assert initial_application_status == new_application_status - assert initial_user_priority != new_user_priority - assert new_user_priority == 2 - assert new_job_state["LastUpdateTime"] == initial_last_update_time - - # Update two properties - res = normal_user_client.patch( - f"/api/jobs/{job_id}", - json={"UserPriority": initial_user_priority, "ApplicationStatus": "Crack"}, - params={"update_timestamp": True}, - ) - assert res.status_code == 200, res.json() - - new_job_state = normal_user_client.post( - "/api/jobs/search", - json={ - "search": [ - { - "parameter": "JobID", - "operator": "eq", - "value": job_id, - } - ] - }, - ).json()[0] - new_user_priority = new_job_state["UserPriority"] - new_application_status = new_job_state["ApplicationStatus"] - - assert initial_application_status != new_application_status - assert new_application_status == "Crack" - assert initial_user_priority == new_user_priority - assert new_job_state["LastUpdateTime"] != initial_last_update_time - - -def test_set_single_job_properties_non_existing_job( - normal_user_client: TestClient, invalid_job_id: int -): - pytest.skip("There seems to be a missing route for this - TODO") - job_id = str(invalid_job_id) - - res = normal_user_client.patch( - "/api/jobs/", - json={job_id: {"UserPriority": 2}}, - ) - assert res.status_code == HTTPStatus.NOT_FOUND, res.json() - - -# def test_remove_jobs_invalid_job_ids( -# normal_user_client: TestClient, invalid_job_ids: list[int] -# ): -# # Act -# r = normal_user_client.post("/api/jobs/remove", params={"job_ids": invalid_job_ids}) - -# # Assert -# assert r.status_code == HTTPStatus.NOT_FOUND, r.json() -# assert r.json() == { -# "detail": { -# "message": f"Failed to remove {len(invalid_job_ids)} jobs out of {len(invalid_job_ids)}", -# "failed_ids": { -# str(invalid_job_id): f"Job {invalid_job_id} not found" -# for invalid_job_id in invalid_job_ids -# }, -# } -# } - - -# def test_remove_jobs_mix_of_valid_and_invalid_job_ids( -# normal_user_client: TestClient, valid_job_ids: list[int], invalid_job_ids: list[int] -# ): -# # Arrange -# job_ids = valid_job_ids + invalid_job_ids - -# # Act -# r = normal_user_client.post("/api/jobs/remove", params={"job_ids": job_ids}) - -# # Assert -# assert r.status_code == HTTPStatus.NOT_FOUND, r.json() -# assert r.json() == { -# "detail": { -# "message": f"Failed to remove {len(invalid_job_ids)} jobs out of {len(job_ids)}", -# "failed_ids": { -# str(invalid_job_id): f"Job {invalid_job_id} not found" -# for invalid_job_id in invalid_job_ids -# }, -# } -# } -# for job_id in valid_job_ids: -# r = normal_user_client.get(f"/api/jobs/{job_id}/status") -# assert r.status_code == HTTPStatus.NOT_FOUND, r.json()