github.com/swiftstack/proxyfs@v0.0.0-20201223034610-5434d919416e/pfs_middleware/tests/helpers.py (about) 1 # Copyright (c) 2016-2017 SwiftStack, Inc. 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 # implied. 13 # See the License for the specific language governing permissions and 14 # limitations under the License. 15 16 import collections 17 from swift.common import swob 18 19 20 class FakeProxy(object): 21 """ 22 Vaguely Swift-proxy-server-ish WSGI application used in testing the 23 ProxyFS middleware. 24 """ 25 def __init__(self): 26 self._calls = [] 27 28 # key is a 2-tuple (request-method, url-path) 29 # 30 # value is how many WSGI iterables were created but not destroyed; 31 # if all is well-behaved, the value should be 0 upon completion of 32 # the user's request. 33 self._unclosed_req_paths = collections.defaultdict(int) 34 35 # key is a 2-tuple (request-method, url-path) 36 # 37 # value is a 3-tuple (response status, headers, body) 38 self._responses = {} 39 40 @property 41 def calls(self): 42 return tuple(self._calls) 43 44 def register(self, method, path, response_status, headers, body=''): 45 self._responses[(method, path)] = (response_status, headers, body) 46 47 def __call__(self, env, start_response): 48 method = env['REQUEST_METHOD'] 49 path = env['PATH_INFO'] 50 51 req = swob.Request(env) 52 self._calls.append((method, path, swob.HeaderKeyDict(req.headers))) 53 54 if (env.get('swift.authorize') and not env.get( 55 'swift.authorize_override')): 56 denial_response = env['swift.authorize'](req) 57 if denial_response: 58 return denial_response 59 60 try: 61 status_int, headers, body = self._responses[(method, path)] 62 except KeyError: 63 print("Didn't find \"%s %s\" in registered responses" % ( 64 method, path)) 65 raise 66 67 if method in ('PUT', 'COALESCE'): 68 bytes_read = 0 69 # consume the whole request body, just like a PUT would 70 for chunk in iter(env['wsgi.input'].read, b''): 71 bytes_read += len(chunk) 72 cl = req.headers.get('Content-Length') 73 74 if cl is not None and int(cl) != bytes_read: 75 error_resp = swob.HTTPClientDisconnect( 76 request=req, 77 body=("Content-Length didn't match" 78 " body length (says FakeProxy)")) 79 return error_resp(env, start_response) 80 81 if cl is None \ 82 and "chunked" not in req.headers.get("Transfer-Encoding", ""): 83 error_resp = swob.HTTPLengthRequired( 84 request=req, 85 body="No Content-Length (says FakeProxy)") 86 return error_resp(env, start_response) 87 88 resp = swob.Response( 89 body=body, status=status_int, headers=headers, 90 # We cheat a little here and use swob's handling of the Range 91 # header instead of doing it ourselves. 92 conditional_response=True) 93 94 return resp(env, start_response) 95 96 97 class FakeJsonRpc(object): 98 """ 99 Fake out JSON-RPC calls. 100 101 This object is used to replace the JsonRpcClient helper object. 102 """ 103 def __init__(self): 104 self._calls = [] 105 self._rpc_handlers = {} 106 107 @property 108 def calls(self): 109 return tuple(self._calls) 110 111 def register_handler(self, method, handler): 112 """ 113 :param method: name of JSON-RPC method, e.g. Server.RpcGetObject 114 115 :param handler: callable to handle that method. Callable must take 116 one JSON-RPC object (dict) as argument and return a single 117 JSON-RPC object (dict). 118 """ 119 self._rpc_handlers[method] = handler 120 121 def call(self, rpc_request, _timeout, raise_on_rpc_error=True): 122 # Note: rpc_request here is a JSON-RPC request object. In Python 123 # terms, it's a dictionary with a particular format. 124 125 call_id = rpc_request.get('id') 126 127 # let any exceptions out so callers can see them and fix their 128 # request generators 129 assert rpc_request['jsonrpc'] == '2.0' 130 method = rpc_request['method'] 131 132 # rpc.* are reserved by JSON-RPC 2.0 133 assert not method.startswith("rpc.") 134 135 # let KeyError out so callers can see it and fix their mocks 136 handler = self._rpc_handlers[method] 137 138 # params may be omitted, a list (positional), or a dict (by name) 139 if 'params' not in rpc_request: 140 rpc_response = handler() 141 self._calls.append((method, ())) 142 elif isinstance(rpc_request['params'], (list, tuple)): 143 rpc_response = handler(*(rpc_request['params'])) 144 self._calls.append((method, tuple(rpc_request['params']))) 145 elif isinstance(rpc_request['params'], (dict,)): 146 raise NotImplementedError("haven't needed this yet") 147 else: 148 raise ValueError( 149 "FakeJsonRpc can't handle params of type %s (%r)" % 150 (type(rpc_request['params']), rpc_request['params'])) 151 152 if call_id is None: 153 # a JSON-RPC request without an "id" parameter is a 154 # "notification", i.e. a special request that receives no 155 # response. 156 return None 157 elif 'id' in rpc_response and rpc_response['id'] != call_id: 158 # We don't enforce that the handler pay any attention to 'id', 159 # but if it does, it has to get it right. 160 raise ValueError("handler for %s set 'id' attr to bogus value" % 161 (method,)) 162 else: 163 rpc_response['id'] = call_id 164 return rpc_response