diff --git a/setup.cfg b/setup.cfg index 4b450a14b..9987df2a1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -60,6 +60,11 @@ markers = slow: mark test to be slow remote: mark test with remote Weaver instance requirement vault: mark test with Vault file feature validation + html: mark test as related to HTML rendering + oap_part1: mark test as 'OGC API - Processes - Part 1: Core' functionalities + oap_part2: mark test as 'OGC API - Processes - Part 2: Deploy, Replace, Undeploy (DRU)' functionalities + oap_part3: mark test as 'OGC API - Processes - Part 3: Workflows and Chaining' functionalities + oap_part4: mark test as 'OGC API - Processes - Part 4: Job Management' functionalities filterwarnings = ignore:No file specified for WPS-1 providers registration:RuntimeWarning ignore:.*configuration setting.*weaver\.cwl_processes_dir.*:RuntimeWarning diff --git a/tests/functional/test_wps_package.py b/tests/functional/test_wps_package.py index 8af4e09e4..163e420f1 100644 --- a/tests/functional/test_wps_package.py +++ b/tests/functional/test_wps_package.py @@ -3598,6 +3598,7 @@ def fix_result_multipart_indent(results): res_dedent = res_dedent.rstrip("\n ") # last line often indented less because of closing multiline string return res_dedent + @pytest.mark.oap_part1 def test_execute_single_output_prefer_header_return_representation_literal(self): proc = "EchoResultsTester" p_id = self.fully_qualified_test_process_name(proc) @@ -3644,6 +3645,7 @@ def test_execute_single_output_prefer_header_return_representation_literal(self) }, } + @pytest.mark.oap_part1 def test_execute_single_output_prefer_header_return_representation_complex(self): proc = "EchoResultsTester" p_id = self.fully_qualified_test_process_name(proc) @@ -3693,6 +3695,7 @@ def test_execute_single_output_prefer_header_return_representation_complex(self) }, } + @pytest.mark.oap_part1 def test_execute_single_output_prefer_header_return_minimal_literal_accept_default(self): """ For single requested output, without ``Accept`` content negotiation, its default format is returned directly. @@ -3746,6 +3749,7 @@ def test_execute_single_output_prefer_header_return_minimal_literal_accept_defau }, } + @pytest.mark.oap_part1 def test_execute_single_output_prefer_header_return_minimal_literal_accept_json(self): """ For single requested output, with ``Accept`` :term:`JSON` content negotiation, document response is returned. @@ -3801,6 +3805,7 @@ def test_execute_single_output_prefer_header_return_minimal_literal_accept_json( }, } + @pytest.mark.oap_part1 def test_execute_single_output_prefer_header_return_minimal_complex_accept_default(self): """ For single requested output, without ``Accept`` content negotiation, its default format is returned by link. @@ -3880,6 +3885,7 @@ def test_execute_single_output_prefer_header_return_minimal_complex_accept_defau }, } + @pytest.mark.oap_part1 def test_execute_single_output_prefer_header_return_minimal_complex_accept_json(self): """ For single requested output, with ``Accept`` :term:`JSON` content negotiation, document response is returned. @@ -3955,6 +3961,7 @@ def test_execute_single_output_prefer_header_return_minimal_complex_accept_json( }, } + @pytest.mark.oap_part1 def test_execute_single_output_response_raw_value_literal(self): proc = "EchoResultsTester" p_id = self.fully_qualified_test_process_name(proc) @@ -3999,6 +4006,7 @@ def test_execute_single_output_response_raw_value_literal(self): }, } + @pytest.mark.oap_part1 def test_execute_single_output_response_raw_value_complex(self): """ Since value transmission is requested for a single output, its :term:`JSON` contents are returned directly. @@ -4054,6 +4062,7 @@ def test_execute_single_output_response_raw_value_complex(self): }, } + @pytest.mark.oap_part1 def test_execute_single_output_response_raw_reference_literal(self): proc = "EchoResultsTester" p_id = self.fully_qualified_test_process_name(proc) @@ -4113,6 +4122,7 @@ def test_execute_single_output_response_raw_reference_literal(self): }, } + @pytest.mark.oap_part1 def test_execute_single_output_response_raw_reference_complex(self): proc = "EchoResultsTester" p_id = self.fully_qualified_test_process_name(proc) @@ -4172,6 +4182,7 @@ def test_execute_single_output_response_raw_reference_complex(self): }, } + @pytest.mark.oap_part1 def test_execute_single_output_multipart_accept_data(self): """ Validate that requesting multipart for a single output is permitted. @@ -4250,6 +4261,7 @@ def test_execute_single_output_multipart_accept_data(self): }, } + @pytest.mark.oap_part1 def test_execute_single_output_multipart_accept_link(self): """ Validate that requesting multipart for a single output is permitted. @@ -4326,6 +4338,7 @@ def test_execute_single_output_multipart_accept_link(self): } # FIXME: implement (https://github.com/crim-ca/weaver/pull/548) + @pytest.mark.oap_part1 @pytest.mark.xfail(reason="not implemented") def test_execute_single_output_multipart_accept_alt_format(self): """ @@ -4408,6 +4421,7 @@ def test_execute_single_output_multipart_accept_alt_format(self): assert result_json.text == "{\"data\":\"test\"}" # FIXME: implement (https://github.com/crim-ca/weaver/pull/548) + @pytest.mark.oap_part1 @pytest.mark.xfail(reason="not implemented") def test_execute_single_output_response_document_alt_format_yaml(self): proc = "EchoResultsTester" @@ -4484,6 +4498,7 @@ def test_execute_single_output_response_document_alt_format_yaml(self): assert result_json.content_type == ContentType.APP_JSON assert result_json.text == "{\"data\":\"test\"}" + @pytest.mark.oap_part1 def test_execute_single_output_response_document_alt_format_json_raw_literal(self): proc = "EchoResultsTester" p_id = self.fully_qualified_test_process_name(proc) @@ -4555,6 +4570,7 @@ def test_execute_single_output_response_document_alt_format_json_raw_literal(sel # assert result_json.content_type == ContentType.APP_JSON # assert result_json.json == {"data": "test"} + @pytest.mark.oap_part1 def test_execute_single_output_response_document_default_format_json_special(self): """ Validate that a :term:`JSON` output is directly embedded in a ``document`` response also using :term:`JSON`. @@ -4631,6 +4647,7 @@ def test_execute_single_output_response_document_default_format_json_special(sel }, } + @pytest.mark.oap_part1 @parameterized.expand([ ContentType.MULTIPART_ANY, ContentType.MULTIPART_MIXED, @@ -4724,6 +4741,7 @@ def test_execute_multi_output_multipart_accept(self, multipart_header): }, } + @pytest.mark.oap_part1 def test_execute_multi_output_multipart_accept_async_not_acceptable(self): """ When executing the process asynchronously, ``Accept`` with multipart (strictly) is not acceptable. @@ -4766,6 +4784,7 @@ def test_execute_multi_output_multipart_accept_async_not_acceptable(self): "in": "headers", } + @pytest.mark.oap_part1 def test_execute_multi_output_multipart_accept_async_alt_acceptable(self): """ When executing the process asynchronously, ``Accept`` with multipart and an alternative is acceptable. @@ -4806,6 +4825,7 @@ def test_execute_multi_output_multipart_accept_async_alt_acceptable(self): assert "Preference-Applied" in resp.headers assert resp.headers["Preference-Applied"] == prefer_header.replace(",", ";") + @pytest.mark.oap_part1 def test_execute_multi_output_prefer_header_return_representation(self): proc = "EchoResultsTester" p_id = self.fully_qualified_test_process_name(proc) @@ -4881,6 +4901,7 @@ def test_execute_multi_output_prefer_header_return_representation(self): }, } + @pytest.mark.oap_part1 def test_execute_multi_output_response_raw_value(self): proc = "EchoResultsTester" p_id = self.fully_qualified_test_process_name(proc) @@ -4954,6 +4975,7 @@ def test_execute_multi_output_response_raw_value(self): }, } + @pytest.mark.oap_part1 def test_execute_multi_output_response_raw_reference_default_links(self): """ All outputs resolved as reference (explicitly or inferred) with raw representation should be all Link headers. @@ -5028,6 +5050,7 @@ def test_execute_multi_output_response_raw_reference_default_links(self): }, } + @pytest.mark.oap_part1 def test_execute_multi_output_response_raw_reference_accept_multipart(self): """ Requesting ``multipart`` explicitly should return it instead of default ``Link`` headers response. @@ -5115,6 +5138,7 @@ def test_execute_multi_output_response_raw_reference_accept_multipart(self): }, } + @pytest.mark.oap_part1 def test_execute_multi_output_response_raw_mixed(self): proc = "EchoResultsTester" p_id = self.fully_qualified_test_process_name(proc) @@ -5200,6 +5224,7 @@ def test_execute_multi_output_response_raw_mixed(self): }, } + @pytest.mark.oap_part1 def test_execute_multi_output_prefer_header_return_minimal_defaults(self): """ Test ``Prefer: return=minimal`` with default ``transmissionMode`` resolutions for literal/complex outputs. @@ -5264,6 +5289,7 @@ def test_execute_multi_output_prefer_header_return_minimal_defaults(self): }, } + @pytest.mark.oap_part1 def test_execute_multi_output_prefer_header_return_minimal_override_transmission(self): """ Test ``Prefer: return=minimal`` with ``transmissionMode`` overrides. @@ -5344,6 +5370,7 @@ def test_execute_multi_output_prefer_header_return_minimal_override_transmission }, } + @pytest.mark.oap_part1 def test_execute_multi_output_response_document_defaults(self): """ Test ``response: document`` with default ``transmissionMode`` resolutions for literal/complex outputs. @@ -5408,6 +5435,7 @@ def test_execute_multi_output_response_document_defaults(self): }, } + @pytest.mark.oap_part1 def test_execute_multi_output_response_document_mixed(self): """ Test ``response: document`` with ``transmissionMode`` specified to force convertion of literal/complex outputs. @@ -5485,6 +5513,18 @@ def test_execute_multi_output_response_document_mixed(self): }, } + @pytest.mark.oap_part4 + def test_execute_jobs_sync(self): + raise NotImplementedError # FIMXE: POST /jobs with 'Prefer: wait=X' and return results directly + + @pytest.mark.oap_part4 + def test_execute_jobs_async(self): + raise NotImplementedError # FIMXE: POST /jobs with 'Prefer: respond-asny' and GET /jobs/{jobId}/results + + @pytest.mark.oap_part4 + def test_execute_jobs_create_trigger(self): + raise NotImplementedError # FIMXE: POST /jobs with 'status:create' and POST /jobs/{jobId}/results to trigger + @pytest.mark.functional class WpsPackageAppWithS3BucketTest(WpsConfigBase, ResourcesUtil): diff --git a/tests/wps_restapi/test_jobs.py b/tests/wps_restapi/test_jobs.py index db76a413c..b7f194aa0 100644 --- a/tests/wps_restapi/test_jobs.py +++ b/tests/wps_restapi/test_jobs.py @@ -283,6 +283,7 @@ def check_basic_jobs_grouped_info(response, groups): total += grouped_jobs["count"] assert total == response.json["total"] + @pytest.mark.oap_part1 def test_get_jobs_normal_paged(self): resp = self.app.get(sd.jobs_service.path, headers=self.json_headers) self.check_basic_jobs_info(resp) @@ -324,6 +325,8 @@ def test_get_jobs_detail_grouped(self): for job in grouped_jobs["jobs"]: self.check_job_format(job) + @pytest.mark.html + @pytest.mark.oap_part1 @parameterized.expand([ ({}, ), # detail omitted should apply it for HTML, unlike JSON that returns the simplified listing by default ({"detail": None}, ), @@ -349,6 +352,7 @@ def test_get_jobs_detail_html_enforced(self, params): jobs = [line for line in resp.text.splitlines() if "job-list-item" in line] assert len(jobs) == 6 + @pytest.mark.html def test_get_jobs_groups_html_unsupported(self): groups = ["process", "service"] path = get_path_kvp(sd.jobs_service.path, groups=groups) @@ -426,6 +430,7 @@ def test_get_jobs_valid_grouping_by_provider(self): """ self.template_get_jobs_valid_grouping_by_service_provider("provider") + @pytest.mark.oap_part1 def test_get_jobs_links_navigation(self): """ Verifies that relation links update according to context in order to allow natural navigation between responses. @@ -545,6 +550,7 @@ def test_get_jobs_links_navigation(self): assert links["first"].startswith(jobs_url) and limit_kvp in links["first"] and "page=0" in links["first"] assert links["last"].startswith(jobs_url) and limit_kvp in links["last"] and "page=0" in links["last"] + @pytest.mark.oap_part1 def test_get_jobs_page_out_of_range(self): resp = self.app.get(sd.jobs_service.path, headers=self.json_headers) total = resp.json["total"] @@ -609,8 +615,8 @@ def test_get_jobs_by_encrypted_email(self): # verify the email is not in plain text job = self.job_store.fetch_by_id(job_id) - assert job.notification_email != email and job.notification_email is not None - assert decrypt_email(job.notification_email, self.settings) == email, "Email should be recoverable." + assert job.notification_email != email and job.notification_email is not None # noqa + assert decrypt_email(job.notification_email, self.settings) == email, "Email should be recoverable." # noqa # make sure that jobs searched using email are found with encryption transparently for the user path = get_path_kvp(sd.jobs_service.path, detail="true", notification_email=email) @@ -620,6 +626,7 @@ def test_get_jobs_by_encrypted_email(self): assert resp.json["total"] == 1, "Should match exactly 1 email with specified literal string as query param." assert resp.json["jobs"][0]["jobID"] == job_id + @pytest.mark.oap_part1 def test_get_jobs_by_type_process(self): path = get_path_kvp(sd.jobs_service.path, type="process") resp = self.app.get(path, headers=self.json_headers) @@ -758,6 +765,7 @@ def test_get_jobs_process_unknown_in_query(self): assert resp.status_code == 404 assert resp.content_type == ContentType.APP_JSON + @pytest.mark.oap_part1 @parameterized.expand([ get_path_kvp( sd.jobs_service.path, @@ -861,9 +869,9 @@ def test_get_jobs_private_service_public_process_forbidden_access_in_query(self) def test_get_jobs_public_service_private_process_forbidden_access_in_query(self): """ - NOTE: - it is up to the remote service to hide private processes - if the process is visible, the a job can be executed and it is automatically considered public + .. note:: + It is up to the remote service to hide private processes. + If the process is visible, the job can be executed and it is automatically considered public. """ path = get_path_kvp(sd.jobs_service.path, service=self.service_public.name, @@ -877,9 +885,9 @@ def test_get_jobs_public_service_private_process_forbidden_access_in_query(self) def test_get_jobs_public_service_no_processes(self): """ - NOTE: - it is up to the remote service to hide private processes - if the process is invisible, no job should have been executed nor can be fetched + .. note:: + It is up to the remote service to hide private processes. + If the process is invisible, no job should have been executed nor can be fetched. """ path = get_path_kvp(sd.jobs_service.path, service=self.service_public.name, @@ -964,6 +972,7 @@ def filter_service(jobs): # type: (Iterable[Job]) -> List[Job] test_values = {"path": path, "access": access, "user_id": user_id} self.assert_equal_with_jobs_diffs(job_result, job_expect, test_values, index=i) + @pytest.mark.oap_part1 def test_jobs_list_with_limit_api(self): """ Test handling of ``limit`` query parameter when listing jobs. @@ -982,6 +991,7 @@ def test_jobs_list_with_limit_api(self): assert resp.json["limit"] == limit_parameter assert len(resp.json["jobs"]) <= limit_parameter + @pytest.mark.oap_part1 def test_jobs_list_schema_not_required_fields(self): """ Test that job listing query parameters for filtering results are marked as optional in OpenAPI schema. @@ -1104,6 +1114,7 @@ def test_get_jobs_datetime_interval(self): assert date_parser.parse(resp.json["created"]) >= date_parser.parse(datetime_after) assert date_parser.parse(resp.json["created"]) <= date_parser.parse(datetime_before) + @pytest.mark.oap_part1 def test_get_jobs_datetime_match(self): """ Test that only filtered jobs at a specific time are returned when ``datetime`` query parameter is provided. @@ -1127,6 +1138,7 @@ def test_get_jobs_datetime_match(self): assert resp.content_type == ContentType.APP_JSON assert date_parser.parse(resp.json["created"]) == date_parser.parse(datetime_match) + @pytest.mark.oap_part1 def test_get_jobs_datetime_invalid(self): """ Test that incorrectly formatted ``datetime`` query parameter value is handled. @@ -1144,6 +1156,7 @@ def test_get_jobs_datetime_invalid(self): resp = self.app.get(path, headers=self.json_headers, expect_errors=True) assert resp.status_code == 400 + @pytest.mark.oap_part1 def test_get_jobs_datetime_interval_invalid(self): """ Test that invalid ``datetime`` query parameter value is handled. @@ -1161,6 +1174,7 @@ def test_get_jobs_datetime_interval_invalid(self): resp = self.app.get(path, headers=self.json_headers, expect_errors=True) assert resp.status_code == 422 + @pytest.mark.oap_part1 def test_get_jobs_datetime_before_invalid(self): """ Test that invalid ``datetime`` query parameter value with a range is handled. @@ -1177,6 +1191,7 @@ def test_get_jobs_datetime_before_invalid(self): resp = self.app.get(path, headers=self.json_headers, expect_errors=True) assert resp.status_code == 400 + @pytest.mark.oap_part1 def test_get_jobs_duration_min_only(self): test = {"minDuration": 35} path = get_path_kvp(sd.jobs_service.path, **test) @@ -1203,6 +1218,7 @@ def test_get_jobs_duration_min_only(self): expect_jobs = [self.job_info[i].id for i in [8]] self.assert_equal_with_jobs_diffs(result_jobs, expect_jobs, test) + @pytest.mark.oap_part1 def test_get_jobs_duration_max_only(self): test = {"maxDuration": 30} path = get_path_kvp(sd.jobs_service.path, **test) @@ -1224,6 +1240,7 @@ def test_get_jobs_duration_max_only(self): expect_jobs = [self.job_info[i].id for i in expect_idx] self.assert_equal_with_jobs_diffs(result_jobs, expect_jobs, test) + @pytest.mark.oap_part1 def test_get_jobs_duration_min_max(self): # note: avoid range <35s for this test to avoid sudden dynamic duration of 9, 10 becoming within min/max test = {"minDuration": 35, "maxDuration": 60} @@ -1249,6 +1266,7 @@ def test_get_jobs_duration_min_max(self): result_jobs = resp.json["jobs"] assert len(result_jobs) == 0 + @pytest.mark.oap_part1 def test_get_jobs_duration_min_max_invalid(self): test = {"minDuration": 30, "maxDuration": 20} path = get_path_kvp(sd.jobs_service.path, **test) @@ -1270,6 +1288,7 @@ def test_get_jobs_duration_min_max_invalid(self): resp = self.app.get(path, headers=self.json_headers, expect_errors=True) assert resp.status_code in [400, 422] + @pytest.mark.oap_part1 def test_get_jobs_by_status_single(self): test = {"status": Status.SUCCEEDED} path = get_path_kvp(sd.jobs_service.path, **test) @@ -1287,6 +1306,7 @@ def test_get_jobs_by_status_single(self): result_jobs = resp.json["jobs"] self.assert_equal_with_jobs_diffs(result_jobs, expect_jobs, test) + @pytest.mark.oap_part1 def test_get_jobs_by_status_multi(self): test = {"status": f"{Status.SUCCEEDED},{Status.RUNNING}"} path = get_path_kvp(sd.jobs_service.path, **test) @@ -1312,6 +1332,7 @@ def test_get_jobs_by_status_invalid(self): assert resp.json["value"]["status"] == status assert "status" in resp.json["cause"] + @pytest.mark.oap_part1 def test_get_job_status_response_process_id(self): """ Verify the processID value in the job status response. @@ -1332,6 +1353,7 @@ def test_get_job_status_response_process_id(self): assert resp.json["processID"] == "process-public" + @pytest.mark.oap_part1 def test_get_job_invalid_uuid(self): """ Test handling of invalid UUID reference to search job. @@ -1350,6 +1372,7 @@ def test_get_job_invalid_uuid(self): assert resp.json["type"].endswith("no-such-job") assert "UUID" in resp.json["detail"] + @pytest.mark.oap_part1 @mocked_dismiss_process() def test_job_dismiss_running_single(self): """ @@ -1388,6 +1411,7 @@ def test_job_dismiss_running_single(self): assert resp.status_code == 410, "Job cannot be dismissed again." assert job.id in resp.json["value"] + @pytest.mark.oap_part1 @mocked_dismiss_process() def test_job_dismiss_complete_single(self): """ @@ -1472,7 +1496,7 @@ def test_job_dismiss_batch(self): def test_job_results_errors(self): """ - Validate errors returned for a incomplete, failed or dismissed job when requesting its results. + Validate errors returned for an incomplete, failed or dismissed job when requesting its results. """ job_accepted = self.make_job( task_id="1111-0000-0000-0000", process=self.process_public.identifier, service=None, @@ -1637,6 +1661,7 @@ def test_jobs_inputs_outputs_validations(self): with self.assertRaises(colander.Invalid): sd.Execute().deserialize({"outputs": {"random": {"transmissionMode": "bad"}}}) + @pytest.mark.oap_part4 def test_job_logs_formats(self): path = f"/jobs/{self.job_info[0].id}/logs" resp = self.app.get(path, headers=self.json_headers) @@ -1703,6 +1728,7 @@ def test_job_logs_formats(self): assert "Process" in lines[1] assert "Complete" in lines[2] + @pytest.mark.oap_part4 def test_job_logs_formats_unsupported(self): path = f"/jobs/{self.job_info[0].id}/logs" resp = self.app.get(path, headers={"Accept": ContentType.IMAGE_GEOTIFF}, expect_errors=True) @@ -1742,7 +1768,28 @@ def test_job_statistics_response(self): if job: self.job_store.delete_job(job.id) + @pytest.mark.oap_part4 + def test_job_inputs_response(self): + raise NotImplementedError # FIXME (https://github.com/crim-ca/weaver/issues/734) + + @pytest.mark.oap_part4 + def test_job_outputs_response(self): + raise NotImplementedError # FIXME + + @pytest.mark.oap_part4 + def test_job_run_response(self): + raise NotImplementedError # FIXME + + @pytest.mark.oap_part4 + def test_job_run_response(self): + raise NotImplementedError # FIXME + + @pytest.mark.oap_part4 + def test_job_update_response(self): + raise NotImplementedError # FIXME + +@pytest.mark.oap_part1 @pytest.mark.parametrize( ["results", "expected"], [ diff --git a/weaver/utils.py b/weaver/utils.py index 3dd180df2..8c492040a 100644 --- a/weaver/utils.py +++ b/weaver/utils.py @@ -90,6 +90,7 @@ MutableMapping, NoReturn, Optional, + Sequence, Tuple, Type, TypeVar, @@ -1538,7 +1539,7 @@ def islambda(func): def get_path_kvp(path, sep=",", **params): - # type: (str, str, **AnyValueType) -> str + # type: (str, str, **Union[AnyValueType, Sequence[AnyValueType]]) -> str """ Generates the URL with Key-Value-Pairs (:term:`KVP`) query parameters. diff --git a/weaver/wps_restapi/api.py b/weaver/wps_restapi/api.py index 83d57db57..59de68a20 100644 --- a/weaver/wps_restapi/api.py +++ b/weaver/wps_restapi/api.py @@ -367,12 +367,18 @@ def get_conformance(category, settings): f"{ogcapi_proc_core}/conf/ogc-process-description", f"{ogcapi_proc_core}/req/json", f"{ogcapi_proc_core}/req/json/definition", + f"{ogcapi_proc_core}/req/job-list/datetime-definition", + f"{ogcapi_proc_core}/req/job-list/datetime-response", + f"{ogcapi_proc_core}/req/job-list/duration-definition", + f"{ogcapi_proc_core}/req/job-list/duration-response", f"{ogcapi_proc_core}/req/job-list/links", f"{ogcapi_proc_core}/req/job-list/jl-limit-definition", f"{ogcapi_proc_core}/req/job-list/job-list-op", f"{ogcapi_proc_core}/req/job-list/processID-definition", f"{ogcapi_proc_core}/req/job-list/processID-mandatory", f"{ogcapi_proc_core}/req/job-list/processid-response", + f"{ogcapi_proc_core}/req/job-list/status-definition", + f"{ogcapi_proc_core}/req/job-list/status-response", f"{ogcapi_proc_core}/req/job-list/type-definition", f"{ogcapi_proc_core}/req/job-list/type-response", # FIXME: KVP exec (https://github.com/crim-ca/weaver/issues/607, https://github.com/crim-ca/weaver/issues/445)