From 893acffbc01ba817361f8d8735513dc784fb92c0 Mon Sep 17 00:00:00 2001 From: Pete Zaitcev Date: Wed, 20 Feb 2019 17:09:08 -0600 Subject: [PATCH] py3: port dlo Change-Id: I7236ddea0acde93d0789ad8affa76df0097a86aa --- swift/common/middleware/dlo.py | 51 +++++++--- test/unit/common/middleware/test_dlo.py | 127 ++++++++++++++---------- tox.ini | 1 + 3 files changed, 113 insertions(+), 66 deletions(-) diff --git a/swift/common/middleware/dlo.py b/swift/common/middleware/dlo.py index 6a15f1390c..5334b1037f 100644 --- a/swift/common/middleware/dlo.py +++ b/swift/common/middleware/dlo.py @@ -121,14 +121,14 @@ Here's an example using ``curl`` with tiny 1-byte segments:: import json import six -from six.moves.urllib.parse import unquote from hashlib import md5 from swift.common import constraints from swift.common.exceptions import ListingIterError, SegmentError from swift.common.http import is_success from swift.common.swob import Request, Response, \ - HTTPRequestedRangeNotSatisfiable, HTTPBadRequest, HTTPConflict + HTTPRequestedRangeNotSatisfiable, HTTPBadRequest, HTTPConflict, \ + str_to_wsgi, wsgi_to_str, wsgi_quote, wsgi_unquote from swift.common.utils import get_logger, \ RateLimitedIterator, quote, close_if_possible, closing_if_possible from swift.common.request_helpers import SegmentedIterable @@ -143,9 +143,18 @@ class GetContext(WSGIContext): def _get_container_listing(self, req, version, account, container, prefix, marker=''): + ''' + :param version: whatever + :param account: native + :param container: native + :param prefix: native + :param marker: native + ''' con_req = make_subrequest( req.environ, - path=quote('/'.join(['', version, account, container])), + path=wsgi_quote('/'.join([ + '', str_to_wsgi(version), + str_to_wsgi(account), str_to_wsgi(container)])), method='GET', headers={'x-auth-token': req.headers.get('x-auth-token')}, agent=('%(orig)s ' + 'DLO MultipartGET'), swift_source='DLO') @@ -156,14 +165,24 @@ class GetContext(WSGIContext): con_resp = con_req.get_response(self.dlo.app) if not is_success(con_resp.status_int): if req.method == 'HEAD': - con_resp.body = '' + con_resp.body = b'' return con_resp, None with closing_if_possible(con_resp.app_iter): - return None, json.loads(''.join(con_resp.app_iter)) + return None, json.loads(b''.join(con_resp.app_iter)) def _segment_listing_iterator(self, req, version, account, container, prefix, segments, first_byte=None, last_byte=None): + ''' + :param req: upstream request + :param version: native + :param account: native + :param container: native + :param prefix: native + :param segments: array of dicts, with native strings + :param first_byte: number + :param last_byte: number + ''' # It's sort of hokey that this thing takes in the first page of # segments as an argument, but we need to compute the etag and content # length from the first page, and it's better to have a hokey @@ -173,7 +192,6 @@ class GetContext(WSGIContext): if last_byte is None: last_byte = float("inf") - marker = '' while True: for segment in segments: seg_length = int(segment['bytes']) @@ -188,7 +206,7 @@ class GetContext(WSGIContext): break seg_name = segment['name'] - if isinstance(seg_name, six.text_type): + if six.PY2: seg_name = seg_name.encode("utf-8") # We deliberately omit the etag and size here; @@ -227,16 +245,18 @@ class GetContext(WSGIContext): "Got status %d listing container /%s/%s" % (error_response.status_int, account, container)) - def get_or_head_response(self, req, x_object_manifest, - response_headers=None): - if response_headers is None: - response_headers = self._response_headers + def get_or_head_response(self, req, x_object_manifest): + ''' + :param req: user's request + :param x_object_manifest: as unquoted, native string + ''' + response_headers = self._response_headers container, obj_prefix = x_object_manifest.split('/', 1) - container = unquote(container) - obj_prefix = unquote(obj_prefix) version, account, _junk = req.split_path(2, 3, True) + version = wsgi_to_str(version) + account = wsgi_to_str(account) error_response, segments = self._get_container_listing( req, version, account, container, obj_prefix) if error_response: @@ -311,7 +331,7 @@ class GetContext(WSGIContext): if h.lower() != "etag"] etag = md5() for seg_dict in segments: - etag.update(seg_dict['hash'].strip('"')) + etag.update(seg_dict['hash'].strip('"').encode('utf8')) response_headers.append(('Etag', '"%s"' % etag.hexdigest())) app_iter = None @@ -353,7 +373,8 @@ class GetContext(WSGIContext): for header, value in self._response_headers: if (header.lower() == 'x-object-manifest'): close_if_possible(resp_iter) - response = self.get_or_head_response(req, value) + response = self.get_or_head_response( + req, wsgi_to_str(wsgi_unquote(value))) return response(req.environ, start_response) # Not a dynamic large object manifest; just pass it through. start_response(self._response_status, diff --git a/test/unit/common/middleware/test_dlo.py b/test/unit/common/middleware/test_dlo.py index 8ac99827d9..d0ceadf976 100644 --- a/test/unit/common/middleware/test_dlo.py +++ b/test/unit/common/middleware/test_dlo.py @@ -23,6 +23,8 @@ from textwrap import dedent import time import unittest +import six + from swift.common import swob from swift.common.header_key_dict import HeaderKeyDict from swift.common.middleware import dlo @@ -34,6 +36,8 @@ LIMIT = 'swift.common.constraints.CONTAINER_LISTING_LIMIT' def md5hex(s): + if isinstance(s, six.text_type): + s = s.encode('utf-8') return hashlib.md5(s).hexdigest() @@ -52,7 +56,7 @@ class DloTestCase(unittest.TestCase): headers[0] = h body_iter = app(req.environ, start_response) - body = '' + body = b'' # appease the close-checker with closing_if_possible(body_iter): for chunk in body_iter: @@ -69,36 +73,36 @@ class DloTestCase(unittest.TestCase): self.app.register( 'GET', '/v1/AUTH_test/c/seg_01', swob.HTTPOk, {'Content-Length': '5', 'Etag': md5hex("aaaaa")}, - 'aaaaa') + b'aaaaa') self.app.register( 'GET', '/v1/AUTH_test/c/seg_02', swob.HTTPOk, {'Content-Length': '5', 'Etag': md5hex("bbbbb")}, - 'bbbbb') + b'bbbbb') self.app.register( 'GET', '/v1/AUTH_test/c/seg_03', swob.HTTPOk, {'Content-Length': '5', 'Etag': md5hex("ccccc")}, - 'ccccc') + b'ccccc') self.app.register( 'GET', '/v1/AUTH_test/c/seg_04', swob.HTTPOk, {'Content-Length': '5', 'Etag': md5hex("ddddd")}, - 'ddddd') + b'ddddd') self.app.register( 'GET', '/v1/AUTH_test/c/seg_05', swob.HTTPOk, {'Content-Length': '5', 'Etag': md5hex("eeeee")}, - 'eeeee') + b'eeeee') # an unrelated object (not seg*) to test the prefix matching self.app.register( 'GET', '/v1/AUTH_test/c/catpicture.jpg', swob.HTTPOk, {'Content-Length': '9', 'Etag': md5hex("meow meow meow meow")}, - 'meow meow meow meow') + b'meow meow meow meow') self.app.register( 'GET', '/v1/AUTH_test/mancon/manifest', swob.HTTPOk, {'Content-Length': '17', 'Etag': 'manifest-etag', 'X-Object-Manifest': 'c/seg'}, - 'manifest-contents') + b'manifest-contents') lm = '2013-11-22T02:42:13.781760' ct = 'application/octet-stream' @@ -120,11 +124,11 @@ class DloTestCase(unittest.TestCase): self.app.register( 'GET', '/v1/AUTH_test/c', swob.HTTPOk, {'Content-Type': 'application/json; charset=utf-8'}, - json.dumps(full_container_listing)) + json.dumps(full_container_listing).encode('ascii')) self.app.register( 'GET', '/v1/AUTH_test/c?prefix=seg', swob.HTTPOk, {'Content-Type': 'application/json; charset=utf-8'}, - json.dumps(segs)) + json.dumps(segs).encode('ascii')) # This is to let us test multi-page container listings; we use the # trailing underscore to send small (pagesize=3) listings. @@ -135,26 +139,26 @@ class DloTestCase(unittest.TestCase): 'GET', '/v1/AUTH_test/mancon/manifest-many-segments', swob.HTTPOk, {'Content-Length': '7', 'Etag': 'etag-manyseg', 'X-Object-Manifest': 'c/seg_'}, - 'manyseg') + b'manyseg') self.app.register( 'GET', '/v1/AUTH_test/c?prefix=seg_', swob.HTTPOk, {'Content-Type': 'application/json; charset=utf-8'}, - json.dumps(segs[:3])) + json.dumps(segs[:3]).encode('ascii')) self.app.register( 'GET', '/v1/AUTH_test/c?prefix=seg_&marker=seg_03', swob.HTTPOk, {'Content-Type': 'application/json; charset=utf-8'}, - json.dumps(segs[3:])) + json.dumps(segs[3:]).encode('ascii')) # Here's a manifest with 0 segments self.app.register( 'GET', '/v1/AUTH_test/mancon/manifest-no-segments', swob.HTTPOk, {'Content-Length': '7', 'Etag': 'noseg', 'X-Object-Manifest': 'c/noseg_'}, - 'noseg') + b'noseg') self.app.register( 'GET', '/v1/AUTH_test/c?prefix=noseg_', swob.HTTPOk, {'Content-Type': 'application/json; charset=utf-8'}, - json.dumps([])) + json.dumps([]).encode('ascii')) class TestDloPutManifest(DloTestCase): @@ -284,7 +288,7 @@ class TestDloGetManifest(DloTestCase): headers = HeaderKeyDict(headers) self.assertEqual(headers["Etag"], expected_etag) self.assertEqual(headers["Content-Length"], "25") - self.assertEqual(body, 'aaaaabbbbbcccccdddddeeeee') + self.assertEqual(body, b'aaaaabbbbbcccccdddddeeeee') for _, _, hdrs in self.app.calls_with_headers[1:]: ua = hdrs.get("User-Agent", "") @@ -302,7 +306,7 @@ class TestDloGetManifest(DloTestCase): req = swob.Request.blank('/v1/AUTH_test/c/catpicture.jpg', environ={'REQUEST_METHOD': 'GET'}) status, headers, body = self.call_dlo(req) - self.assertEqual(body, "meow meow meow meow") + self.assertEqual(body, b"meow meow meow meow") def test_get_non_object_passthrough(self): self.app.register('GET', '/info', swob.HTTPOk, @@ -311,7 +315,7 @@ class TestDloGetManifest(DloTestCase): environ={'REQUEST_METHOD': 'GET'}) status, headers, body = self.call_dlo(req) self.assertEqual(status, '200 OK') - self.assertEqual(body, 'useful stuff here') + self.assertEqual(body, b'useful stuff here') self.assertEqual(self.app.call_count, 1) def test_get_manifest_passthrough(self): @@ -328,7 +332,7 @@ class TestDloGetManifest(DloTestCase): status, headers, body = self.call_dlo(req) headers = HeaderKeyDict(headers) self.assertEqual(headers["Etag"], "manifest-etag") - self.assertEqual(body, "manifest-contents") + self.assertEqual(body, b'manifest-contents') def test_error_passthrough(self): self.app.register( @@ -347,7 +351,7 @@ class TestDloGetManifest(DloTestCase): headers = HeaderKeyDict(headers) self.assertEqual(status, "206 Partial Content") self.assertEqual(headers["Content-Length"], "10") - self.assertEqual(body, "bbcccccddd") + self.assertEqual(body, b'bbcccccddd') expected_etag = '"%s"' % md5hex( md5hex("aaaaa") + md5hex("bbbbb") + md5hex("ccccc") + md5hex("ddddd") + md5hex("eeeee")) @@ -361,7 +365,7 @@ class TestDloGetManifest(DloTestCase): headers = HeaderKeyDict(headers) self.assertEqual(status, "206 Partial Content") self.assertEqual(headers["Content-Length"], "10") - self.assertEqual(body, "cccccddddd") + self.assertEqual(body, b'cccccddddd') def test_get_range_first_byte(self): req = swob.Request.blank('/v1/AUTH_test/mancon/manifest', @@ -371,7 +375,7 @@ class TestDloGetManifest(DloTestCase): headers = HeaderKeyDict(headers) self.assertEqual(status, "206 Partial Content") self.assertEqual(headers["Content-Length"], "1") - self.assertEqual(body, "a") + self.assertEqual(body, b'a') def test_get_range_last_byte(self): req = swob.Request.blank('/v1/AUTH_test/mancon/manifest', @@ -381,7 +385,7 @@ class TestDloGetManifest(DloTestCase): headers = HeaderKeyDict(headers) self.assertEqual(status, "206 Partial Content") self.assertEqual(headers["Content-Length"], "1") - self.assertEqual(body, "e") + self.assertEqual(body, b'e') def test_get_range_overlapping_end(self): req = swob.Request.blank('/v1/AUTH_test/mancon/manifest', @@ -392,7 +396,7 @@ class TestDloGetManifest(DloTestCase): self.assertEqual(status, "206 Partial Content") self.assertEqual(headers["Content-Length"], "7") self.assertEqual(headers["Content-Range"], "bytes 18-24/25") - self.assertEqual(body, "ddeeeee") + self.assertEqual(body, b'ddeeeee') def test_get_range_unsatisfiable(self): req = swob.Request.blank('/v1/AUTH_test/mancon/manifest', @@ -428,7 +432,7 @@ class TestDloGetManifest(DloTestCase): # # Since the truth is forbidden, we lie. self.assertEqual(headers["Content-Range"], "bytes 3-12/15") - self.assertEqual(body, "aabbbbbccc") + self.assertEqual(body, b"aabbbbbccc") self.assertEqual( self.app.calls, @@ -449,7 +453,7 @@ class TestDloGetManifest(DloTestCase): # this requires multiple pages of container listing, so we can't send # a Content-Length header self.assertIsNone(headers.get("Content-Length")) - self.assertEqual(body, "aaaaabbbbbcccccdddddeeeee") + self.assertEqual(body, b"aaaaabbbbbcccccdddddeeeee") def test_get_suffix_range(self): req = swob.Request.blank('/v1/AUTH_test/mancon/manifest', @@ -459,7 +463,7 @@ class TestDloGetManifest(DloTestCase): headers = HeaderKeyDict(headers) self.assertEqual(status, "206 Partial Content") self.assertEqual(headers["Content-Length"], "25") - self.assertEqual(body, "aaaaabbbbbcccccdddddeeeee") + self.assertEqual(body, b"aaaaabbbbbcccccdddddeeeee") def test_get_suffix_range_many_segments(self): req = swob.Request.blank('/v1/AUTH_test/mancon/manifest-many-segments', @@ -471,7 +475,7 @@ class TestDloGetManifest(DloTestCase): self.assertEqual(status, "200 OK") self.assertIsNone(headers.get("Content-Length")) self.assertIsNone(headers.get("Content-Range")) - self.assertEqual(body, "aaaaabbbbbcccccdddddeeeee") + self.assertEqual(body, b"aaaaabbbbbcccccdddddeeeee") def test_get_multi_range(self): # DLO doesn't support multi-range GETs. The way that you express that @@ -485,7 +489,7 @@ class TestDloGetManifest(DloTestCase): self.assertEqual(status, "200 OK") self.assertIsNone(headers.get("Content-Length")) self.assertIsNone(headers.get("Content-Range")) - self.assertEqual(body, "aaaaabbbbbcccccdddddeeeee") + self.assertEqual(body, b'aaaaabbbbbcccccdddddeeeee') def test_if_match_matches(self): manifest_etag = '"%s"' % md5hex( @@ -500,7 +504,7 @@ class TestDloGetManifest(DloTestCase): self.assertEqual(status, '200 OK') self.assertEqual(headers['Content-Length'], '25') - self.assertEqual(body, 'aaaaabbbbbcccccdddddeeeee') + self.assertEqual(body, b'aaaaabbbbbcccccdddddeeeee') def test_if_match_does_not_match(self): req = swob.Request.blank('/v1/AUTH_test/mancon/manifest', @@ -512,7 +516,7 @@ class TestDloGetManifest(DloTestCase): self.assertEqual(status, '412 Precondition Failed') self.assertEqual(headers['Content-Length'], '0') - self.assertEqual(body, '') + self.assertEqual(body, b'') def test_if_none_match_matches(self): manifest_etag = '"%s"' % md5hex( @@ -527,7 +531,7 @@ class TestDloGetManifest(DloTestCase): self.assertEqual(status, '304 Not Modified') self.assertEqual(headers['Content-Length'], '0') - self.assertEqual(body, '') + self.assertEqual(body, b'') def test_if_none_match_does_not_match(self): req = swob.Request.blank('/v1/AUTH_test/mancon/manifest', @@ -539,7 +543,7 @@ class TestDloGetManifest(DloTestCase): self.assertEqual(status, '200 OK') self.assertEqual(headers['Content-Length'], '25') - self.assertEqual(body, 'aaaaabbbbbcccccdddddeeeee') + self.assertEqual(body, b'aaaaabbbbbcccccdddddeeeee') def test_get_with_if_modified_since(self): # It's important not to pass the If-[Un]Modified-Since header to the @@ -581,7 +585,11 @@ class TestDloGetManifest(DloTestCase): headers = HeaderKeyDict(headers) self.assertEqual(status, "200 OK") - self.assertEqual(''.join(body), "aaaaa") # first segment made it out + # first segment made it out + if six.PY2: + self.assertEqual(''.join(body), "aaaaa") + else: + self.assertEqual(body, b'aaaaa') self.assertEqual(self.dlo.logger.get_lines_for_level('error'), [ 'While processing manifest /v1/AUTH_test/mancon/manifest, ' 'got 403 while retrieving /v1/AUTH_test/c/seg_02', @@ -610,7 +618,7 @@ class TestDloGetManifest(DloTestCase): with mock.patch(LIMIT, 3): status, headers, body = self.call_dlo(req) self.assertEqual(status, "200 OK") - self.assertEqual(body, "aaaaabbbbbccccc") + self.assertEqual(body, b'aaaaabbbbbccccc') def test_error_listing_container_HEAD(self): self.app.register( @@ -639,7 +647,11 @@ class TestDloGetManifest(DloTestCase): headers = HeaderKeyDict(headers) self.assertEqual(status, "200 OK") - self.assertEqual(''.join(body), "aaaaabbWRONGbb") # stop after error + if six.PY2: + # stop after error + self.assertEqual(''.join(body), "aaaaabbWRONGbb") + else: + self.assertEqual(body, b"aaaaabbWRONGbb") def test_etag_comparison_ignores_quotes(self): # a little future-proofing here in case we ever fix this in swob @@ -662,7 +674,7 @@ class TestDloGetManifest(DloTestCase): status, headers, body = self.call_dlo(req) headers = HeaderKeyDict(headers) self.assertEqual(headers["Etag"], - '"' + hashlib.md5("abcdef").hexdigest() + '"') + '"' + hashlib.md5(b"abcdef").hexdigest() + '"') def test_object_prefix_quoting(self): self.app.register( @@ -675,22 +687,26 @@ class TestDloGetManifest(DloTestCase): self.app.register( 'GET', '/v1/AUTH_test/c?prefix=%C3%A9', swob.HTTPOk, {'Content-Type': 'application/json'}, - json.dumps(segs)) + json.dumps(segs).encode('ascii')) + if six.PY2: + path = b'/v1/AUTH_test/c/\xC3\xa9' + else: + path = u'/v1/AUTH_test/c/\xc3\xa9' self.app.register( - 'GET', '/v1/AUTH_test/c/\xC3\xa91', + 'GET', path + '1', swob.HTTPOk, {'Content-Length': '5', 'Etag': md5hex("AAAAA")}, - "AAAAA") + b"AAAAA") self.app.register( - 'GET', '/v1/AUTH_test/c/\xC3\xA92', + 'GET', path + '2', swob.HTTPOk, {'Content-Length': '5', 'Etag': md5hex("BBBBB")}, - "BBBBB") + b"BBBBB") req = swob.Request.blank('/v1/AUTH_test/man/accent', environ={'REQUEST_METHOD': 'GET'}) status, headers, body = self.call_dlo(req) self.assertEqual(status, "200 OK") - self.assertEqual(body, "AAAAABBBBB") + self.assertEqual(body, b'AAAAABBBBB') def test_get_taking_too_long(self): the_time = [time.time()] @@ -715,7 +731,7 @@ class TestDloGetManifest(DloTestCase): status, headers, body = self.call_dlo(req) self.assertEqual(status, '200 OK') - self.assertEqual(body, 'aaaaabbbbbccccc') + self.assertEqual(body, b'aaaaabbbbbccccc') def test_get_oversize_segment(self): # If we send a Content-Length header to the client, it's based on the @@ -738,7 +754,7 @@ class TestDloGetManifest(DloTestCase): self.assertEqual(status, '200 OK') # sanity check self.assertEqual(headers.get('Content-Length'), '25') # sanity check - self.assertEqual(body, 'aaaaabbbbbccccccccccccccc') + self.assertEqual(body, b'aaaaabbbbbccccccccccccccc') self.assertEqual( self.app.calls, [('GET', '/v1/AUTH_test/mancon/manifest'), @@ -770,7 +786,7 @@ class TestDloGetManifest(DloTestCase): self.assertEqual(status, '200 OK') # sanity check self.assertEqual(headers.get('Content-Length'), '25') # sanity check - self.assertEqual(body, 'aaaaabbbbbccccdddddeeeee') + self.assertEqual(body, b'aaaaabbbbbccccdddddeeeee') def test_get_undersize_segment_range(self): # Shrink it by a single byte @@ -788,7 +804,7 @@ class TestDloGetManifest(DloTestCase): self.assertEqual(status, '206 Partial Content') # sanity check self.assertEqual(headers.get('Content-Length'), '15') # sanity check - self.assertEqual(body, 'aaaaabbbbbcccc') + self.assertEqual(body, b'aaaaabbbbbcccc') def test_get_with_auth_overridden(self): auth_got_called = [0] @@ -840,7 +856,7 @@ class TestDloConfiguration(unittest.TestCase): max_get_time = 2900 """) - conffile = tempfile.NamedTemporaryFile() + conffile = tempfile.NamedTemporaryFile(mode='w') conffile.write(proxy_conf) conffile.flush() @@ -853,6 +869,8 @@ class TestDloConfiguration(unittest.TestCase): self.assertEqual(10, mware.rate_limit_after_segment) self.assertEqual(3600, mware.max_get_time) + conffile.close() + def test_finding_defaults_from_file(self): # If DLO has no config vars, go pull them from the proxy server's # config section @@ -875,7 +893,7 @@ class TestDloConfiguration(unittest.TestCase): set max_get_time = 2900 """) - conffile = tempfile.NamedTemporaryFile() + conffile = tempfile.NamedTemporaryFile(mode='w') conffile.write(proxy_conf) conffile.flush() @@ -887,6 +905,8 @@ class TestDloConfiguration(unittest.TestCase): self.assertEqual(13, mware.rate_limit_after_segment) self.assertEqual(2900, mware.max_get_time) + conffile.close() + def test_finding_defaults_from_dir(self): # If DLO has no config vars, go pull them from the proxy server's # config section @@ -913,11 +933,13 @@ class TestDloConfiguration(unittest.TestCase): conf_dir = self.tmpdir - conffile1 = tempfile.NamedTemporaryFile(dir=conf_dir, suffix='.conf') + conffile1 = tempfile.NamedTemporaryFile(mode='w', + dir=conf_dir, suffix='.conf') conffile1.write(proxy_conf1) conffile1.flush() - conffile2 = tempfile.NamedTemporaryFile(dir=conf_dir, suffix='.conf') + conffile2 = tempfile.NamedTemporaryFile(mode='w', + dir=conf_dir, suffix='.conf') conffile2.write(proxy_conf2) conffile2.flush() @@ -929,6 +951,9 @@ class TestDloConfiguration(unittest.TestCase): self.assertEqual(13, mware.rate_limit_after_segment) self.assertEqual(2900, mware.max_get_time) + conffile1.close() + conffile2.close() + if __name__ == '__main__': unittest.main() diff --git a/tox.ini b/tox.ini index 2b25529225..87b50ee445 100644 --- a/tox.ini +++ b/tox.ini @@ -48,6 +48,7 @@ commands = test/unit/common/middleware/test_container_sync.py \ test/unit/common/middleware/test_copy.py \ test/unit/common/middleware/test_crossdomain.py \ + test/unit/common/middleware/test_dlo.py \ test/unit/common/middleware/test_domain_remap.py \ test/unit/common/middleware/test_formpost.py \ test/unit/common/middleware/test_gatekeeper.py \