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