/atom/mock_http_core.py
http://radioappz.googlecode.com/ · Python · 323 lines · 209 code · 35 blank · 79 comment · 29 complexity · 209522f565186e6c24e3bfc1239aa752 MD5 · raw file
- #!/usr/bin/env python
- #
- # Copyright (C) 2009 Google Inc.
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- # This module is used for version 2 of the Google Data APIs.
- __author__ = 'j.s@google.com (Jeff Scudder)'
- import StringIO
- import pickle
- import os.path
- import tempfile
- import atom.http_core
- class Error(Exception):
- pass
- class NoRecordingFound(Error):
- pass
- class MockHttpClient(object):
- debug = None
- real_client = None
- last_request_was_live = False
- # The following members are used to construct the session cache temp file
- # name.
- # These are combined to form the file name
- # /tmp/cache_prefix.cache_case_name.cache_test_name
- cache_name_prefix = 'gdata_live_test'
- cache_case_name = ''
- cache_test_name = ''
- def __init__(self, recordings=None, real_client=None):
- self._recordings = recordings or []
- if real_client is not None:
- self.real_client = real_client
- def add_response(self, http_request, status, reason, headers=None,
- body=None):
- response = MockHttpResponse(status, reason, headers, body)
- # TODO Scrub the request and the response.
- self._recordings.append((http_request._copy(), response))
- AddResponse = add_response
- def request(self, http_request):
- """Provide a recorded response, or record a response for replay.
- If the real_client is set, the request will be made using the
- real_client, and the response from the server will be recorded.
- If the real_client is None (the default), this method will examine
- the recordings and find the first which matches.
- """
- request = http_request._copy()
- _scrub_request(request)
- if self.real_client is None:
- self.last_request_was_live = False
- for recording in self._recordings:
- if _match_request(recording[0], request):
- return recording[1]
- else:
- # Pass along the debug settings to the real client.
- self.real_client.debug = self.debug
- # Make an actual request since we can use the real HTTP client.
- self.last_request_was_live = True
- response = self.real_client.request(http_request)
- scrubbed_response = _scrub_response(response)
- self.add_response(request, scrubbed_response.status,
- scrubbed_response.reason,
- dict(atom.http_core.get_headers(scrubbed_response)),
- scrubbed_response.read())
- # Return the recording which we just added.
- return self._recordings[-1][1]
- raise NoRecordingFound('No recoding was found for request: %s %s' % (
- request.method, str(request.uri)))
- Request = request
- def _save_recordings(self, filename):
- recording_file = open(os.path.join(tempfile.gettempdir(), filename),
- 'wb')
- pickle.dump(self._recordings, recording_file)
- recording_file.close()
- def _load_recordings(self, filename):
- recording_file = open(os.path.join(tempfile.gettempdir(), filename),
- 'rb')
- self._recordings = pickle.load(recording_file)
- recording_file.close()
- def _delete_recordings(self, filename):
- full_path = os.path.join(tempfile.gettempdir(), filename)
- if os.path.exists(full_path):
- os.remove(full_path)
- def _load_or_use_client(self, filename, http_client):
- if os.path.exists(os.path.join(tempfile.gettempdir(), filename)):
- self._load_recordings(filename)
- else:
- self.real_client = http_client
- def use_cached_session(self, name=None, real_http_client=None):
- """Attempts to load recordings from a previous live request.
- If a temp file with the recordings exists, then it is used to fulfill
- requests. If the file does not exist, then a real client is used to
- actually make the desired HTTP requests. Requests and responses are
- recorded and will be written to the desired temprary cache file when
- close_session is called.
- Args:
- name: str (optional) The file name of session file to be used. The file
- is loaded from the temporary directory of this machine. If no name
- is passed in, a default name will be constructed using the
- cache_name_prefix, cache_case_name, and cache_test_name of this
- object.
- real_http_client: atom.http_core.HttpClient the real client to be used
- if the cached recordings are not found. If the default
- value is used, this will be an
- atom.http_core.HttpClient.
- """
- if real_http_client is None:
- real_http_client = atom.http_core.HttpClient()
- if name is None:
- self._recordings_cache_name = self.get_cache_file_name()
- else:
- self._recordings_cache_name = name
- self._load_or_use_client(self._recordings_cache_name, real_http_client)
- def close_session(self):
- """Saves recordings in the temporary file named in use_cached_session."""
- if self.real_client is not None:
- self._save_recordings(self._recordings_cache_name)
- def delete_session(self, name=None):
- """Removes recordings from a previous live request."""
- if name is None:
- self._delete_recordings(self._recordings_cache_name)
- else:
- self._delete_recordings(name)
- def get_cache_file_name(self):
- return '%s.%s.%s' % (self.cache_name_prefix, self.cache_case_name,
- self.cache_test_name)
- def _dump(self):
- """Provides debug information in a string."""
- output = 'MockHttpClient\n real_client: %s\n cache file name: %s\n' % (
- self.real_client, self.get_cache_file_name())
- output += ' recordings:\n'
- i = 0
- for recording in self._recordings:
- output += ' recording %i is for: %s %s\n' % (
- i, recording[0].method, str(recording[0].uri))
- i += 1
- return output
- def _match_request(http_request, stored_request):
- """Determines whether a request is similar enough to a stored request
- to cause the stored response to be returned."""
- # Check to see if the host names match.
- if (http_request.uri.host is not None
- and http_request.uri.host != stored_request.uri.host):
- return False
- # Check the request path in the URL (/feeds/private/full/x)
- elif http_request.uri.path != stored_request.uri.path:
- return False
- # Check the method used in the request (GET, POST, etc.)
- elif http_request.method != stored_request.method:
- return False
- # If there is a gsession ID in either request, make sure that it is matched
- # exactly.
- elif ('gsessionid' in http_request.uri.query
- or 'gsessionid' in stored_request.uri.query):
- if 'gsessionid' not in stored_request.uri.query:
- return False
- elif 'gsessionid' not in http_request.uri.query:
- return False
- elif (http_request.uri.query['gsessionid']
- != stored_request.uri.query['gsessionid']):
- return False
- # Ignores differences in the query params (?start-index=5&max-results=20),
- # the body of the request, the port number, HTTP headers, just to name a
- # few.
- return True
- def _scrub_request(http_request):
- """ Removes email address and password from a client login request.
- Since the mock server saves the request and response in plantext, sensitive
- information like the password should be removed before saving the
- recordings. At the moment only requests sent to a ClientLogin url are
- scrubbed.
- """
- if (http_request and http_request.uri and http_request.uri.path and
- http_request.uri.path.endswith('ClientLogin')):
- # Remove the email and password from a ClientLogin request.
- http_request._body_parts = []
- http_request.add_form_inputs(
- {'form_data': 'client login request has been scrubbed'})
- else:
- # We can remove the body of the post from the recorded request, since
- # the request body is not used when finding a matching recording.
- http_request._body_parts = []
- return http_request
- def _scrub_response(http_response):
- return http_response
- class EchoHttpClient(object):
- """Sends the request data back in the response.
- Used to check the formatting of the request as it was sent. Always responds
- with a 200 OK, and some information from the HTTP request is returned in
- special Echo-X headers in the response. The following headers are added
- in the response:
- 'Echo-Host': The host name and port number to which the HTTP connection is
- made. If no port was passed in, the header will contain
- host:None.
- 'Echo-Uri': The path portion of the URL being requested. /example?x=1&y=2
- 'Echo-Scheme': The beginning of the URL, usually 'http' or 'https'
- 'Echo-Method': The HTTP method being used, 'GET', 'POST', 'PUT', etc.
- """
- def request(self, http_request):
- return self._http_request(http_request.uri, http_request.method,
- http_request.headers, http_request._body_parts)
- def _http_request(self, uri, method, headers=None, body_parts=None):
- body = StringIO.StringIO()
- response = atom.http_core.HttpResponse(status=200, reason='OK', body=body)
- if headers is None:
- response._headers = {}
- else:
- # Copy headers from the request to the response but convert values to
- # strings. Server response headers always come in as strings, so an int
- # should be converted to a corresponding string when echoing.
- for header, value in headers.iteritems():
- response._headers[header] = str(value)
- response._headers['Echo-Host'] = '%s:%s' % (uri.host, str(uri.port))
- response._headers['Echo-Uri'] = uri._get_relative_path()
- response._headers['Echo-Scheme'] = uri.scheme
- response._headers['Echo-Method'] = method
- for part in body_parts:
- if isinstance(part, str):
- body.write(part)
- elif hasattr(part, 'read'):
- body.write(part.read())
- body.seek(0)
- return response
- class SettableHttpClient(object):
- """An HTTP Client which responds with the data given in set_response."""
- def __init__(self, status, reason, body, headers):
- """Configures the response for the server.
- See set_response for details on the arguments to the constructor.
- """
- self.set_response(status, reason, body, headers)
- self.last_request = None
- def set_response(self, status, reason, body, headers):
- """Determines the response which will be sent for each request.
- Args:
- status: An int for the HTTP status code, example: 200, 404, etc.
- reason: String for the HTTP reason, example: OK, NOT FOUND, etc.
- body: The body of the HTTP response as a string or a file-like
- object (something with a read method).
- headers: dict of strings containing the HTTP headers in the response.
- """
- self.response = atom.http_core.HttpResponse(status=status, reason=reason,
- body=body)
- self.response._headers = headers.copy()
- def request(self, http_request):
- self.last_request = http_request
- return self.response
- class MockHttpResponse(atom.http_core.HttpResponse):
- def __init__(self, status=None, reason=None, headers=None, body=None):
- self._headers = headers or {}
- if status is not None:
- self.status = status
- if reason is not None:
- self.reason = reason
- if body is not None:
- # Instead of using a file-like object for the body, store as a string
- # so that reads can be repeated.
- if hasattr(body, 'read'):
- self._body = body.read()
- else:
- self._body = body
- def read(self):
- return self._body