PageRenderTime 52ms CodeModel.GetById 32ms app.highlight 16ms RepoModel.GetById 1ms app.codeStats 0ms

/atom/mock_http_core.py

http://radioappz.googlecode.com/
Python | 323 lines | 290 code | 13 blank | 20 comment | 1 complexity | 209522f565186e6c24e3bfc1239aa752 MD5 | raw file
  1#!/usr/bin/env python
  2#
  3# Copyright (C) 2009 Google Inc.
  4#
  5# Licensed under the Apache License, Version 2.0 (the "License");
  6# you may not use this file except in compliance with the License.
  7# You may obtain a copy of the License at
  8#
  9#      http://www.apache.org/licenses/LICENSE-2.0
 10#
 11# Unless required by applicable law or agreed to in writing, software
 12# distributed under the License is distributed on an "AS IS" BASIS,
 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 14# See the License for the specific language governing permissions and
 15# limitations under the License.
 16
 17
 18# This module is used for version 2 of the Google Data APIs.
 19
 20
 21__author__ = 'j.s@google.com (Jeff Scudder)'
 22
 23
 24import StringIO
 25import pickle
 26import os.path
 27import tempfile
 28import atom.http_core
 29
 30
 31class Error(Exception):
 32  pass
 33
 34
 35class NoRecordingFound(Error):
 36  pass
 37
 38
 39class MockHttpClient(object):
 40  debug = None
 41  real_client = None
 42  last_request_was_live = False
 43
 44  # The following members are used to construct the session cache temp file
 45  # name.
 46  # These are combined to form the file name
 47  # /tmp/cache_prefix.cache_case_name.cache_test_name
 48  cache_name_prefix = 'gdata_live_test'
 49  cache_case_name = ''
 50  cache_test_name = ''
 51
 52  def __init__(self, recordings=None, real_client=None):
 53    self._recordings = recordings or []
 54    if real_client is not None:
 55      self.real_client = real_client
 56
 57  def add_response(self, http_request, status, reason, headers=None,
 58      body=None):
 59    response = MockHttpResponse(status, reason, headers, body)
 60    # TODO Scrub the request and the response.
 61    self._recordings.append((http_request._copy(), response))
 62
 63  AddResponse = add_response
 64
 65  def request(self, http_request):
 66    """Provide a recorded response, or record a response for replay.
 67
 68    If the real_client is set, the request will be made using the
 69    real_client, and the response from the server will be recorded.
 70    If the real_client is None (the default), this method will examine
 71    the recordings and find the first which matches.
 72    """
 73    request = http_request._copy()
 74    _scrub_request(request)
 75    if self.real_client is None:
 76      self.last_request_was_live = False
 77      for recording in self._recordings:
 78        if _match_request(recording[0], request):
 79          return recording[1]
 80    else:
 81      # Pass along the debug settings to the real client.
 82      self.real_client.debug = self.debug
 83      # Make an actual request since we can use the real HTTP client.
 84      self.last_request_was_live = True
 85      response = self.real_client.request(http_request)
 86      scrubbed_response = _scrub_response(response)
 87      self.add_response(request, scrubbed_response.status,
 88                        scrubbed_response.reason,
 89                        dict(atom.http_core.get_headers(scrubbed_response)),
 90                        scrubbed_response.read())
 91      # Return the recording which we just added.
 92      return self._recordings[-1][1]
 93    raise NoRecordingFound('No recoding was found for request: %s %s' % (
 94        request.method, str(request.uri)))
 95
 96  Request = request
 97
 98  def _save_recordings(self, filename):
 99    recording_file = open(os.path.join(tempfile.gettempdir(), filename),
100                          'wb')
101    pickle.dump(self._recordings, recording_file)
102    recording_file.close()
103
104  def _load_recordings(self, filename):
105    recording_file = open(os.path.join(tempfile.gettempdir(), filename),
106                          'rb')
107    self._recordings = pickle.load(recording_file)
108    recording_file.close()
109
110  def _delete_recordings(self, filename):
111    full_path = os.path.join(tempfile.gettempdir(), filename)
112    if os.path.exists(full_path):
113      os.remove(full_path)
114
115  def _load_or_use_client(self, filename, http_client):
116    if os.path.exists(os.path.join(tempfile.gettempdir(), filename)):
117      self._load_recordings(filename)
118    else:
119      self.real_client = http_client
120
121  def use_cached_session(self, name=None, real_http_client=None):
122    """Attempts to load recordings from a previous live request.
123
124    If a temp file with the recordings exists, then it is used to fulfill
125    requests. If the file does not exist, then a real client is used to
126    actually make the desired HTTP requests. Requests and responses are
127    recorded and will be written to the desired temprary cache file when
128    close_session is called.
129
130    Args:
131      name: str (optional) The file name of session file to be used. The file
132            is loaded from the temporary directory of this machine. If no name
133            is passed in, a default name will be constructed using the
134            cache_name_prefix, cache_case_name, and cache_test_name of this
135            object.
136      real_http_client: atom.http_core.HttpClient the real client to be used
137                        if the cached recordings are not found. If the default
138                        value is used, this will be an
139                        atom.http_core.HttpClient.
140    """
141    if real_http_client is None:
142      real_http_client = atom.http_core.HttpClient()
143    if name is None:
144      self._recordings_cache_name = self.get_cache_file_name()
145    else:
146      self._recordings_cache_name = name
147    self._load_or_use_client(self._recordings_cache_name, real_http_client)
148
149  def close_session(self):
150    """Saves recordings in the temporary file named in use_cached_session."""
151    if self.real_client is not None:
152      self._save_recordings(self._recordings_cache_name)
153
154  def delete_session(self, name=None):
155    """Removes recordings from a previous live request."""
156    if name is None:
157      self._delete_recordings(self._recordings_cache_name)
158    else:
159      self._delete_recordings(name)
160
161  def get_cache_file_name(self):
162    return '%s.%s.%s' % (self.cache_name_prefix, self.cache_case_name,
163                         self.cache_test_name)
164
165  def _dump(self):
166    """Provides debug information in a string."""
167    output = 'MockHttpClient\n  real_client: %s\n  cache file name: %s\n' % (
168        self.real_client, self.get_cache_file_name())
169    output += '  recordings:\n'
170    i = 0
171    for recording in self._recordings:
172      output += '    recording %i is for: %s %s\n' % (
173          i, recording[0].method, str(recording[0].uri))
174      i += 1
175    return output
176
177
178def _match_request(http_request, stored_request):
179  """Determines whether a request is similar enough to a stored request
180     to cause the stored response to be returned."""
181  # Check to see if the host names match.
182  if (http_request.uri.host is not None
183      and http_request.uri.host != stored_request.uri.host):
184    return False
185  # Check the request path in the URL (/feeds/private/full/x)
186  elif http_request.uri.path != stored_request.uri.path:
187    return False
188  # Check the method used in the request (GET, POST, etc.)
189  elif http_request.method != stored_request.method:
190    return False
191  # If there is a gsession ID in either request, make sure that it is matched
192  # exactly.
193  elif ('gsessionid' in http_request.uri.query
194        or 'gsessionid' in stored_request.uri.query):
195    if 'gsessionid' not in stored_request.uri.query:
196      return False
197    elif 'gsessionid' not in http_request.uri.query:
198      return False
199    elif (http_request.uri.query['gsessionid']
200          != stored_request.uri.query['gsessionid']):
201      return False
202  # Ignores differences in the query params (?start-index=5&max-results=20),
203  # the body of the request, the port number, HTTP headers, just to name a
204  # few.
205  return True
206
207
208def _scrub_request(http_request):
209  """ Removes email address and password from a client login request.
210
211  Since the mock server saves the request and response in plantext, sensitive
212  information like the password should be removed before saving the
213  recordings. At the moment only requests sent to a ClientLogin url are
214  scrubbed.
215  """
216  if (http_request and http_request.uri and http_request.uri.path and
217      http_request.uri.path.endswith('ClientLogin')):
218    # Remove the email and password from a ClientLogin request.
219    http_request._body_parts = []
220    http_request.add_form_inputs(
221        {'form_data': 'client login request has been scrubbed'})
222  else:
223    # We can remove the body of the post from the recorded request, since
224    # the request body is not used when finding a matching recording.
225    http_request._body_parts = []
226  return http_request
227
228
229def _scrub_response(http_response):
230  return http_response
231
232
233class EchoHttpClient(object):
234  """Sends the request data back in the response.
235
236  Used to check the formatting of the request as it was sent. Always responds
237  with a 200 OK, and some information from the HTTP request is returned in
238  special Echo-X headers in the response. The following headers are added
239  in the response:
240  'Echo-Host': The host name and port number to which the HTTP connection is
241               made. If no port was passed in, the header will contain
242               host:None.
243  'Echo-Uri': The path portion of the URL being requested. /example?x=1&y=2
244  'Echo-Scheme': The beginning of the URL, usually 'http' or 'https'
245  'Echo-Method': The HTTP method being used, 'GET', 'POST', 'PUT', etc.
246  """
247
248  def request(self, http_request):
249    return self._http_request(http_request.uri, http_request.method,
250                              http_request.headers, http_request._body_parts)
251
252  def _http_request(self, uri, method, headers=None, body_parts=None):
253    body = StringIO.StringIO()
254    response = atom.http_core.HttpResponse(status=200, reason='OK', body=body)
255    if headers is None:
256      response._headers = {}
257    else:
258      # Copy headers from the request to the response but convert values to
259      # strings. Server response headers always come in as strings, so an int
260      # should be converted to a corresponding string when echoing.
261      for header, value in headers.iteritems():
262        response._headers[header] = str(value)
263    response._headers['Echo-Host'] = '%s:%s' % (uri.host, str(uri.port))
264    response._headers['Echo-Uri'] = uri._get_relative_path()
265    response._headers['Echo-Scheme'] = uri.scheme
266    response._headers['Echo-Method'] = method
267    for part in body_parts:
268      if isinstance(part, str):
269        body.write(part)
270      elif hasattr(part, 'read'):
271        body.write(part.read())
272    body.seek(0)
273    return response
274
275
276class SettableHttpClient(object):
277  """An HTTP Client which responds with the data given in set_response."""
278
279  def __init__(self, status, reason, body, headers):
280    """Configures the response for the server.
281
282    See set_response for details on the arguments to the constructor.
283    """
284    self.set_response(status, reason, body, headers)
285    self.last_request = None
286
287  def set_response(self, status, reason, body, headers):
288    """Determines the response which will be sent for each request.
289
290    Args:
291      status: An int for the HTTP status code, example: 200, 404, etc.
292      reason: String for the HTTP reason, example: OK, NOT FOUND, etc.
293      body: The body of the HTTP response as a string or a file-like
294            object (something with a read method).
295      headers: dict of strings containing the HTTP headers in the response.
296    """
297    self.response = atom.http_core.HttpResponse(status=status, reason=reason,
298        body=body)
299    self.response._headers = headers.copy()
300
301  def request(self, http_request):
302    self.last_request = http_request
303    return self.response
304
305
306class MockHttpResponse(atom.http_core.HttpResponse):
307
308  def __init__(self, status=None, reason=None, headers=None, body=None):
309    self._headers = headers or {}
310    if status is not None:
311      self.status = status
312    if reason is not None:
313      self.reason = reason
314    if body is not None:
315      # Instead of using a file-like object for the body, store as a string
316      # so that reads can be repeated.
317      if hasattr(body, 'read'):
318        self._body = body.read()
319      else:
320        self._body = body
321
322  def read(self):
323    return self._body