diff --git a/src/encoded/__init__.py b/src/encoded/__init__.py index 6340e4ef68..4cce918ddf 100644 --- a/src/encoded/__init__.py +++ b/src/encoded/__init__.py @@ -35,7 +35,7 @@ # from webob.cookies import JSONSerializer from .loadxl import load_all -from .util import find_other_in_pair +# from .util import find_other_in_pair if sys.version_info.major < 3: diff --git a/src/encoded/renderers.py b/src/encoded/renderers.py index 7a122b8ff4..ff52ce2f76 100644 --- a/src/encoded/renderers.py +++ b/src/encoded/renderers.py @@ -4,28 +4,26 @@ import psutil import time -from pkg_resources import resource_filename -from urllib.parse import urlencode, urlparse +from dcicutils.misc_utils import environ_bool, PRINT, ignored from functools import lru_cache +from pkg_resources import resource_filename from pyramid.events import BeforeRender, subscriber from pyramid.httpexceptions import ( HTTPMovedPermanently, HTTPPreconditionFailed, HTTPUnauthorized, - # HTTPForbidden, HTTPUnsupportedMediaType, HTTPNotAcceptable, HTTPServerError ) -# from pyramid.security import forget +from pyramid.response import Response from pyramid.settings import asbool from pyramid.threadlocal import manager -from pyramid.response import Response from pyramid.traversal import split_path_info, _join_path_tuple -# from snovault.validation import CSRFTokenError -# from subprocess_middleware.tween import SubprocessTween from subprocess_middleware.worker import TransformWorker +from urllib.parse import urlencode from webob.cookies import Cookie +from .util import content_type_allowed log = logging.getLogger(__name__) @@ -70,10 +68,12 @@ def includeme(config): """ config.add_tween('.renderers.validate_request_tween_factory', under='snovault.stats.stats_tween_factory') - # DISABLED - .add_tween('.renderers.remove_expired_session_cookies_tween_factory', under='.renderers.validate_request_tween_factory') + # DISABLED - .add_tween('.renderers.remove_expired_session_cookies_tween_factory', + # under='.renderers.validate_request_tween_factory') config.add_tween('.renderers.render_page_html_tween_factory', under='.renderers.validate_request_tween_factory') - # The above tweens, when using response (= `handler(request)`) act on the _transformed_ response (containing HTML body). + # The above tweens, when using response (= `handler(request)`) act on the _transformed_ response + # (containing HTML body). # The below tweens run _before_ the JS rendering. Responses in these tweens have not been transformed to HTML yet. config.add_tween('.renderers.set_response_headers_tween_factory', under='.renderers.render_page_html_tween_factory') @@ -93,6 +93,7 @@ def validate_request_tween_factory(handler, registry): Apache config: SetEnvIf Request_Method HEAD X_REQUEST_METHOD=HEAD """ + ignored(registry) def validate_request_tween(request): @@ -107,19 +108,18 @@ def validate_request_tween(request): # Includes page text/html requests. return handler(request) - elif request.content_type != 'application/json': - if request.content_type == 'application/x-www-form-urlencoded' and request.path[0:10] == '/metadata/': - # Special case to allow us to POST to metadata TSV requests via form submission - return handler(request) + elif content_type_allowed(request): + return handler(request) + + else: detail = "Request content type %s is not 'application/json'" % request.content_type raise HTTPUnsupportedMediaType(detail) - return handler(request) - return validate_request_tween def security_tween_factory(handler, registry): + ignored(registry) def security_tween(request): """ @@ -147,14 +147,20 @@ def security_tween(request): raise HTTPUnauthorized( title="No Access", comment="Invalid Authorization header or Auth Challenge response.", - headers={'WWW-Authenticate': "Bearer realm=\"{}\"; Basic realm=\"{}\"".format(request.domain, request.domain) } + headers={ + 'WWW-Authenticate': ( + f'Bearer realm="{request.domain}";' + f' Basic realm="{request.domain}"' + ) + } ) if hasattr(request, 'auth0_expired'): # Add some security-related headers on the up-swing response = handler(request) if request.auth0_expired: - #return response + # return response + # # If have the attribute and it is true, then our session has expired. # This is true for both AJAX requests (which have request.authorization) & browser page # requests (which have cookie); both cases handled in authentication.py @@ -167,14 +173,17 @@ def security_tween(request): # to be synced w/ server-side response.set_cookie( name='jwtToken', - value=None, + value=None, # = i.e., same as response.delete_cookie(..) domain=request.domain, max_age=0, path='/', overwrite=True - ) # = Same as response.delete_cookie(..) + ) response.status_code = 401 - response.headers['WWW-Authenticate'] = "Bearer realm=\"{}\", title=\"Session Expired\"; Basic realm=\"{}\"".format(request.domain, request.domain) + response.headers['WWW-Authenticate'] = ( + f'Bearer realm="{request.domain}", title="Session Expired";' + f' Basic realm="{request.domain}"' + ) else: # We have JWT and it's not expired. Add 'X-Request-JWT' & 'X-User-Info' header. # For performance, only do it if should transform to HTML as is not needed on every request. @@ -186,8 +195,12 @@ def security_tween(request): # This header is parsed in renderer.js, or, more accurately, # by libs/react-middleware.js which is imported by server.js and compiled into # renderer.js. Is used to get access to User Info on initial web page render. - response.headers['X-Request-JWT'] = request.cookies.get('jwtToken','') - user_info = request.user_info.copy() # Re-ified property set in authentication.py + response.headers['X-Request-JWT'] = request.cookies.get('jwtToken', '') + user_info = request.user_info.copy() # Re-ified property set in authentication.py + # On CGAP we are using HTTPOnly cookie and not storing token inside user_info ever + # (so don't need to delete it from there). In 4DN the token is stored/duplicated in + # authentication.py. If we port the HTTPOnly code, we presumably won't need this del. + # -kmp & alexb 28-Jan-2022 del user_info["id_token"] # Redundant - don't need this in SSR nor browser as get from X-Request-JWT. response.headers['X-User-Info'] = json.dumps(user_info) else: @@ -196,20 +209,6 @@ def security_tween(request): return handler(request) - # This was commented out when we introduced JWT authentication - # Theoretically we mitigate CSRF requests now by grabbing JWT for transactional - # requests from Authorization header which acts like a CSRF token. - # See authentication.py - get_jwt() - - #token = request.headers.get('X-CSRF-Token') - #if token is not None: - # # Avoid dirtying the session and adding a Set-Cookie header - # # XXX Should consider if this is a good idea or not and timeouts - # if token == dict.get(request.session, '_csrft_', None): - # return handler(request) - # raise CSRFTokenError('Incorrect CSRF token') - # raise CSRFTokenError('Missing CSRF token') - return security_tween @@ -222,7 +221,8 @@ def remove_expired_session_cookies_tween_factory(handler, registry): We disable it for now via removing from tween chain as are using JWT tokens and handling their removal in security_tween_factory & authentication.py as well as client-side (upon "Logout" action). If needed for some reason, can re-enable. - """ + """ # noQA - not going to break the long URL line above + ignored(registry) ignore = { '/favicon.ico', @@ -233,8 +233,8 @@ def remove_expired_session_cookies_tween(request): return handler(request) session = request.session - #if session or session._cookie_name not in request.cookies: - # return handler(request) + # if session or session._cookie_name not in request.cookies: + # return handler(request) response = handler(request) # Below seems to be empty always; though we do have some in request.cookies @@ -259,6 +259,8 @@ def remove_expired_session_cookies_tween(request): def set_response_headers_tween_factory(handler, registry): """Add additional response headers here""" + ignored(registry) + def set_response_headers_tween(request): response = handler(request) response.headers['X-Request-URL'] = request.url @@ -314,14 +316,81 @@ def canonical_redirect(event): raise HTTPMovedPermanently(location=location, detail="Redirected from " + str(request.path_info)) +# Web browsers send an Accept request header for initial (e.g. non-AJAX) page requests +# which should contain 'text/html' +MIME_TYPE_HTML = 'text/html' +MIME_TYPE_JSON = 'application/json' +MIME_TYPE_LD_JSON = 'application/ld+json' + +# Note: In cgap-portal, MIME_TYPE_JSON is at the head of this list. In fourfront, MIME_TYPE_HTML is. +# The cgap-portal behavior might be a bug we should look at bringing into alignment. -kmp 29-Jan-2022 +MIME_TYPES_SUPPORTED = [MIME_TYPE_HTML, MIME_TYPE_JSON, MIME_TYPE_LD_JSON] +MIME_TYPE_DEFAULT = MIME_TYPES_SUPPORTED[0] +MIME_TYPE_TRIAGE_MODE = 'modern' # if this doesn't work, fall back to 'legacy' + +DEBUG_MIME_TYPES = environ_bool("DEBUG_MIME_TYPES", default=False) + + +def best_mime_type(request, mode=MIME_TYPE_TRIAGE_MODE): + # TODO: I think this function does nothing but return MIME_TYPES_SUPPORTED[0] -kmp 3-Feb-2021 + """ + Given a request, tries to figure out the best kind of MIME type to use in response + based on what kinds of responses we support and what was requested. + + In the case we can't comply, we just use application/json whether or not that's what was asked for. + """ + if mode == 'legacy': + # See: + # https://tedboy.github.io/flask/generated/generated/werkzeug.Accept.best_match.html#werkzeug-accept-best-match + # Note that this is now deprecated, or will be. The message is oddly worded ("will be deprecated") + # that presumably means "will be removed". Deprecation IS the warning of actual action, not the action itself. + # "This is currently maintained for backward compatibility, and will be deprecated in the future. + # AcceptValidHeader.best_match() uses its own algorithm (one not specified in RFC 7231) to determine + # what is a best match. The algorithm has many issues, and does not conform to RFC 7231." + # Anyway, we were getting this warning during testing: + # DeprecationWarning: The behavior of AcceptValidHeader.best_match is currently + # being maintained for backward compatibility, but it will be deprecated in the future, + # as it does not conform to the RFC. + # TODO: Once the modern replacement is shown to work, we should remove this conditional branch. + result = request.accept.best_match(MIME_TYPES_SUPPORTED, MIME_TYPE_DEFAULT) + else: + options = request.accept.acceptable_offers(MIME_TYPES_SUPPORTED) + if not options: + # TODO: Probably we should return a 406 response by raising HTTPNotAcceptable if + # no acceptable types are available. (Certainly returning JSON in this case is + # not some kind of friendly help toa naive user with an old browser.) + # Ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status + result = MIME_TYPE_DEFAULT + else: + mime_type, score = options[0] + result = mime_type + if DEBUG_MIME_TYPES: + PRINT("Using mime type", result, "for", request.method, request.url) + for k, v in request.headers.items(): + PRINT("%s: %s" % (k, v)) + PRINT("----------") + return result + + @lru_cache(maxsize=16) def should_transform(request, response): """ Determines whether to transform the response from JSON->HTML/JS depending on type of response - and what the request is looking for to be returned via e.g. request Accept, Authorization header. - In case of no Accept header, attempts to guess. - - Memoized via `lru_cache`. Cache size is set to be 16 (> 1) in case sub-requests fired off during handling. + and what the request is looking for to be returned via these criteria, which are tried in order + until one succeeds: + + * If the request method is other than GET or HEAD, returns False. + * If the response.content_type is other than 'application/json', returns False. + * If a 'frame=' query param is given and not 'page' (the default), returns False. + * If a 'format=json' query param is given explicitly, + * For 'format=html', returns True. + * For 'format=json', returns False. + This rule does not match if 'format=' is not given explicitly. + If 'format=' is given an explicit value of ther than 'html' or 'json', an HTTPNotAcceptable error will be raised. + * If the first element of MIME_TYPES_SUPPORTED[0] is 'text/html', returns True. + * Otherwise, in all remaining cases, returns False. + + NOTE: Memoized via `lru_cache`. Cache size is set to be 16 (> 1) in case sub-requests fired off during handling. """ # We always return JSON in response to POST, PATCH, etc. if request.method not in ('GET', 'HEAD'): @@ -331,6 +400,22 @@ def should_transform(request, response): if response.content_type != 'application/json': return False + # cgap uses the following extra check that I think is the right thing, but leaving this commented out + # leads to bug-for-bug compatibility in the PR that revises the more modern transformation logic. + # The question is whether, for example, a conflicting thing like frame=raw&format=html + # should try to transform. I agree with cgap that using non-page frames should ignore format + # and return False, so should return False notwithstanding the format=html. + # Another alternative would be for this conflict to return an error. + # But Fourfront thinks it should just go ahead and try the transform. + # Note that uncommenting this logic will require uncommenting the corresponding line in + # test_should_transform that is presently marked as a bug we should fix. + # + # # If we have a 'frame' that is not None or page, force JSON, since our UI doesn't handle all various + # # forms of the data, just embedded/page. + # request_frame = request.params.get("frame", "page") + # if request_frame != "page": + # return False + # The `format` URI param allows us to override request's 'Accept' header. format_param = request.params.get('format') if format_param is not None: @@ -340,19 +425,20 @@ def should_transform(request, response): if format_param == 'html': return True else: - raise HTTPNotAcceptable("Improper format URI parameter", comment="The format URI parameter should be set to either html or json.") + raise HTTPNotAcceptable("Improper format URI parameter", + comment="The format URI parameter should be set to either html or json.") # Web browsers send an Accept request header for initial (e.g. non-AJAX) page requests # which should contain 'text/html' # See: https://tedboy.github.io/flask/generated/generated/werkzeug.Accept.best_match.html#werkzeug-accept-best-match - mime_type = request.accept.best_match(['text/html', 'application/json', 'application/ld+json'], 'application/json') - mime_type_format = mime_type.split('/', 1)[1] # Will be 1 of 'html', 'json', 'json-ld' + mime_type = best_mime_type(request) # Result will be one of MIME_TYPES_SUPPORTED - # N.B. ld+json (JSON-LD) is likely more unique case and might be sent by search engines (?) which can parse JSON-LDs. - # At some point we could maybe have it to be same as making an `@@object` or `?frame=object` request (?) esp if fill + # N.B. ld+json (JSON-LD) is likely more unique case and might be sent by search engines (?) + # which can parse JSON-LDs. At some point we could maybe have it to be same as + # making an `@@object` or `?frame=object` request (?) esp if fill # out @context response w/ schema(s) (or link to schema) - return mime_type_format == 'html' + return mime_type == MIME_TYPE_HTML def render_page_html_tween_factory(handler, registry): @@ -374,7 +460,9 @@ class TransformErrorResponse(HTTPServerError): rss_limit = 256 * (1024 ** 2) # MB - reload_process = True if registry.settings.get('reload_templates', False) else lambda proc: psutil.Process(proc.pid).memory_info().rss > rss_limit + reload_process = (True + if registry.settings.get('reload_templates', False) + else lambda proc: psutil.Process(proc.pid).memory_info().rss > rss_limit) # TransformWorker inits and manages a subprocess # it re-uses the subprocess so interestingly data in JS global variables diff --git a/src/encoded/tests/test_renderers.py b/src/encoded/tests/test_renderers.py index 15e67091b4..20226ce1cc 100644 --- a/src/encoded/tests/test_renderers.py +++ b/src/encoded/tests/test_renderers.py @@ -5,7 +5,12 @@ from dcicutils.misc_utils import filtered_warnings from dcicutils.qa_utils import MockResponse from pyramid.testing import DummyRequest -from ..renderers import should_transform +from unittest import mock +from .. import renderers +from ..renderers import ( + best_mime_type, should_transform, MIME_TYPES_SUPPORTED, MIME_TYPE_DEFAULT, + MIME_TYPE_JSON, MIME_TYPE_HTML, MIME_TYPE_LD_JSON, MIME_TYPE_TRIAGE_MODE, +) pytestmark = [pytest.mark.setone, pytest.mark.working] @@ -20,8 +25,53 @@ def __init__(self, content_type=None, status_code: int = 200, json=None, content super().__init__(status_code=status_code, json=json, content=content, url=url) +def test_mime_variables(): + + # Really these don't need testing but it's useful visually to remind us of their values here. + assert MIME_TYPE_HTML == 'text/html' + assert MIME_TYPE_JSON == 'application/json' + assert MIME_TYPE_LD_JSON == 'application/ld+json' + + # The MIME_TYPES_SUPPORTED is a list whose first element has elevated importance as we've structured things. + # First check that it is a list, and that its contents contain the things we support. That isn't controversial. + assert isinstance(MIME_TYPES_SUPPORTED, list) + assert set(MIME_TYPES_SUPPORTED) == {MIME_TYPE_JSON, MIME_TYPE_HTML, MIME_TYPE_LD_JSON} + # Check that the first element is consistent with the MIME_TYPE_DEFAULT. + # It's an accident of history that this next relationship matters, but at this point check for consistency. + assert MIME_TYPE_DEFAULT == MIME_TYPES_SUPPORTED[0] + # Now we concern ourselves with the actual values... + # TODO: I think it's a bug that JSON is at the head of this list (and so the default) in cgap-portal. + # cgap-portal needs to be made to match what Fourfront does to dig it out of a bug I introduced. + # -kmp 29-Jan-2022 + assert MIME_TYPES_SUPPORTED == [MIME_TYPE_HTML, MIME_TYPE_JSON, MIME_TYPE_LD_JSON] + assert MIME_TYPE_DEFAULT == MIME_TYPE_HTML + + # Regardless of whether we're using legacy mode or modern mode, we should get the same result. + assert MIME_TYPE_TRIAGE_MODE in ['legacy', 'modern'] + + VARIOUS_MIME_TYPES_TO_TEST = ['*/*', 'text/html', 'application/json', 'application/ld+json', 'text/xml', 'who/cares'] + +def test_best_mime_type(): + + the_constant_answer=MIME_TYPE_DEFAULT + + with filtered_warnings("ignore", category=DeprecationWarning): + # Suppresses this warning: + # DeprecationWarning: The behavior of .best_match for the Accept classes is currently being maintained + # for backward compatibility, but the method will be deprecated in the future, as its behavior is not + # specified in (and currently does not conform to) RFC 7231. + + for requested_mime_type in VARIOUS_MIME_TYPES_TO_TEST: + req = DummyRequest(headers={'Accept': requested_mime_type}) + assert best_mime_type(req, 'legacy') == the_constant_answer + assert best_mime_type(req, 'modern') == the_constant_answer + req = DummyRequest(headers={}) # The Accept header in the request just isn't being consulted + assert best_mime_type(req, 'modern') == the_constant_answer + assert best_mime_type(req, 'modern') == the_constant_answer + + TYPICAL_URLS = [ 'http://whatever/foo', 'http://whatever/foo/', @@ -134,3 +184,18 @@ def test_should_transform(): % (n_passed, n_failed, n_of(problem_area, "problem area"), ", ".join(problem_area)) ) print("\n", n_passed, "combinations tried. ALL PASSED") + + +def test_should_transform_without_best_mime_type(): + + # As we call things now, we really don't need the best_mime_type function because it just returns the + # first element of its first argument. That probably should change. Because it should be a function + # of the request and its Accept offerings. Even so, we test for this now not because this makes programs + # right, but so we notice if/when this truth changes. -kmp 23-Mar-2021 + + with mock.patch.object(renderers, "best_mime_type") as mock_best_mime_type: + + # Demonstrate that best_mime_type(...) could be replaced by MIME_TYPES_SUPPORTED[0] + mock_best_mime_type.return_value = MIME_TYPES_SUPPORTED[0] + + test_should_transform() diff --git a/src/encoded/tests/test_types_init_collections.py b/src/encoded/tests/test_types_init_collections.py index f2431a9636..bd2bec3674 100644 --- a/src/encoded/tests/test_types_init_collections.py +++ b/src/encoded/tests/test_types_init_collections.py @@ -1,7 +1,8 @@ import pytest +from dcicutils.misc_utils import utc_today_str from ..types.image import Image -from ..util import utc_today_str +# from ..util import utc_today_str pytestmark = [pytest.mark.setone, pytest.mark.working, pytest.mark.schema] diff --git a/src/encoded/tests/test_types_protocol.py b/src/encoded/tests/test_types_protocol.py index 8a2febd9cc..8ab43db866 100644 --- a/src/encoded/tests/test_types_protocol.py +++ b/src/encoded/tests/test_types_protocol.py @@ -1,7 +1,8 @@ -import datetime +# import datetime import pytest -from ..util import utc_today_str +from dcicutils.misc_utils import utc_today_str +# from ..util import utc_today_str pytestmark = [pytest.mark.setone, pytest.mark.working, pytest.mark.schema] diff --git a/src/encoded/tests/test_util.py b/src/encoded/tests/test_util.py new file mode 100644 index 0000000000..0d16d34a03 --- /dev/null +++ b/src/encoded/tests/test_util.py @@ -0,0 +1,97 @@ +import datetime +import pytest +import re +from pyramid.httpexceptions import HTTPForbidden + +from ..util import ( + # compute_set_difference_one, find_other_in_pair, + delay_rerun, # utc_today_str, + customized_delay_rerun, check_user_is_logged_in +) + + +pytestmark = pytest.mark.working + + +# def test_compute_set_difference_one(): +# """ Tests that our set difference utility function behaves correctly under various cases """ +# s1 = {1, 2, 3, 4} +# s2 = {1, 2, 3} +# assert compute_set_difference_one(s1, s2) == 4 +# s1 = {1, 2, 3, 4} +# s2 = {1, 2, 3, 4} +# with pytest.raises(StopIteration): +# compute_set_difference_one(s1, s2) +# with pytest.raises(StopIteration): +# compute_set_difference_one(s2, s1) +# s1 = {1, 2, 3, 4, 5, 6} +# s2 = {1, 2, 3, 4} +# with pytest.raises(RuntimeError): +# compute_set_difference_one(s1, s2) +# with pytest.raises(StopIteration): +# compute_set_difference_one(s2, s1) +# s1 = {1, 2} +# s2 = {1} +# assert compute_set_difference_one(s1, s2) == 2 +# +# +# def test_find_other_in_pair(): +# """ Tests the wrapper for the above function """ +# assert find_other_in_pair(1, [1, 2]) == 2 +# assert find_other_in_pair(2, [1, 2]) == 1 +# lst = [1, 2, 3] +# val = [1, 2] +# with pytest.raises(TypeError): +# find_other_in_pair(val, lst) # val is 'not single valued' +# val = 1 +# with pytest.raises(TypeError): +# find_other_in_pair(val, None) # no pair to compare to +# with pytest.raises(RuntimeError): # too many results +# find_other_in_pair(None, lst) + + +DELAY_FUZZ_SECONDS = 0.1 + + +def test_delay_rerun(): + expected_delay = 1.0 + t0 = datetime.datetime.now() + delay_rerun() + t1 = datetime.datetime.now() + assert (t1 - t0).total_seconds() > expected_delay + assert (t1 - t0).total_seconds() < expected_delay + DELAY_FUZZ_SECONDS + + +def test_customize_delay_rerun(): + custom_delay = 0.5 + half_delay_rerun = customized_delay_rerun(sleep_seconds=custom_delay) + t0 = datetime.datetime.now() + half_delay_rerun() + t1 = datetime.datetime.now() + assert (t1 - t0).total_seconds() > custom_delay + assert (t1 - t0).total_seconds() < custom_delay + DELAY_FUZZ_SECONDS + + +# def test_utc_today_str(): +# pattern = "[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]" +# actual = utc_today_str() +# assert re.match(pattern, actual), "utc_today_str() result %s did not match format: %s" % (actual, pattern) + + +@pytest.mark.parametrize('principals, allow', [ + (['role1', 'role2'], False), + (['role1', 'userid.uuid'], True), + (['role1', 'group.admin'], True), + (['system.Everyone'], False) +]) +def test_check_user_is_logged_in(principals, allow): + """ Simple test that ensures the logged in check is working as expected """ + class MockRequest: + def __init__(self, principals): + self.effective_principals = principals + req = MockRequest(principals) + if allow: + check_user_is_logged_in(req) + else: + with pytest.raises(HTTPForbidden): + check_user_is_logged_in(req) diff --git a/src/encoded/tests/test_utils.py b/src/encoded/tests/test_utils.py deleted file mode 100644 index 4c7fbca5da..0000000000 --- a/src/encoded/tests/test_utils.py +++ /dev/null @@ -1,94 +0,0 @@ -import datetime -import pytest -import re -from pyramid.httpexceptions import HTTPForbidden - -from ..util import (compute_set_difference_one, find_other_in_pair, delay_rerun, utc_today_str, - customized_delay_rerun, check_user_is_logged_in) - - -pytestmark = pytest.mark.working - - -def test_compute_set_difference_one(): - """ Tests that our set difference utility function behaves correctly under various cases """ - s1 = {1, 2, 3, 4} - s2 = {1, 2, 3} - assert compute_set_difference_one(s1, s2) == 4 - s1 = {1, 2, 3, 4} - s2 = {1, 2, 3, 4} - with pytest.raises(StopIteration): - compute_set_difference_one(s1, s2) - with pytest.raises(StopIteration): - compute_set_difference_one(s2, s1) - s1 = {1, 2, 3, 4, 5, 6} - s2 = {1, 2, 3, 4} - with pytest.raises(RuntimeError): - compute_set_difference_one(s1, s2) - with pytest.raises(StopIteration): - compute_set_difference_one(s2, s1) - s1 = {1, 2} - s2 = {1} - assert compute_set_difference_one(s1, s2) == 2 - - -def test_find_other_in_pair(): - """ Tests the wrapper for the above function """ - assert find_other_in_pair(1, [1, 2]) == 2 - assert find_other_in_pair(2, [1, 2]) == 1 - lst = [1, 2, 3] - val = [1, 2] - with pytest.raises(TypeError): - find_other_in_pair(val, lst) # val is 'not single valued' - val = 1 - with pytest.raises(TypeError): - find_other_in_pair(val, None) # no pair to compare to - with pytest.raises(RuntimeError): # too many results - find_other_in_pair(None, lst) - - -DELAY_FUZZ_SECONDS = 0.1 - - -def test_delay_rerun(): - expected_delay = 1.0 - t0 = datetime.datetime.now() - delay_rerun() - t1 = datetime.datetime.now() - assert (t1 - t0).total_seconds() > expected_delay - assert (t1 - t0).total_seconds() < expected_delay + DELAY_FUZZ_SECONDS - - -def test_customize_delay_rerun(): - custom_delay = 0.5 - half_delay_rerun = customized_delay_rerun(sleep_seconds=custom_delay) - t0 = datetime.datetime.now() - half_delay_rerun() - t1 = datetime.datetime.now() - assert (t1 - t0).total_seconds() > custom_delay - assert (t1 - t0).total_seconds() < custom_delay + DELAY_FUZZ_SECONDS - - -def test_utc_today_str(): - pattern = "[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]" - actual = utc_today_str() - assert re.match(pattern, actual), "utc_today_str() result %s did not match format: %s" % (actual, pattern) - - -@pytest.mark.parametrize('principals, allow', [ - (['role1', 'role2'], False), - (['role1', 'userid.uuid'], True), - (['role1', 'group.admin'], True), - (['system.Everyone'], False) -]) -def test_check_user_is_logged_in(principals, allow): - """ Simple test that ensures the logged in check is working as expected """ - class MockRequest: - def __init__(self, principals): - self.effective_principals = principals - req = MockRequest(principals) - if allow: - check_user_is_logged_in(req) - else: - with pytest.raises(HTTPForbidden): - check_user_is_logged_in(req) diff --git a/src/encoded/util.py b/src/encoded/util.py index fb16ce9d67..eae09f78f4 100644 --- a/src/encoded/util.py +++ b/src/encoded/util.py @@ -296,31 +296,31 @@ def beanstalk_env_from_registry(registry): return registry.settings.get('env.name') -def compute_set_difference_one(s1, s2): - """ Computes the set difference between s1 and s2 (ie: in s1 but not in s2) - PRE: s1 and s2 differ by one element and thus their set - difference is a single element - - :arg s1 (set(T)): super set - :arg s2 (set(T)): subset - :returns (T): the single differing element between s1 and s2. - :raises: exception if more than on element is found - """ - res = s1 - s2 - if len(res) > 1: - raise RuntimeError('Got more than one result for set difference') - return next(iter(res)) - - -def find_other_in_pair(element, pair): - """ Wrapper for compute_set_difference_one - - :arg element (T): item to look for in pair - :arg pair (2-tuple of T): pair of things 'element' is in - :returns (T): item in pair that is not element - :raises: exception if types do not match or in compute_set_diferrence_one - """ - return compute_set_difference_one(set(pair), {element}) +# def compute_set_difference_one(s1, s2): +# """ Computes the set difference between s1 and s2 (ie: in s1 but not in s2) +# PRE: s1 and s2 differ by one element and thus their set +# difference is a single element +# +# :arg s1 (set(T)): super set +# :arg s2 (set(T)): subset +# :returns (T): the single differing element between s1 and s2. +# :raises: exception if more than on element is found +# """ +# res = s1 - s2 +# if len(res) > 1: +# raise RuntimeError('Got more than one result for set difference') +# return next(iter(res)) +# +# +# def find_other_in_pair(element, pair): +# """ Wrapper for compute_set_difference_one +# +# :arg element (T): item to look for in pair +# :arg pair (2-tuple of T): pair of things 'element' is in +# :returns (T): item in pair that is not element +# :raises: exception if types do not match or in compute_set_diferrence_one +# """ +# return compute_set_difference_one(set(pair), {element}) def customized_delay_rerun(sleep_seconds=1): @@ -335,8 +335,8 @@ def parameterized_delay_rerun(*args): delay_rerun = customized_delay_rerun(sleep_seconds=1) -def utc_today_str(): - return datetime.datetime.strftime(datetime.datetime.utcnow(), "%Y-%m-%d") +# def utc_today_str(): +# return datetime.datetime.strftime(datetime.datetime.utcnow(), "%Y-%m-%d") def check_user_is_logged_in(request):