diff --git a/CHANGES.rst b/CHANGES.rst index 3a929a386..3c018a407 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,7 +9,13 @@ Changes `Unreleased `_ (latest) ------------------------------------------------------------------------------------ -* Nothing new for the moment. +Features / Changes +~~~~~~~~~~~~~~~~~~~~~ +* Add `API` endpoint ``GET /services/{service_name}/resources/{resource_id}`` similar to + existing endpoint ``GET /resources/{resource_id}`` allowing retrieval of a `Resource` details + with prior validation that it lies under the referenced `Service` + (fixes `#347 `_). +* Improve ``JSON`` typing definitions to reduce false-positives linting errors and add missing typing definitions. .. _changes_3.35.0: diff --git a/magpie/api/management/resource/resource_views.py b/magpie/api/management/resource/resource_views.py index f28649cff..608a538ff 100644 --- a/magpie/api/management/resource/resource_views.py +++ b/magpie/api/management/resource/resource_views.py @@ -1,6 +1,13 @@ from typing import TYPE_CHECKING -from pyramid.httpexceptions import HTTPBadRequest, HTTPConflict, HTTPForbidden, HTTPInternalServerError, HTTPOk +from pyramid.httpexceptions import ( + HTTPBadRequest, + HTTPConflict, + HTTPForbidden, + HTTPInternalServerError, + HTTPNotFound, + HTTPOk +) from pyramid.settings import asbool from pyramid.view import view_config from ziggurat_foundations.models.services.group import GroupService @@ -12,7 +19,7 @@ from magpie.api import schemas as s from magpie.api.management.resource import resource_formats as rf from magpie.api.management.resource import resource_utils as ru -from magpie.api.management.service.service_formats import format_service_resources +from magpie.api.management.service import service_formats as sf from magpie.api.management.service.service_utils import get_services_by_type from magpie.api.management.user import user_utils as uu from magpie.permissions import PermissionType, format_permissions @@ -20,12 +27,15 @@ from magpie.services import SERVICE_TYPE_DICT, get_resource_child_allowed if TYPE_CHECKING: - from magpie.typedefs import NestingKeyType + from typing import List + + from magpie.typedefs import JSON, AnyRequestType, AnyResponseType, NestingKeyType @s.ResourcesAPI.get(tags=[s.ResourcesTag], response_schemas=s.Resources_GET_responses) @view_config(route_name=s.ResourcesAPI.name, request_method="GET") def get_resources_view(request): + # type: (AnyRequestType) -> AnyResponseType """ List all registered resources. """ @@ -34,20 +44,34 @@ def get_resources_view(request): services = get_services_by_type(svc_type, db_session=request.db) res_json[svc_type] = {} for svc in services: - res_json[svc_type][svc.resource_name] = format_service_resources( + res_json[svc_type][svc.resource_name] = sf.format_service_resources( svc, request.db, show_all_children=True, show_private_url=False) res_json = {"resources": res_json} return ax.valid_http(http_success=HTTPOk, detail=s.Resources_GET_OkResponseSchema.description, content=res_json) -@s.ResourceAPI.get(schema=s.Resource_GET_RequestSchema, tags=[s.ResourcesTag], - response_schemas=s.Resource_GET_responses) -@view_config(route_name=s.ResourceAPI.name, request_method="GET") -def get_resource_view(request): +def get_resource_handler(request): + # type: (AnyRequestType) -> AnyResponseType """ - Get resource information. + Obtains the resource specified by the request with all applicable parameter validation and handling. """ resource = ar.get_resource_matchdict_checked(request) + + # additional check only for endpoint based under service, resource-based endpoint accesses the resource directly + if "service_name" in request.matchdict: + # if the requested resource ID is the service itself, 'root_service_id=None', must check 'resource_id' also + service = ar.get_service_matchdict_checked(request) + ax.verify_param( + service.resource_id, [resource.root_service_id, resource.resource_id], + is_in=True, param_name="service_name", + http_error=HTTPNotFound, + content={ + "service": sf.format_service(service, basic_info=True), + "resource": rf.format_resource(resource, basic_info=True), + }, + msg_on_fail="Requested resource is not located under the specified service.", + ) + parents = asbool(ar.get_query_param(request, ["parents", "parent"], False)) flatten = False if parents: @@ -101,10 +125,22 @@ def get_resource_view(request): detail=s.Resource_GET_OkResponseSchema.description) +@s.ResourceAPI.get(schema=s.Resource_GET_RequestSchema, tags=[s.ResourcesTag], + response_schemas=s.Resource_GET_responses) +@view_config(route_name=s.ResourceAPI.name, request_method="GET") +def get_resource_view(request): + # type: (AnyRequestType) -> AnyResponseType + """ + Get resource information. + """ + return get_resource_handler(request) + + @s.ResourcesAPI.post(schema=s.Resources_POST_RequestSchema, tags=[s.ResourcesTag], response_schemas=s.Resources_POST_responses) @view_config(route_name=s.ResourcesAPI.name, request_method="POST") def create_resource_view(request): + # type: (AnyRequestType) -> AnyResponseType """ Register a new resource. """ @@ -119,6 +155,7 @@ def create_resource_view(request): response_schemas=s.Resources_DELETE_responses) @view_config(route_name=s.ResourceAPI.name, request_method="DELETE") def delete_resource_view(request): + # type: (AnyRequestType) -> AnyResponseType """ Unregister a resource. """ @@ -129,6 +166,7 @@ def delete_resource_view(request): response_schemas=s.Resource_PATCH_responses) @view_config(route_name=s.ResourceAPI.name, request_method="PATCH") def update_resource(request): + # type: (AnyRequestType) -> AnyResponseType """ Update a resource information. """ @@ -170,6 +208,7 @@ def rename_service_magpie_and_phoenix(): response_schemas=s.ResourcePermissions_GET_responses) @view_config(route_name=s.ResourcePermissionsAPI.name, request_method="GET") def get_resource_permissions_view(request): + # type: (AnyRequestType) -> AnyResponseType """ List all applicable permissions for a resource. """ @@ -186,6 +225,7 @@ def get_resource_permissions_view(request): response_schemas=s.ResourceTypes_GET_responses) @view_config(route_name=s.ResourceTypesAPI.name, request_method="GET") def get_resource_types_view(request): + # type: (AnyRequestType) -> AnyResponseType """ List all applicable children resource types under another resource within a service hierarchy. """ @@ -221,10 +261,11 @@ def get_res_types(res): response_schema=s.Permissions_PATCH_responses) @view_config(route_name=s.PermissionsAPI.name, request_method="PATCH") def update_permissions(request): + # type: (AnyRequestType) -> AnyResponseType """ Update the requested permissions and create missing related resources if necessary. """ - permissions = ar.get_value_multiformat_body_checked(request, "permissions", check_type=list) + permissions = ar.get_value_multiformat_body_checked(request, "permissions", check_type=list) # type: List[JSON] ax.verify_param(permissions, not_none=True, not_empty=True, http_error=HTTPBadRequest, msg_on_fail="No permissions to update (empty `permissions` parameter).") @@ -294,7 +335,7 @@ def update_permissions(request): resource_full_type += "/" + resource_type if permission: cfg_entry = { - "service": service_name, + "service": service_name, # noqa "resource": resource_full_path, "type": resource_type if resource_type == "service" else resource_full_type, "permission": permission, diff --git a/magpie/api/management/service/service_views.py b/magpie/api/management/service/service_views.py index 465e90c54..6d8221548 100644 --- a/magpie/api/management/service/service_views.py +++ b/magpie/api/management/service/service_views.py @@ -16,6 +16,7 @@ from magpie.api import requests as ar from magpie.api import schemas as s from magpie.api.management.resource import resource_utils as ru +from magpie.api.management.resource import resource_views as rv from magpie.api.management.service import service_formats as sf from magpie.api.management.service import service_utils as su from magpie.permissions import Permission, PermissionType, format_permissions @@ -26,14 +27,16 @@ if TYPE_CHECKING: from typing import List, Optional, Union + from pyramid.request import Request from sqlalchemy.orm.session import Session - from magpie.typedefs import JSON, Str + from magpie.typedefs import JSON, AnyResponseType, Str @s.ServiceTypesAPI.get(tags=[s.ServicesTag], response_schemas=s.ServiceTypes_GET_responses) @view_config(route_name=s.ServiceTypesAPI.name, request_method="GET") def get_service_types_view(request): # noqa: F811 + # type: (Request) -> AnyResponseType """ List all available service types. """ @@ -45,6 +48,7 @@ def get_service_types_view(request): # noqa: F811 response_schemas=s.ServiceType_GET_responses) @view_config(route_name=s.ServiceTypeAPI.name, request_method="GET") def get_services_by_type_view(request): + # type: (Request) -> AnyResponseType """ List all registered services from a specific type. """ @@ -55,6 +59,7 @@ def get_services_by_type_view(request): response_schemas=s.Services_GET_responses) @view_config(route_name=s.ServicesAPI.name, request_method="GET") def get_services_view(request): + # type: (Request) -> AnyResponseType """ List all registered services. """ @@ -62,6 +67,7 @@ def get_services_view(request): def get_services_runner(request): + # type: (Request) -> AnyResponseType """ Generates services response format from request conditions. @@ -111,6 +117,7 @@ def get_services_runner(request): response_schemas=s.Services_POST_responses) @view_config(route_name=s.ServicesAPI.name, request_method="POST") def register_service_view(request): + # type: (Request) -> AnyResponseType """ Registers a new service. """ @@ -127,6 +134,7 @@ def register_service_view(request): response_schemas=s.Service_PATCH_responses) @view_config(route_name=s.ServiceAPI.name, request_method="PATCH") def update_service_view(request): + # type: (Request) -> AnyResponseType """ Update service information. """ @@ -200,8 +208,9 @@ def update_service_magpie_and_phoenix(_svc, new_name, new_url, svc_push, db_sess @s.ServiceAPI.get(tags=[s.ServicesTag], response_schemas=s.Service_GET_responses) @view_config(route_name=s.ServiceAPI.name, request_method="GET") def get_service_view(request): + # type: (Request) -> AnyResponseType """ - Get a service information. + Get service information. """ service = ar.get_service_matchdict_checked(request) service_info = sf.format_service(service, show_private_url=True, @@ -214,6 +223,7 @@ def get_service_view(request): response_schemas=s.Service_DELETE_responses) @view_config(route_name=s.ServiceAPI.name, request_method="DELETE") def unregister_service_view(request): + # type: (Request) -> AnyResponseType """ Unregister a service. """ @@ -227,6 +237,7 @@ def unregister_service_view(request): msg_on_fail="Delete service from resource tree failed.", content=svc_content) def remove_service_magpie_and_phoenix(svc, svc_push, db_session): + # type: (models.Service, bool, Session) -> None db_session.delete(svc) if svc_push and svc.type in SERVICES_PHOENIX_ALLOWED: sync_services_phoenix(db_session.query(models.Service)) @@ -241,6 +252,7 @@ def remove_service_magpie_and_phoenix(svc, svc_push, db_session): @s.ServicePermissionsAPI.get(tags=[s.ServicesTag], response_schemas=s.ServicePermissions_GET_responses) @view_config(route_name=s.ServicePermissionsAPI.name, request_method="GET") def get_service_permissions_view(request): + # type: (Request) -> AnyResponseType """ List all applicable permissions for a service. """ @@ -253,10 +265,22 @@ def get_service_permissions_view(request): content=format_permissions(svc_perms, PermissionType.ALLOWED)) +@s.ServiceResourceAPI.get(schema=s.ServiceResource_GET_RequestSchema, tags=[s.ServicesTag, s.ResourcesTag], + response_schemas=s.ServiceResource_GET_responses) +@view_config(route_name=s.ServiceResourceAPI.name, request_method="GET") +def get_service_resource_view(request): + # type: (Request) -> AnyResponseType + """ + Get resource information under a service. + """ + return rv.get_resource_handler(request) + + @s.ServiceResourceAPI.delete(schema=s.ServiceResource_DELETE_RequestSchema, tags=[s.ServicesTag], response_schemas=s.ServiceResource_DELETE_responses) @view_config(route_name=s.ServiceResourceAPI.name, request_method="DELETE") def delete_service_resource_view(request): + # type: (Request) -> AnyResponseType """ Unregister a resource. """ @@ -267,6 +291,7 @@ def delete_service_resource_view(request): response_schemas=s.ServiceResources_GET_responses) @view_config(route_name=s.ServiceResourcesAPI.name, request_method="GET") def get_service_resources_view(request): + # type: (Request) -> AnyResponseType """ List all resources registered under a service. """ @@ -281,6 +306,7 @@ def get_service_resources_view(request): response_schemas=s.ServiceResources_POST_responses) @view_config(route_name=s.ServiceResourcesAPI.name, request_method="POST") def create_service_resource_view(request): + # type: (Request) -> AnyResponseType """ Register a new resource directly under a service or under one of its children resources. """ @@ -313,6 +339,7 @@ def create_service_resource_view(request): response_schemas=s.ServiceTypeResources_GET_responses) @view_config(route_name=s.ServiceTypeResourcesAPI.name, request_method="GET") def get_service_type_resources_view(request): + # type: (Request) -> AnyResponseType """ List details of resource types supported under a specific service type. """ @@ -335,6 +362,7 @@ def _get_resource_types_info(res_type_names): response_schemas=s.ServiceTypeResourceTypes_GET_responses) @view_config(route_name=s.ServiceTypeResourceTypesAPI.name, request_method="GET") def get_service_type_resource_types_view(request): + # type: (Request) -> AnyResponseType """ List all resource types supported under a specific service type. """ diff --git a/magpie/api/schemas.py b/magpie/api/schemas.py index ea51432a2..cb0fc870e 100644 --- a/magpie/api/schemas.py +++ b/magpie/api/schemas.py @@ -455,19 +455,19 @@ class ContentType(colander.SchemaNode): class RequestHeaderSchemaAPI(colander.MappingSchema): accept = AcceptType(name="Accept", validator=colander.OneOf(SUPPORTED_ACCEPT_TYPES), - description="Desired MIME type for the response body content.") + description="Desired media-type for the response body content.") content_type = ContentType(validator=colander.OneOf(KNOWN_CONTENT_TYPES), - description="MIME content type of the request body.") + description="Media-type of the request body content.") class RequestHeaderSchemaUI(colander.MappingSchema): content_type = ContentType(default=CONTENT_TYPE_HTML, example=CONTENT_TYPE_HTML, - description="MIME content type of the request body.") + description="Media-type of the request body content.") class QueryRequestSchemaAPI(colander.MappingSchema): format = AcceptType(validator=colander.OneOf(SUPPORTED_FORMAT_TYPES), - description="Desired MIME type for the response body content. " + description="Desired media-type for the response body content. " "This formatting alternative by query parameter overrides the Accept header.") @@ -601,7 +601,7 @@ class BaseRequestSchemaAPI(colander.MappingSchema): class HeaderResponseSchema(colander.MappingSchema): content_type = ContentType(validator=colander.OneOf(SUPPORTED_ACCEPT_TYPES), - description="MIME content type of the response body.") + description="Media-type of the response body content.") class BaseResponseSchemaAPI(colander.MappingSchema): @@ -1789,6 +1789,10 @@ class ServiceResources_POST_UnprocessableEntityResponseSchema(BaseResponseSchema body = ErrorResponseBodySchema(code=HTTPUnprocessableEntity.code, description=description) +class ServiceResource_GET_RequestSchema(Resource_GET_RequestSchema): + service_name = ServiceNameParameter + + # delete service's resource use same method as direct resource delete class ServiceResource_DELETE_RequestSchema(Resource_DELETE_RequestSchema): service_name = ServiceNameParameter @@ -3654,6 +3658,7 @@ class SwaggerAPI_GET_OkResponseSchema(colander.MappingSchema): "422": UnprocessableEntityResponseSchema(), "500": InternalServerErrorResponseSchema(), } +ServiceResource_GET_responses = Resource_GET_responses ServiceResource_DELETE_responses = { "200": ServiceResource_DELETE_OkResponseSchema(), "400": Resource_MatchDictCheck_BadRequestResponseSchema(), # FIXME: https://github.com/Ouranosinc/Magpie/issues/359 diff --git a/magpie/typedefs.py b/magpie/typedefs.py index 904627a2d..128d97c31 100644 --- a/magpie/typedefs.py +++ b/magpie/typedefs.py @@ -56,9 +56,13 @@ AnyKey = Union[Str, int] AnyValue = Union[Str, Number, bool, None] - _JSONType = "JSON" # type: TypeAlias # pylint: disable=C0103 - BaseJSON = Union[AnyValue, List[_JSONType], Dict[AnyKey, _JSONType]] - JSON = Union[Dict[Str, Union[_JSONType]], List[_JSONType]] + _JSON = "JSON" # type: TypeAlias # pylint: disable=C0103 + _JsonObjectItemAlias = "_JsonObjectItem" # type: TypeAlias # pylint: disable=C0103 + _JsonListItemAlias = "_JsonListItem" # type: TypeAlias # pylint: disable=C0103 + _JsonObjectItem = Dict[str, Union[AnyValue, _JSON, _JsonObjectItemAlias, _JsonListItemAlias]] + _JsonListItem = List[Union[AnyValue, _JSON, _JsonObjectItem, _JsonListItemAlias]] + _JsonItem = Union[AnyValue, _JSON, _JsonObjectItem, _JsonListItem] + JSON = Union[Dict[str, Union[_JSON, _JsonItem]], List[Union[_JSON, _JsonItem]], AnyValue] GroupPriority = Union[int, Type[math.inf]] UserServicesType = Union[Dict[Str, Dict[Str, Any]], List[Dict[Str, Any]]] diff --git a/tests/interfaces.py b/tests/interfaces.py index 8e0a5b3ab..a355c87aa 100644 --- a/tests/interfaces.py +++ b/tests/interfaces.py @@ -70,34 +70,42 @@ class ConfigTestCase(object): # pylint: disable=C0103,invalid-name # note: all following should be overridden by Test Case accordingly to the needs of their unit tests - version = None # type: Optional[Str] - require = None # type: Optional[Str] - url = None # type: Optional[Str] - app = None # type: Optional[TestApp] + version = None # type: Optional[Str] + require = None # type: Optional[Str] + url = None # type: Optional[Str] + app = None # type: Optional[TestApp] # parameters for setup operations, admin-level access to the app - grp = None # type: Optional[Str] - usr = None # type: Optional[Str] - pwd = None # type: Optional[Str] - cookies = None # type: Optional[CookiesType] - headers = None # type: Optional[HeadersType] + grp = None # type: Optional[Str] + usr = None # type: Optional[Str] + pwd = None # type: Optional[Str] + cookies = None # type: Optional[CookiesType] + headers = None # type: Optional[HeadersType] json_headers = {"Accept": CONTENT_TYPE_JSON, "Content-Type": CONTENT_TYPE_JSON} # parameters for testing, extracted automatically within 'utils.TestSetup' methods # test cookies/headers can match above admin cookies/headers for test suites targeting administrative operations # for other access level operations, they should correspond to another appropriate test user - test_headers = None # type: Optional[HeadersType] - test_cookies = None # type: Optional[CookiesType] - test_service_type = None # type: Optional[Str] - test_service_name = None # type: Optional[Str] - test_resource_name = None # type: Optional[Str] - test_resource_type = None # type: Optional[Str] - test_user_name = None # type: Optional[Str] # reuse as password to simplify calls when creating test user - test_group_name = None # type: Optional[Str] + test_headers = None # type: Optional[HeadersType] + test_cookies = None # type: Optional[CookiesType] + test_service_type = None # type: Optional[Str] + test_service_name = None # type: Optional[Str] + test_services_info = None # type: Optional[Dict[Str, JSON]] + test_service_resource_perms = None # type: Optional[List[Permission]] + test_resource_name = None # type: Optional[Str] + test_resource_type = None # type: Optional[Str] + test_resource_class = None # type: Optional[Str] + test_resource_perm_name = None # type: Optional[Str] + test_resource_perm_type = None # type: Optional[Permission] + test_admin = None # type: Optional[Str] + test_user_name = None # type: Optional[Str] # also used as password when creating test user + test_group_name = None # type: Optional[Str] # extra parameters to indicate cleanup on final tear down # add new test values on test case startup before they *potentially* get interrupted because of error - extra_user_names = set() # type: Set[Str] - extra_group_names = set() # type: Set[Str] - extra_resource_ids = set() # type: Set[int] - extra_service_names = set() # type: Set[Str] + reserved_users = None # type: Optional[List[Str]] + reserved_groups = None # type: Optional[List[Str]] + extra_user_names = set() # type: Set[Str] + extra_group_names = set() # type: Set[Str] + extra_resource_ids = set() # type: Set[int] + extra_service_names = set() # type: Set[Str] @six.add_metaclass(ABCMeta) @@ -1525,7 +1533,7 @@ def test_GetUserResources_OnlyServicesWithPermissions(self): utils.check_val_is_in("resources", body) svc_types = utils.get_service_types_for_version(self.version) utils.check_all_equal(list(body["resources"]), svc_types, any_order=True) - for svc_type in svc_types: + for svc_type in svc_types: # type: JSON services = body["resources"][svc_type] # type: JSON if svc_type == self.test_service_type: expected_services = [svc0_name, svc1_name] @@ -2415,7 +2423,7 @@ def test_PatchPermissions(self): "action": "create" } ] - } + } # type: JSON resp = utils.test_request(self, "PATCH", "/permissions", headers=self.json_headers, cookies=self.cookies, json=data) utils.check_response_basic_info(resp, 200, expected_method="PATCH") @@ -2594,13 +2602,13 @@ def create_validate_permissions(self, override_resource_id=new_resource_id, override_permission=new_permission) # expected permissions can have more than one entry if the names are not the same, otherwise always only one - _expect_inherit = [_perm[0].json() for _perm in expected_inherited_perm_reasons] + _expect_inherit = [_perm[0].json() for _perm in expected_inherited_perm_reasons] # type: List[JSON] for i, (_, _reason) in enumerate(expected_inherited_perm_reasons): _expect_inherit[i]["reason"] = _reason - _expect_resolve = [_perm[0].json() for _perm in expected_resolved_perm_reasons] + _expect_resolve = [_perm[0].json() for _perm in expected_resolved_perm_reasons] # type: List[JSON] for i, (_, _reason) in enumerate(expected_resolved_perm_reasons): _expect_resolve[i]["reason"] = _reason - _expect_effect = [_perm[0].json() for _perm in expected_effective_perm_reasons] + _expect_effect = [_perm[0].json() for _perm in expected_effective_perm_reasons] # type: List[JSON] for i, (_, _reason) in enumerate(expected_effective_perm_reasons): _expect_effect[i]["reason"] = _reason # tests @@ -3375,7 +3383,7 @@ def test_GetUserResources_OnlyUserAndInheritedGroupPermissions(self): # with or without inherit flag, "other" services and resources should all have no permission service_types = utils.get_service_types_for_version(self.version) - service_type_no_perm = set(service_types) - {svc_type} + service_type_no_perm = set(service_types) - {svc_type} # type: Set[Str] utils.check_val_not_equal(len(service_type_no_perm), 0, msg="Cannot evaluate response values with insufficient service types.") for query in ["", "?inherit=true"]: @@ -3513,7 +3521,7 @@ def test_GetUserResources_Filtered(self): path = "/users/{}/resources?filtered=true".format(self.test_user_name) resp = utils.test_request(self, "GET", path, headers=self.json_headers, cookies=self.cookies) - body = utils.check_response_basic_info(resp) + body = utils.check_response_basic_info(resp) # type: JSON svc_types = utils.get_service_types_for_version(self.version) utils.check_all_equal(list(body["resources"]), svc_types, any_order=True, msg="All service types listed.") for svc_type in svc_types: @@ -6205,7 +6213,7 @@ def test_GetResources_ResponseFormat(self): utils.check_all_equal(resource["permission_names"], service_perms, any_order=True) # children resources utils.check_val_is_in("resources", resource) - children = resource["resources"] + children = resource["resources"] # type: Dict[int, JSON] utils.check_val_type(children, dict) for res_id, child in children.items(): # test only one just to be sure of recursive nature (should have at least one from TestSetup) @@ -6312,8 +6320,18 @@ def test_PostResources_ConflictName(self): headers=self.json_headers, cookies=self.cookies) utils.check_response_basic_info(resp, 409, expected_method="POST") + @runner.MAGPIE_TEST_SERVICES + @runner.MAGPIE_TEST_RESOURCES + def test_GetServiceResource_ResponseFormat_ChildrenNested(self): + self.run_GetResource_ResponseFormat_ChildrenNested(True) + @runner.MAGPIE_TEST_RESOURCES def test_GetResource_ResponseFormat_ChildrenNested(self): + self.run_GetResource_ResponseFormat_ChildrenNested(False) + + @runner.MAGPIE_TEST_RESOURCES + def run_GetResource_ResponseFormat_ChildrenNested(self, prefix_service_path): + # type: (bool) -> None """ Test format of nested resource tree. @@ -6336,7 +6354,8 @@ def test_GetResource_ResponseFormat_ChildrenNested(self): info = utils.TestSetup.get_ResourceInfo(self, override_body=body) res3_id = info["resource_id"] - path = "/resources/{}".format(svc_id) + prefix_path = "/services/{}".format(self.test_service_name) if prefix_service_path else "" + path = "{}/resources/{}".format(prefix_path, svc_id) resp = utils.test_request(self, "GET", path, headers=self.json_headers, cookies=self.cookies) body = utils.check_response_basic_info(resp) utils.check_val_is_in("resource", body) @@ -6382,8 +6401,18 @@ def check_resource_node(res_body, res_id, parent_id, root_id, perms, children_id res3_body = res1_body["children"][str(res3_id)] check_resource_node(res3_body, res3_id, res1_id, svc_id, res_perms, None) + @runner.MAGPIE_TEST_SERVICES + @runner.MAGPIE_TEST_RESOURCES + def test_GetServiceResource_ResponseFormat_ParentNested_NormalAndInvert(self): + self.run_GetResource_ResponseFormat_ParentNested_NormalAndInvert(True) + @runner.MAGPIE_TEST_RESOURCES def test_GetResource_ResponseFormat_ParentNested_NormalAndInvert(self): + self.run_GetResource_ResponseFormat_ParentNested_NormalAndInvert(False) + + @runner.MAGPIE_TEST_RESOURCES + def run_GetResource_ResponseFormat_ParentNested_NormalAndInvert(self, prefix_service_path): + # type: (bool) -> None """ Test format of nested resource tree. @@ -6405,13 +6434,15 @@ def test_GetResource_ResponseFormat_ParentNested_NormalAndInvert(self): self, override_service_name=svc1_name, override_service_type=ServiceTHREDDS.service_type, override_resource_names=["res" + str(i) for i in range(2, 5)], - override_resource_types=[type_dir, type_dir, type_file] + override_resource_types=[type_dir, type_dir, type_file], + override_exist=True, ) # extra sibling resource not checked, but just to make sure that it doesn't appear somewhere else # all nested parent object should only contain 1 resource utils.TestSetup.create_TestResource(self, parent_resource_id=svc1_id, override_resource_type=type_file) - path = "/resources/{}".format(res3_id) + prefix_path = "/services/{}".format(svc1_name) if prefix_service_path else "" + path = "{}/resources/{}".format(prefix_path, res3_id) query = {"parent": "true"} body = utils.test_request(self, "GET", path, params=query, headers=self.json_headers, cookies=self.cookies) info = utils.check_response_basic_info(body) # type: JSON @@ -6424,7 +6455,8 @@ def test_GetResource_ResponseFormat_ParentNested_NormalAndInvert(self): utils.check_val_equal(info["resource"]["parent_id"], res2_id) utils.check_val_equal(info["resource"]["root_service_id"], svc1_id) utils.check_val_is_in(str(res2_id), info["resource"]["parent"]) - res2_parent = info["resource"]["parent"][str(res2_id)] + res_parents = info["resource"]["parent"] # type: JSON + res2_parent = res_parents[str(res2_id)] utils.check_val_not_in("children", res2_parent) utils.check_val_is_in("parent", res2_parent) utils.check_val_equal(len(res2_parent["parent"]), 1) @@ -6432,7 +6464,8 @@ def test_GetResource_ResponseFormat_ParentNested_NormalAndInvert(self): utils.check_val_equal(res2_parent["parent_id"], svc1_id) utils.check_val_equal(res2_parent["root_service_id"], svc1_id) utils.check_val_is_in(str(svc1_id), res2_parent["parent"]) - svc1_parent = res2_parent["parent"][str(svc1_id)] + res2_parents = res2_parent["parent"] # type: JSON + svc1_parent = res2_parents[str(svc1_id)] # type: JSON utils.check_val_not_in("children", svc1_parent) utils.check_val_is_in("parent", svc1_parent) utils.check_val_equal(len(svc1_parent["parent"]), 0) @@ -6442,13 +6475,13 @@ def test_GetResource_ResponseFormat_ParentNested_NormalAndInvert(self): utils.check_val_equal(svc1_parent["root_service_id"], None) # verify inverted "parent" that should still make use of "children" field, since inverted - path = "/resources/{}".format(res3_id) + path = "{}/resources/{}".format(prefix_path, res3_id) query = {"parent": "true", "invert": "true"} body = utils.test_request(self, "GET", path, params=query, headers=self.json_headers, cookies=self.cookies) info = utils.check_response_basic_info(body) # type: JSON err_one = ( - "Even if service has more than one children, using parent query should only list " + "Even if service has more than one child resource, using parent query should only list " "single child of the branch that leads to requested resource." ) err_last = ( @@ -6463,7 +6496,8 @@ def test_GetResource_ResponseFormat_ParentNested_NormalAndInvert(self): utils.check_val_equal(info["resource"]["parent_id"], None) utils.check_val_equal(info["resource"]["root_service_id"], None) utils.check_val_is_in(str(res2_id), info["resource"]["children"]) - res2_child = info["resource"]["children"][str(res2_id)] + svc1_children = info["resource"]["children"] # type: JSON + res2_child = svc1_children[str(res2_id)] utils.check_val_not_in("parent", res2_child) utils.check_val_is_in("children", res2_child) utils.check_val_equal(len(res2_child["children"]), 1, msg=err_one) @@ -6471,7 +6505,8 @@ def test_GetResource_ResponseFormat_ParentNested_NormalAndInvert(self): utils.check_val_equal(res2_child["parent_id"], svc1_id) utils.check_val_equal(res2_child["root_service_id"], svc1_id) utils.check_val_is_in(str(res3_id), res2_child["children"]) - res3_child = res2_child["children"][str(res3_id)] + res2_children = res2_child["children"] # type: JSON + res3_child = res2_children[str(res3_id)] # type: JSON utils.check_val_not_in("parent", res3_child) utils.check_val_is_in("children", res3_child) utils.check_val_equal(len(res3_child["children"]), 0, msg=err_last) @@ -6480,8 +6515,18 @@ def test_GetResource_ResponseFormat_ParentNested_NormalAndInvert(self): utils.check_val_equal(res3_child["parent_id"], res2_id) utils.check_val_equal(res3_child["root_service_id"], svc1_id) + @runner.MAGPIE_TEST_SERVICES + @runner.MAGPIE_TEST_RESOURCES + def test_GetServiceResource_ResponseFormat_ParentListing_NormalAndInvert(self): + self.run_GetResource_ResponseFormat_ParentListing_NormalAndInvert(True) + @runner.MAGPIE_TEST_RESOURCES def test_GetResource_ResponseFormat_ParentListing_NormalAndInvert(self): + self.run_GetResource_ResponseFormat_ParentListing_NormalAndInvert(False) + + @runner.MAGPIE_TEST_RESOURCES + def run_GetResource_ResponseFormat_ParentListing_NormalAndInvert(self, prefix_service_path): + # type: (bool) -> None """ Test format of nested resource tree. @@ -6503,13 +6548,15 @@ def test_GetResource_ResponseFormat_ParentListing_NormalAndInvert(self): self, override_service_name=svc1_name, override_service_type=ServiceTHREDDS.service_type, override_resource_names=["res" + str(i) for i in range(2, 5)], - override_resource_types=[type_dir, type_dir, type_file] + override_resource_types=[type_dir, type_dir, type_file], + override_exist=True, ) # extra sibling resource not checked, but just to make sure that it doesn't appear somewhere else # all nested parent object should only contain 1 resource utils.TestSetup.create_TestResource(self, parent_resource_id=svc1_id, override_resource_type=type_file) - path = "/resources/{}".format(res3_id) + prefix_path = "/services/{}".format(svc1_name) if prefix_service_path else "" + path = "{}/resources/{}".format(prefix_path, res3_id) query = {"parent": "true", "flatten": "true"} body = utils.test_request(self, "GET", path, params=query, headers=self.json_headers, cookies=self.cookies) info = utils.check_response_basic_info(body) # type: JSON @@ -6533,7 +6580,7 @@ def test_GetResource_ResponseFormat_ParentListing_NormalAndInvert(self): utils.check_val_equal(info["resources"][2]["root_service_id"], None) # verify inverted list ordering - path = "/resources/{}".format(res3_id) + path = "{}/resources/{}".format(prefix_path, res3_id) query = {"parent": "true", "flatten": "true", "invert": "true"} body = utils.test_request(self, "GET", path, params=query, headers=self.json_headers, cookies=self.cookies) info = utils.check_response_basic_info(body) diff --git a/tests/test_magpie_api.py b/tests/test_magpie_api.py index 8450f7164..68d593e99 100644 --- a/tests/test_magpie_api.py +++ b/tests/test_magpie_api.py @@ -591,7 +591,7 @@ def setUpClass(cls): cls.version = utils.TestSetup.get_Version(cls, real_version=True) cls.setup_admin() cls.headers, cls.cookies = utils.check_or_try_login_user(cls, cls.usr, cls.pwd, use_ui_form_submit=True) - cls.require = "cannot run tests without logged in user with '{}' permissions".format(cls.grp) + cls.require = "cannot run tests without logged-in user with '{}' permissions".format(cls.grp) assert cls.headers and cls.cookies, cls.require # nosec cls.test_service_name = "unittest-user-auth-remote_test-service" @@ -688,4 +688,4 @@ def raise_request(*_, **__): if __name__ == "__main__": import sys - sys.exit(unittest.main()) + sys.exit(unittest.main()) # type: ignore diff --git a/tests/utils.py b/tests/utils.py index 1a62357ca..e8cd192dc 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -61,7 +61,6 @@ import tests.interfaces as ti from magpie.compat import TupleVersion - from magpie.services import ServiceInterface from magpie.typedefs import ( JSON, AnyCookiesType, @@ -613,7 +612,7 @@ def get_json_body(response): def get_service_types_for_version(version): - # type: (Str) -> List[ServiceInterface] + # type: (Str) -> List[Str] available_service_types = set(services.SERVICE_TYPE_DICT) if TestVersion(version) <= TestVersion("0.6.1"): available_service_types = available_service_types - {ServiceAccess.service_type}