From e6f4dfffd5573eb5f768f78af723155d8a755d62 Mon Sep 17 00:00:00 2001 From: Francis Charette Migneault Date: Mon, 27 Jul 2020 17:03:00 -0400 Subject: [PATCH] disable UI non-editable users/groups + error API side (fixes #164) --- HISTORY.rst | 6 +- magpie/api/generic.py | 3 - magpie/api/login/login.py | 2 +- magpie/api/schemas.py | 2 +- magpie/typedefs.py | 2 +- .../ui/management/templates/edit_group.mako | 22 ++- magpie/ui/management/templates/edit_user.mako | 32 ++-- .../ui/user/templates/edit_current_user.mako | 32 ++-- magpie/ui/user/views.py | 8 +- magpie/ui/utils.py | 11 +- setup.cfg | 15 +- tests/interfaces.py | 163 +++++++++++++----- tests/test_magpie_api.py | 12 +- tests/utils.py | 44 ++++- 14 files changed, 241 insertions(+), 113 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 790a6e127..02c949ebe 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -13,17 +13,21 @@ Features / Changes * Add tag description into generated Swagger API documentation. * Add more usage details to start `Magpie` web application in documentation. * Add database migration for new ``discoverable`` column of groups. -* Allow logged user to update its own information. +* Allow logged user to update its own information + (relates to `#170 `_). * Allow logged user to join *discoverable* groups. * Change some UI CSS for certain pages to improve table readability. * Add UI page to render unauthorized and forbidden views due to insufficient permissions. * Add more validation and inputs to update group information. * Allow combined configuration files (providers, permissions, users, groups) with inter-references between them specified with ``MAGPIE_CONFIG_PATH`` environment variable or ``magpie.config_path`` setting (example in ``configs``). +* Add disabled checkboxes for UI rendering of non-editable items + (relates to `#164 `_). Bug Fixes ~~~~~~~~~~~~~~~~~~~~~ * Fix invalid API documentation of request body for ``POST /users/{user_name}/groups``. +* Fix `#164 `_ (forbid *special* users and groups update and delete). * Fix minor HTML issues in mako templates. `1.11.0 `_ (2020-06-19) diff --git a/magpie/api/generic.py b/magpie/api/generic.py index a39eb88f8..f4283818d 100644 --- a/magpie/api/generic.py +++ b/magpie/api/generic.py @@ -101,9 +101,6 @@ def unauthorized_or_forbidden(request): path = request.route_path("error").replace("/magpie", "") data = {"error_request": content, "error_code": http_err.code} return request_api(request, path, "POST", data) - subreq = Request.blank(location, base_url=request.application_url, POST=data, - headers={"Content-Type": CONTENT_TYPE_JSON}) - return request.invoke_subrequest(subreq, use_tweens=True) return raise_http(nothrow=True, http_error=http_err, detail=content["detail"], content=content, content_type=accept) diff --git a/magpie/api/login/login.py b/magpie/api/login/login.py index fc5a86027..3e3fc725f 100644 --- a/magpie/api/login/login.py +++ b/magpie/api/login/login.py @@ -29,7 +29,7 @@ from magpie.api import schemas as s from magpie.api.management.user.user_formats import format_user from magpie.api.management.user.user_utils import create_user -from magpie.api.requests import get_multiformat_post, get_value_multiformat_post_checked +from magpie.api.requests import get_multiformat_post, get_principals, get_value_multiformat_post_checked from magpie.constants import get_constant from magpie.security import authomatic_setup, get_provider_names from magpie.utils import CONTENT_TYPE_JSON, convert_response, get_logger, get_magpie_url diff --git a/magpie/api/schemas.py b/magpie/api/schemas.py index 138acf69e..8b30b581f 100644 --- a/magpie/api/schemas.py +++ b/magpie/api/schemas.py @@ -24,7 +24,7 @@ from magpie import __meta__ from magpie.constants import get_constant from magpie.permissions import Permission -from magpie.utils import CONTENT_TYPE_HTML, CONTENT_TYPE_JSON, get_magpie_url +from magpie.utils import CONTENT_TYPE_HTML, CONTENT_TYPE_JSON if TYPE_CHECKING: # pylint: disable=W0611,unused-import diff --git a/magpie/typedefs.py b/magpie/typedefs.py index 6adc7fb4b..d4cf824cf 100644 --- a/magpie/typedefs.py +++ b/magpie/typedefs.py @@ -50,7 +50,7 @@ AnyKey = Union[Str, int] AnyValue = Union[Str, Number, bool, None] BaseJSON = Union[AnyValue, List["BaseJSON"], Dict[AnyKey, "BaseJSON"]] - JSON = Dict[AnyKey, BaseJSON] + JSON = Union[Dict[AnyKey, Union[BaseJSON, "JSON"]], List[BaseJSON]] UserServicesType = Union[Dict[Str, Dict[Str, Any]], List[Dict[Str, Any]]] ServiceOrResourceType = Union[models.Service, models.Resource] diff --git a/magpie/ui/management/templates/edit_group.mako b/magpie/ui/management/templates/edit_group.mako index 37a3e582f..16652d66c 100644 --- a/magpie/ui/management/templates/edit_group.mako +++ b/magpie/ui/management/templates/edit_group.mako @@ -95,15 +95,21 @@ %for user in users: - + %if user in members: + checked + %endif + %if group_name in MAGPIE_FIXED_GROUP_MEMBERSHIPS: + disabled + %else: + onchange="document.getElementById('edit_members').submit()" + %endif + > + ${user} + + %endfor
+
diff --git a/magpie/ui/management/templates/edit_user.mako b/magpie/ui/management/templates/edit_user.mako index 1a7fb5895..14e2f8c3d 100644 --- a/magpie/ui/management/templates/edit_user.mako +++ b/magpie/ui/management/templates/edit_user.mako @@ -144,23 +144,21 @@ %for group in groups: - % if group in own_groups: - - % else: - - % endif + %endfor
- - - - + +
diff --git a/magpie/ui/user/templates/edit_current_user.mako b/magpie/ui/user/templates/edit_current_user.mako index 10de2b5cf..2d4eb64ec 100644 --- a/magpie/ui/user/templates/edit_current_user.mako +++ b/magpie/ui/user/templates/edit_current_user.mako @@ -82,23 +82,21 @@ %for group in groups: - % if group in joined_groups: - - % else: - - % endif + %endfor
- - - - + +
diff --git a/magpie/ui/user/views.py b/magpie/ui/user/views.py index ee03ce4d6..26ffec272 100644 --- a/magpie/ui/user/views.py +++ b/magpie/ui/user/views.py @@ -60,11 +60,11 @@ def leave_discoverable_group(self, group_name): @view_config(route_name="edit_current_user", renderer="templates/edit_current_user.mako", permission=Authenticated) def edit_current_user(self): - own_groups = self.get_current_user_groups() + joined_groups = self.get_current_user_groups() public_groups = self.get_discoverable_groups() user_info = self.get_current_user_info() user_info[u"edit_mode"] = u"no_edit" - user_info[u"own_groups"] = own_groups + user_info[u"joined_groups"] = joined_groups user_info[u"groups"] = public_groups error_message = "" @@ -101,8 +101,8 @@ def edit_current_user(self): # edits to groups checkboxes if is_edit_group_membership: selected_groups = self.request.POST.getall("member") - removed_groups = list(set(own_groups) - set(selected_groups)) - new_groups = list(set(selected_groups) - set(own_groups)) + removed_groups = list(set(joined_groups) - set(selected_groups)) + new_groups = list(set(selected_groups) - set(joined_groups)) for group in removed_groups: self.leave_discoverable_group(group) for group in new_groups: diff --git a/magpie/ui/utils.py b/magpie/ui/utils.py index 353bbe677..23236ca50 100644 --- a/magpie/ui/utils.py +++ b/magpie/ui/utils.py @@ -1,10 +1,11 @@ import json from typing import TYPE_CHECKING -from pyramid.httpexceptions import HTTPBadRequest, HTTPUnauthorized, HTTPForbidden, exception_response +from pyramid.httpexceptions import HTTPBadRequest, exception_response from pyramid.request import Request from magpie.api.requests import get_logged_user +from magpie.constants import get_constant from magpie.utils import CONTENT_TYPE_JSON, get_header, get_logger, get_magpie_url if TYPE_CHECKING: @@ -83,16 +84,24 @@ def wrap(*args, **kwargs): class BaseViews(object): + """Base methods for Magpie UI pages.""" + def __init__(self, request): self.request = request self.magpie_url = get_magpie_url(request.registry) self.logged_user = get_logged_user(request) + self.MAGPIE_FIXED_GROUP_MEMBERSHIPS = [ + get_constant("MAGPIE_ANONYMOUS_GROUP", settings_container=request), + ] + """Special groups membership that cannot be edited.""" + def add_template_data(self, data=None): # type: (Optional[Dict[Str, Any]]) -> Dict[Str, Any] """Adds required template data for the 'heading' mako template applied to every UI page.""" all_data = data or {} all_data.setdefault("MAGPIE_SUB_TITLE", "Administration") + all_data.setdefault("MAGPIE_FIXED_GROUP_MEMBERSHIPS", self.MAGPIE_FIXED_GROUP_MEMBERSHIPS) magpie_logged_user = get_logged_user(self.request) if magpie_logged_user: all_data.update({"MAGPIE_LOGGED_USER": magpie_logged_user.user_name}) diff --git a/setup.cfg b/setup.cfg index e0172b7bb..9520ef46b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,15 +17,15 @@ search = __version__ = "{current_version}" replace = __version__ = "{new_version}" [bumpversion:file:HISTORY.rst] -search = +search = `Unreleased `_ (latest) ------------------------------------------------------------------------------------ -replace = +replace = `Unreleased `_ (latest) ------------------------------------------------------------------------------------ - + * Nothing yet. - + `{new_version} `_ ({now:%%Y-%%m-%%d}) ------------------------------------------------------------------------------------ @@ -39,7 +39,7 @@ ignore-path = docs/_build [flake8] ignore = E501,W291,W503,W504 max-line-length = 120 -exclude = +exclude = .git, __pycache__, docs, @@ -67,10 +67,10 @@ known_second_party = twitcher skip_glob = *pyramid.httpexceptions* [tool:pytest] -addopts = +addopts = --strict --tb=native -markers = +markers = defaults: magpie default users, providers and views register: magpie methods employed in 'register' module login: magpie login operations @@ -78,6 +78,7 @@ markers = resources: magpie resources operations groups: magpie groups operations users: magpie users operations + logged: magpie logged user operations (current) status: magpie views validation found/displayed as per permissions remote: magpie tests running on remote instance specified by url local: magpie tests running on local instance created by test suite diff --git a/tests/interfaces.py b/tests/interfaces.py index 79976485d..f2bd0b54b 100644 --- a/tests/interfaces.py +++ b/tests/interfaces.py @@ -1,3 +1,4 @@ +import unittest import warnings from abc import ABCMeta from copy import deepcopy @@ -21,12 +22,29 @@ if TYPE_CHECKING: # pylint: disable=W0611,unused-import - from magpie.typedefs import CookiesType, HeadersType, Optional, Str + from magpie.typedefs import CookiesType, HeadersType, JSON, Optional, Str -# don't use 'unittest.TestCase' base -# some test runner raise (ERROR) the 'NotImplementedError' although overridden by other classes -class Base_Magpie_TestCase(object): +class Base_Magpie_TestCase(unittest.TestCase): + """ + Base definition for all other Test Suite interfaces. + + The implementers must provide :meth:`initClass` which prepares the various test parameters, session cookies and + the local application or remote Magpie URL configuration to evaluate test cases on. + + The implementers Test Suites must also set :attr:`__test__` to ``True`` so that tests are picked up as executable. + + .. note:: + Attribute attr:`__test__` is employed to avoid duplicate runs of this base class or other derived classes that + must not be considered as the *final implementer* Test Suite. + + .. warning:: + Do not use :meth:`setUpClass` with :py:exception:`NotImplementedError` in this class or any derived *incomplete* + class as this method still gets called by some test runners although marked with ``__test__ = False``. + The tests would be interpreted as failing in this situation (due to raised error) instead of only indicating an + abstract class definition. You are free to use it if the method is non-raising, but remember that the code will + be executed during initialization of the Test Suite even if seemingly disabled for testing. + """ # pylint: disable=C0103,invalid-name version = None # type: Optional[Str] grp = None # type: Optional[Str] @@ -44,7 +62,7 @@ class Base_Magpie_TestCase(object): __test__ = False # won't run this as a test suite, only its derived classes that overrides to True @classmethod - def setUpClass(cls): # noqa: N802 + def initClass(cls): # noqa: N802 raise NotImplementedError @classmethod @@ -58,13 +76,17 @@ class Interface_MagpieAPI_NoAuth(six.with_metaclass(ABCMeta, Base_Magpie_TestCas """ Interface class for unittests of Magpie API. Test any operation that do not require user AuthN/AuthZ. - Derived classes must implement ``setUpClass`` accordingly to generate the Magpie test application. + Derived classes must implement :meth:`initClass` accordingly to generate the Magpie test application. """ @classmethod - def setUpClass(cls): + def initClass(cls): raise NotImplementedError + @classmethod + def setUpClass(cls): + cls.initClass() + @runner.MAGPIE_TEST_LOGIN def test_GetSession_Anonymous(self): resp = utils.test_request(self, "GET", "/session", headers=self.json_headers) @@ -131,11 +153,11 @@ class Interface_MagpieAPI_UsersAuth(six.with_metaclass(ABCMeta, Base_Magpie_Test """ Interface class for unittests of Magpie API. Test any operation that require at least logged user AuthN/AuthZ. - Derived classes must implement ``setUpClass`` accordingly to generate the Magpie test application. + Derived classes must implement :meth:`initClass` accordingly to generate the Magpie test application. """ @classmethod - def setUpClass(cls): + def initClass(cls): raise NotImplementedError @classmethod @@ -329,11 +351,11 @@ class Interface_MagpieAPI_AdminAuth(six.with_metaclass(ABCMeta, Base_Magpie_Test Interface class for unittests of Magpie API. Test any operation that require at least 'administrator' group AuthN/AuthZ. - Derived classes must implement ``setUpClass`` accordingly to generate the Magpie test application. + Derived classes must implement :meth:`initClass` accordingly to generate the Magpie test application. """ @classmethod - def setUpClass(cls): + def initClass(cls): raise NotImplementedError def tearDown(self): @@ -381,7 +403,7 @@ def setup_test_values(cls): cls.test_group_name = u"magpie-unittest-dummy-group" cls.test_user_name = u"magpie-unittest-toto" - cls.test_user_group = u"users" + cls.test_user_group = get_constant("MAGPIE_USERS_GROUP") def setUp(self): self.check_requirements() @@ -725,7 +747,7 @@ def test_GetUserResources_OnlyUserAndInheritedGroupPermissions_values(self): resources_anonymous_body = utils.check_response_basic_info(resp, 200, expected_method="GET") # validation for svc_type_no_perm in service_type_no_perm: - svc_type_body = body["resources"][svc_type_no_perm] + svc_type_body = body["resources"][svc_type_no_perm] # type: JSON svc_type_services_anonymous = resources_anonymous_body.get("resources", {}).get(svc_type_no_perm, {}) for svc_name_no_perm in svc_type_body: # remove inherited anonymous-only group resources and permissions (see above) @@ -736,7 +758,7 @@ def test_GetUserResources_OnlyUserAndInheritedGroupPermissions_values(self): utils.check_val_equal(len(svc_res_ids_only_user), 0, msg="User should not have any permitted resource under the service") svc_perms_anonymous = svc_anonymous.get("permission_names", []) - svc_perms_test_user = svc_type_body[svc_name_no_perm]["permission_names"] + svc_perms_test_user = svc_type_body[svc_name_no_perm]["permission_names"] # noqa svc_perms_only_user = set(svc_perms_test_user) - set(svc_perms_anonymous) utils.check_val_equal(len(svc_perms_only_user), 0, msg="User should not have any service permissions") @@ -745,10 +767,11 @@ def test_GetUserResources_OnlyUserAndInheritedGroupPermissions_values(self): path = "/users/{usr}/resources".format(usr=usr_name) resp = utils.test_request(self, "GET", path, headers=self.json_headers, cookies=self.cookies, timeout=20) body = utils.check_response_basic_info(resp, 200, expected_method="GET") - test_service = body["resources"][svc_type][svc_name] + test_service = body["resources"][svc_type][svc_name] # type: JSON utils.check_val_equal(test_service["permission_names"], [perm_svc_usr]) utils.check_val_is_in(str(res_id), test_service["resources"]) - utils.check_val_equal(test_service["resources"][str(res_id)]["permission_names"], [perm_res_usr]) + test_perms = test_service["resources"][str(res_id)]["permission_names"] # noqa + utils.check_val_equal(test_perms, [perm_res_usr]) # with inherit flag, both user and group permissions are visible on service and resource path = "/users/{usr}/resources?inherit=true".format(usr=usr_name) @@ -757,7 +780,7 @@ def test_GetUserResources_OnlyUserAndInheritedGroupPermissions_values(self): test_service = body["resources"][svc_type][svc_name] utils.check_all_equal(test_service["permission_names"], [perm_svc_usr, perm_svc_grp], any_order=True) utils.check_val_is_in(str(res_id), test_service["resources"]) - utils.check_all_equal(test_service["resources"][str(res_id)]["permission_names"], + utils.check_all_equal(test_service["resources"][str(res_id)]["permission_names"], # noqa [perm_res_usr, perm_res_grp], any_order=True) @runner.MAGPIE_TEST_USERS @@ -776,7 +799,7 @@ def test_GetUserInheritedResources_format(self): utils.check_all_equal(body["resources"].keys(), service_types, any_order=True) for svc_type in body["resources"]: for svc in body["resources"][svc_type]: - svc_dict = body["resources"][svc_type][svc] + svc_dict = body["resources"][svc_type][svc] # type: JSON utils.check_val_type(svc_dict, dict) utils.check_val_is_in("resource_id", svc_dict) utils.check_val_is_in("service_name", svc_dict) @@ -855,7 +878,7 @@ def test_GetUserServices(self): for svc_type in services: utils.check_val_is_in(svc_type, service_types) # one of valid service types for svc in services[svc_type]: - svc_dict = services[svc_type][svc] + svc_dict = services[svc_type][svc] # type: JSON utils.check_val_type(svc_dict, dict) utils.check_val_is_in("resource_id", svc_dict) utils.check_val_is_in("service_name", svc_dict) @@ -919,7 +942,7 @@ def test_GetUserServiceResources_OnlyUserAndInheritedGroupPermissions_values(sel utils.check_val_equal(body["service"]["service_type"], svc_type) utils.check_val_equal(body["service"]["permission_names"], [perm_svc_usr]) utils.check_val_is_in(str(res_id), body["service"]["resources"]) - utils.check_val_equal(body["service"]["resources"][str(res_id)]["permission_names"], [perm_res_usr]) + utils.check_val_equal(body["service"]["resources"][str(res_id)]["permission_names"], [perm_res_usr]) # noqa # with inherit flag, both user and group permissions are visible on service and resource path = "/users/{usr}/services/{svc}/resources?inherit=true".format(usr=usr_name, svc=svc_name) @@ -929,7 +952,7 @@ def test_GetUserServiceResources_OnlyUserAndInheritedGroupPermissions_values(sel utils.check_val_equal(body["service"]["service_type"], svc_type) utils.check_all_equal(body["service"]["permission_names"], [perm_svc_usr, perm_svc_grp], any_order=True) utils.check_val_is_in(str(res_id), body["service"]["resources"]) - utils.check_all_equal(body["service"]["resources"][str(res_id)]["permission_names"], + utils.check_all_equal(body["service"]["resources"][str(res_id)]["permission_names"], # noqa [perm_res_usr, perm_res_grp], any_order=True) @runner.MAGPIE_TEST_USERS @@ -947,6 +970,23 @@ def test_PostUsers(self): users = utils.TestSetup.get_RegisteredUsersList(self) utils.check_val_is_in(self.test_user_name, users) + @runner.MAGPIE_TEST_USERS + def test_PostUsers_AutoMemberships(self): + new_test_group = "test-group-{}".format(self._testMethodName) # noqa + utils.TestSetup.delete_TestGroup(self, override_group_name=new_test_group) # if previous run + utils.TestSetup.create_TestGroup(self, override_group_name=new_test_group) + data = {"group_name": new_test_group} + utils.TestSetup.create_TestUser(self, override_data=data) + + resp = utils.test_request(self, "GET", "/users/{}/groups".format(self.test_user_name), + headers=self.json_headers, cookies=self.cookies) + body = utils.check_response_basic_info(resp) + utils.check_val_is_in(new_test_group, body["group_names"], + msg="Specified group during user creation should have been applied.") + utils.check_val_is_in(get_constant("MAGPIE_ANONYMOUS_GROUP"), body["group_names"], + msg="User should be automatically added to 'anonymous' group to access public resources.") + utils.TestSetup.delete_TestGroup(self, override_group_name=new_test_group) + @runner.MAGPIE_TEST_USERS @runner.MAGPIE_TEST_LOGGED def test_PostUsers_ReservedKeyword_Current(self): @@ -954,7 +994,7 @@ def test_PostUsers_ReservedKeyword_Current(self): "user_name": get_constant("MAGPIE_LOGGED_USER"), "password": "pwd", "email": "email@mail.com", - "group_name": "users", + "group_name": self.test_group_name, } resp = utils.test_request(self, "POST", "/users", data=data, headers=self.json_headers, cookies=self.cookies, expect_errors=True) @@ -1025,9 +1065,15 @@ def test_PutUsers_username(self): utils.check_val_equal(body["authenticated"], False) @runner.MAGPIE_TEST_USERS - def test_PutUser_username(self): + @runner.MAGPIE_TEST_LOGGED + def test_PutUser_username_ReservedKeyword_Current(self): """Even administrator level user is not allowed to update any user name to reserved keyword.""" - raise NotImplementedError # TODO + utils.TestSetup.create_TestUser(self) + data = {"user_name": get_constant("MAGPIE_LOGGED_USER")} + path = "/users/{usr}".format(usr=self.test_user_name) + resp = utils.test_request(self, "PUT", path, data=data, expect_errors=True, + headers=self.json_headers, cookies=self.cookies) + utils.check_response_basic_info(resp, 403, expected_method="PUT") @runner.MAGPIE_TEST_USERS def test_PutUsers_email(self): @@ -1160,6 +1206,20 @@ def test_DeleteUser(self): utils.check_response_basic_info(resp, 200, expected_method="GET") utils.TestSetup.check_NonExistingTestUser(self) + @runner.MAGPIE_TEST_USERS + @runner.MAGPIE_TEST_DEFAULTS + def test_DeleteUser_forbidden_ReservedKeyword_Anonymous(self): + """Even administrator level user is not allowed to remove the special anonymous user.""" + anonymous = get_constant("MAGPIE_ANONYMOUS_USER") + users = utils.TestSetup.get_RegisteredUsersList(self) + utils.check_val_is_in(anonymous, users, msg="Anonymous user pre-requirement missing for test.") + path = "/users/{}".format(anonymous) + resp = utils.test_request(self, "DELETE", path, expect_errors=True, + headers=self.json_headers, cookies=self.cookies) + utils.check_response_basic_info(resp, 403, expected_method="DELETE") + users = utils.TestSetup.get_RegisteredUsersList(self) + utils.check_val_is_in(anonymous, users, msg="Anonymous special user should still exist.") + @runner.MAGPIE_TEST_USERS def test_DeleteUser_not_found(self): path = "/users/magpie-unittest-random-user" @@ -1201,12 +1261,6 @@ def test_GetGroups(self): ]: utils.check_val_is_in(group, body["group_names"]) - @runner.MAGPIE_TEST_GROUPS - def test_PostGroups(self): - utils.TestSetup.delete_TestGroup(self) # setup as required - utils.TestSetup.create_TestGroup(self) # actual test - utils.TestSetup.delete_TestGroup(self) # cleanup - @runner.MAGPIE_TEST_GROUPS def test_GetGroup_admin(self): admin_grp = get_constant("MAGPIE_ADMIN_GROUP") @@ -1270,6 +1324,35 @@ def test_GetGroup_not_found(self): expect_errors=True) utils.check_response_basic_info(resp, 404, expected_method="GET") + @runner.MAGPIE_TEST_GROUPS + def test_PostGroups(self): + utils.TestSetup.delete_TestGroup(self) # setup as required + utils.TestSetup.create_TestGroup(self) # actual test + utils.TestSetup.delete_TestGroup(self) # cleanup + + @runner.MAGPIE_TEST_GROUPS + def test_PostGroups_conflict(self): + utils.TestSetup.delete_TestGroup(self) + utils.TestSetup.create_TestGroup(self) + data = {"group_name": self.test_group_name} + resp = utils.test_request(self, "POST", "/groups", data=data, expect_errors=True, + headers=self.json_headers, cookies=self.cookies) + utils.check_response_basic_info(resp, 409, expected_method="POST") + + @runner.MAGPIE_TEST_GROUPS + @runner.MAGPIE_TEST_DEFAULTS + def test_DeleteGroup_forbidden_ReservedKeyword_Anonymous(self): + """Even administrator level user is not allowed to remove the special anonymous group.""" + anonymous = get_constant("MAGPIE_ANONYMOUS_GROUP") + groups = utils.TestSetup.get_RegisteredGroupsList(self) + utils.check_val_is_in(anonymous, groups, msg="Anonymous group pre-requirement missing for test.") + path = "/groups/{}".format(anonymous) + resp = utils.test_request(self, "DELETE", path, expect_errors=True, + headers=self.json_headers, cookies=self.cookies) + utils.check_response_basic_info(resp, 403, expected_method="DELETE") + groups = utils.TestSetup.get_RegisteredGroupsList(self) + utils.check_val_is_in(anonymous, groups, msg="Anonymous special group should still exist.") + @runner.MAGPIE_TEST_GROUPS def test_GetGroupUsers(self): path = "/groups/{grp}/users".format(grp=get_constant("MAGPIE_ADMIN_GROUP")) @@ -1303,7 +1386,7 @@ def test_GetGroupServices(self): for svc_type in services: utils.check_val_is_in(svc_type, service_types) # one of valid service types for svc in services[svc_type]: - svc_dict = services[svc_type][svc] + svc_dict = services[svc_type][svc] # type: JSON utils.check_val_type(svc_dict, dict) utils.check_val_is_in("resource_id", svc_dict) utils.check_val_is_in("service_name", svc_dict) @@ -1515,7 +1598,7 @@ def test_GetServiceTypeResources_ResponseFormat(self): utils.check_val_is_in("resource_types", body) utils.check_val_type(body["resource_types"], list) utils.check_val_equal(len(body["resource_types"]) > 0, True) - for rt in body["resource_types"]: + for rt in body["resource_types"]: # type: JSON utils.check_val_type(rt, dict) utils.check_val_is_in("resource_type", rt) utils.check_val_is_in("resource_child_allowed", rt) @@ -1556,7 +1639,7 @@ def test_GetServiceTypeResources_CheckValues(self): body = utils.check_response_basic_info(resp, 200, expected_method="GET") utils.check_val_type(body["resource_types"], list) utils.check_val_equal(len(body["resource_types"]), len(svc_res_info)) - for r in body["resource_types"]: + for r in body["resource_types"]: # type: JSON utils.check_val_is_in(r["resource_type"], svc_res_info) r_type = svc_res_info[r["resource_type"]] utils.check_val_equal(r["resource_child_allowed"], r_type["child"]) @@ -1739,14 +1822,14 @@ def test_ValidateDefaultServiceProviders(self): path = "/users/{usr}/services".format(usr=anonymous) resp = utils.test_request(self, "GET", path, headers=self.json_headers, cookies=self.cookies) body = utils.check_response_basic_info(resp, 200, expected_method="GET") - services_body = body["services"] + services_body = body["services"] # type: JSON for svc in services_list_getcap: svc_name = svc["service_name"] svc_type = svc["service_type"] msg = "Service '{name}' of type '{type}' is expected to have '{perm}' permissions for user '{usr}'." \ .format(name=svc_name, type=svc_type, perm="getcapabilities", usr=anonymous) utils.check_val_is_in(svc_name, services_body[svc_type], msg=msg) - utils.check_val_is_in("getcapabilities", services_body[svc_type][svc_name]["permission_names"]) + utils.check_val_is_in("getcapabilities", services_body[svc_type][svc_name]["permission_names"]) # noqa @runner.MAGPIE_TEST_RESOURCES def test_PostResources_DirectServiceResource(self): @@ -1836,11 +1919,11 @@ class Interface_MagpieUI_NoAuth(six.with_metaclass(ABCMeta, Base_Magpie_TestCase """ Interface class for unittests of Magpie UI. Test any operation that do not require user AuthN/AuthZ. - Derived classes must implement ``setUpClass`` accordingly to generate the Magpie test application. + Derived classes must implement :meth:`initClass` accordingly to generate the Magpie test application. """ @classmethod - def setUpClass(cls): + def initClass(cls): raise NotImplementedError @runner.MAGPIE_TEST_STATUS @@ -1914,7 +1997,7 @@ class Interface_MagpieUI_UsersAuth(six.with_metaclass(ABCMeta, Base_Magpie_TestC """ Interface class for unittests of Magpie UI. Test any operation that require at least logged user AuthN/AuthZ. - Derived classes must implement ``setUpClass`` accordingly to generate the Magpie test application. + Derived classes must implement :meth:`initClass` accordingly to generate the Magpie test application. """ @runner.MAGPIE_TEST_USERS @@ -1949,11 +2032,11 @@ class Interface_MagpieUI_AdminAuth(six.with_metaclass(ABCMeta, Base_Magpie_TestC Interface class for unittests of Magpie UI. Test any operation that require at least 'administrator' group AuthN/AuthZ. - Derived classes must implement ``setUpClass`` accordingly to generate the Magpie test application. + Derived classes must implement :meth:`initClass` accordingly to generate the Magpie test application. """ @classmethod - def setUpClass(cls): + def initClass(cls): raise NotImplementedError @classmethod diff --git a/tests/test_magpie_api.py b/tests/test_magpie_api.py index d3df1b282..d8d1f7644 100644 --- a/tests/test_magpie_api.py +++ b/tests/test_magpie_api.py @@ -32,7 +32,7 @@ class TestCase_MagpieAPI_NoAuth_Local(ti.Interface_MagpieAPI_NoAuth, unittest.Te __test__ = True @classmethod - def setUpClass(cls): + def initClass(cls): cls.app = utils.get_test_magpie_app() cls.json_headers = utils.get_headers(cls.app, {"Accept": CONTENT_TYPE_JSON, "Content-Type": CONTENT_TYPE_JSON}) cls.cookies = None @@ -54,7 +54,7 @@ class TestCase_MagpieAPI_UsersAuth_Local(ti.Interface_MagpieAPI_UsersAuth, unitt __test__ = True @classmethod - def setUpClass(cls): + def initClass(cls): cls.app = utils.get_test_magpie_app() # admin login credentials for setup operations, use 'test' parameters for testing actual feature cls.grp = get_constant("MAGPIE_ADMIN_GROUP") @@ -93,7 +93,7 @@ class TestCase_MagpieAPI_AdminAuth_Local(ti.Interface_MagpieAPI_AdminAuth, unitt __test__ = True @classmethod - def setUpClass(cls): + def initClass(cls): cls.app = utils.get_test_magpie_app() cls.grp = get_constant("MAGPIE_ADMIN_GROUP") cls.usr = get_constant("MAGPIE_TEST_ADMIN_USERNAME") @@ -123,7 +123,7 @@ class TestCase_MagpieAPI_NoAuth_Remote(ti.Interface_MagpieAPI_NoAuth, unittest.T __test__ = True @classmethod - def setUpClass(cls): + def initClass(cls): cls.url = get_constant("MAGPIE_TEST_REMOTE_SERVER_URL") cls.json_headers = utils.get_headers(cls.url, {"Accept": CONTENT_TYPE_JSON, "Content-Type": CONTENT_TYPE_JSON}) cls.cookies = None @@ -145,7 +145,7 @@ class TestCase_MagpieAPI_UsersAuth_Remote(ti.Interface_MagpieAPI_UsersAuth, unit __test__ = True @classmethod - def setUpClass(cls): + def initClass(cls): cls.url = get_constant("MAGPIE_TEST_REMOTE_SERVER_URL") @@ -162,7 +162,7 @@ class TestCase_MagpieAPI_AdminAuth_Remote(ti.Interface_MagpieAPI_AdminAuth, unit __test__ = True @classmethod - def setUpClass(cls): + def initClass(cls): cls.grp = get_constant("MAGPIE_ADMIN_GROUP") cls.usr = get_constant("MAGPIE_TEST_ADMIN_USERNAME") cls.pwd = get_constant("MAGPIE_TEST_ADMIN_PASSWORD") diff --git a/tests/utils.py b/tests/utils.py index e894b81b0..9473a9d9a 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -29,8 +29,8 @@ # pylint: disable=W0611,unused-import from tests.interfaces import Base_Magpie_TestCase # noqa: F401 from magpie.typedefs import ( # noqa: F401 - Str, Callable, Dict, HeadersType, OptionalHeaderCookiesType, Optional, Type, - AnyMagpieTestType, AnyResponseType, TestAppOrUrlType + Any, AnyMagpieTestType, AnyHeadersType, AnyResponseType, Callable, Dict, Iterable, HeadersType, JSON, List, + Optional, OptionalHeaderCookiesType, SettingsType, Str, TestAppOrUrlType, Type, Union ) OptionalStringType = six.string_types + tuple([type(None)]) # pylint: disable=C0103,invalid-name # noqa: 802 @@ -134,6 +134,8 @@ def config_setup_from_ini(config_ini_file_path): def get_test_magpie_app(settings=None): + # type: (Optional[SettingsType]) -> TestApp + """Instantiate a Magpie local test application.""" # parse settings from ini file to pass them to the application config = config_setup_from_ini(get_constant("MAGPIE_INI_FILE_PATH")) config.include("ziggurat_foundations.ext.pyramid.sign_in") @@ -148,6 +150,7 @@ def get_test_magpie_app(settings=None): def get_app_or_url(test_item): # type: (AnyMagpieTestType) -> TestAppOrUrlType + """Obtains the referenced Magpie local application or remote URL from Test Suite implementation.""" if isinstance(test_item, (TestApp, six.string_types)): return test_item app_or_url = getattr(test_item, "app", None) or getattr(test_item, "url", None) @@ -158,6 +161,7 @@ def get_app_or_url(test_item): def get_hostname(test_item): # type: (AnyMagpieTestType) -> Str + """Obtains stored hostname in the class implementation.""" app_or_url = get_app_or_url(test_item) if isinstance(app_or_url, TestApp): app_or_url = get_magpie_url(app_or_url.app.registry) @@ -165,16 +169,22 @@ def get_hostname(test_item): def get_headers(app_or_url, header_dict): + # type: (TestAppOrUrlType, AnyHeadersType) -> HeadersType + """Obtains stored headers in the class implementation.""" if isinstance(app_or_url, TestApp): return header_dict.items() return header_dict def get_response_content_types_list(response): + # type: (AnyResponseType) -> List[Str] + """Obtains the specified response Content-Type header(s) without additional formatting parameters.""" return [ct.strip() for ct in response.headers["Content-Type"].split(";")] def get_json_body(response): + # type: (AnyResponseType) -> JSON + """Obtains the JSON payload of the response regardless of its class implementation.""" if isinstance(response, TestResponse): return response.json return response.json() @@ -406,29 +416,49 @@ def all_equal(iter_val, iter_ref, any_order=False): return all(it == ir for it, ir in zip(iter_val, iter_ref)) -def check_all_equal(iter_val, iter_ref, any_order=False, msg=None): +def check_all_equal(iter_val, iter_ref, msg=None, any_order=False): + # type: (Iterable[Any], Union[Iterable[Any], NullType], Optional[Str], bool) -> None + """ + :param iter_val: tested values. + :param iter_ref: reference values. + :param msg: override message to display if failing test. + :param any_order: allow equal values to be provided in any order, otherwise order must match as well as values. + :raises AssertionError: + If all values in :paramref:`iter_val` are not equal to values within :paramref:`iter_ref`. + If :paramref:`any_order` is ``False``, also raises if equal items are not in the same order. + """ r_it_val = repr(iter_val) r_it_ref = repr(iter_ref) assert all_equal(iter_val, iter_ref, any_order), format_test_val_ref(r_it_val, r_it_ref, pre="Equal Fail", msg=msg) def check_val_equal(val, ref, msg=None): + # type: (Any, Union[Any, NullType], Optional[Str]) -> None + """:raises AssertionError: if :paramref:`val` is not equal to :paramref:`ref`.""" assert is_null(ref) or val == ref, format_test_val_ref(val, ref, pre="Equal Fail", msg=msg) def check_val_not_equal(val, ref, msg=None): + # type: (Any, Union[Any, NullType], Optional[Str]) -> None + """:raises AssertionError: if :paramref:`val` is equal to :paramref:`ref`.""" assert is_null(ref) or val != ref, format_test_val_ref(val, ref, pre="Equal Fail", msg=msg) def check_val_is_in(val, ref, msg=None): + # type: (Any, Union[Any, NullType], Optional[Str]) -> None + """:raises AssertionError: if :paramref:`val` is not in to :paramref:`ref`.""" assert is_null(ref) or val in ref, format_test_val_ref(val, ref, pre="Is In Fail", msg=msg) def check_val_not_in(val, ref, msg=None): + # type: (Any, Union[Any, NullType], Optional[Str]) -> None + """:raises AssertionError: if :paramref:`val` is in to :paramref:`ref`.""" assert is_null(ref) or val not in ref, format_test_val_ref(val, ref, pre="Not In Fail", msg=msg) def check_val_type(val, ref, msg=None): + # type: (Any, Union[Any, NullType], Optional[Str]) -> None + """:raises AssertionError: if :paramref:`val` is not an instanced of :paramref:`ref`.""" assert isinstance(val, ref), format_test_val_ref(val, repr(ref), pre="Type Fail", msg=msg) @@ -462,6 +492,7 @@ def check_no_raise(func): def check_response_basic_info(response, expected_code=200, expected_type=CONTENT_TYPE_JSON, expected_method="GET"): + # type: (AnyResponseType, int, Str, Str) -> JSON """ Validates basic Magpie API response metadata. @@ -502,7 +533,7 @@ def check_ui_response_basic_info(response, expected_code=200, expected_type=CONT check_val_is_in("Magpie Administration", response.text, msg=null) # don't output big html if failing -class _NullType(six.with_metaclass(SingletonMeta)): +class NullType(six.with_metaclass(SingletonMeta)): """ Represents a null value to differentiate from None. """ @@ -518,11 +549,11 @@ def __nonzero__(): __len__ = __nonzero__ -null = _NullType() # pylint: disable=C0103,invalid-name +null = NullType() # pylint: disable=C0103,invalid-name def is_null(item): - return isinstance(item, _NullType) or item is null + return isinstance(item, NullType) or item is null def check_error_param_structure(json_body, param_value=null, param_name=null, param_compare=null, @@ -763,6 +794,7 @@ def get_ExistingTestServiceInfo(test_class): @staticmethod def get_TestServiceDirectResources(test_class, ignore_missing_service=False): + # type: (AnyMagpieTestType, bool) -> List[JSON] app_or_url = get_app_or_url(test_class) path = "/services/{svc}/resources".format(svc=test_class.test_service_name) resp = test_request(app_or_url, "GET", path,