/atom/mock_service.py
Python | 243 lines | 174 code | 12 blank | 57 comment | 17 complexity | 3832989352a1630fbd83a05df3bdbfe5 MD5 | raw file
1#!/usr/bin/python 2# 3# Copyright (C) 2008 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"""MockService provides CRUD ops. for mocking calls to AtomPub services. 19 20 MockService: Exposes the publicly used methods of AtomService to provide 21 a mock interface which can be used in unit tests. 22""" 23 24import atom.service 25import pickle 26 27 28__author__ = 'api.jscudder (Jeffrey Scudder)' 29 30 31# Recordings contains pairings of HTTP MockRequest objects with MockHttpResponse objects. 32recordings = [] 33# If set, the mock service HttpRequest are actually made through this object. 34real_request_handler = None 35 36def ConcealValueWithSha(source): 37 import sha 38 return sha.new(source[:-5]).hexdigest() 39 40def DumpRecordings(conceal_func=ConcealValueWithSha): 41 if conceal_func: 42 for recording_pair in recordings: 43 recording_pair[0].ConcealSecrets(conceal_func) 44 return pickle.dumps(recordings) 45 46def LoadRecordings(recordings_file_or_string): 47 if isinstance(recordings_file_or_string, str): 48 atom.mock_service.recordings = pickle.loads(recordings_file_or_string) 49 elif hasattr(recordings_file_or_string, 'read'): 50 atom.mock_service.recordings = pickle.loads( 51 recordings_file_or_string.read()) 52 53def HttpRequest(service, operation, data, uri, extra_headers=None, 54 url_params=None, escape_params=True, content_type='application/atom+xml'): 55 """Simulates an HTTP call to the server, makes an actual HTTP request if 56 real_request_handler is set. 57 58 This function operates in two different modes depending on if 59 real_request_handler is set or not. If real_request_handler is not set, 60 HttpRequest will look in this module's recordings list to find a response 61 which matches the parameters in the function call. If real_request_handler 62 is set, this function will call real_request_handler.HttpRequest, add the 63 response to the recordings list, and respond with the actual response. 64 65 Args: 66 service: atom.AtomService object which contains some of the parameters 67 needed to make the request. The following members are used to 68 construct the HTTP call: server (str), additional_headers (dict), 69 port (int), and ssl (bool). 70 operation: str The HTTP operation to be performed. This is usually one of 71 'GET', 'POST', 'PUT', or 'DELETE' 72 data: ElementTree, filestream, list of parts, or other object which can be 73 converted to a string. 74 Should be set to None when performing a GET or PUT. 75 If data is a file-like object which can be read, this method will read 76 a chunk of 100K bytes at a time and send them. 77 If the data is a list of parts to be sent, each part will be evaluated 78 and sent. 79 uri: The beginning of the URL to which the request should be sent. 80 Examples: '/', '/base/feeds/snippets', 81 '/m8/feeds/contacts/default/base' 82 extra_headers: dict of strings. HTTP headers which should be sent 83 in the request. These headers are in addition to those stored in 84 service.additional_headers. 85 url_params: dict of strings. Key value pairs to be added to the URL as 86 URL parameters. For example {'foo':'bar', 'test':'param'} will 87 become ?foo=bar&test=param. 88 escape_params: bool default True. If true, the keys and values in 89 url_params will be URL escaped when the form is constructed 90 (Special characters converted to %XX form.) 91 content_type: str The MIME type for the data being sent. Defaults to 92 'application/atom+xml', this is only used if data is set. 93 """ 94 full_uri = atom.service.BuildUri(uri, url_params, escape_params) 95 (server, port, ssl, uri) = atom.service.ProcessUrl(service, uri) 96 current_request = MockRequest(operation, full_uri, host=server, ssl=ssl, 97 data=data, extra_headers=extra_headers, url_params=url_params, 98 escape_params=escape_params, content_type=content_type) 99 # If the request handler is set, we should actually make the request using 100 # the request handler and record the response to replay later. 101 if real_request_handler: 102 response = real_request_handler.HttpRequest(service, operation, data, uri, 103 extra_headers=extra_headers, url_params=url_params, 104 escape_params=escape_params, content_type=content_type) 105 # TODO: need to copy the HTTP headers from the real response into the 106 # recorded_response. 107 recorded_response = MockHttpResponse(body=response.read(), 108 status=response.status, reason=response.reason) 109 # Insert a tuple which maps the request to the response object returned 110 # when making an HTTP call using the real_request_handler. 111 recordings.append((current_request, recorded_response)) 112 return recorded_response 113 else: 114 # Look through available recordings to see if one matches the current 115 # request. 116 for request_response_pair in recordings: 117 if request_response_pair[0].IsMatch(current_request): 118 return request_response_pair[1] 119 return None 120 121 122class MockRequest(object): 123 """Represents a request made to an AtomPub server. 124 125 These objects are used to determine if a client request matches a recorded 126 HTTP request to determine what the mock server's response will be. 127 """ 128 129 def __init__(self, operation, uri, host=None, ssl=False, port=None, 130 data=None, extra_headers=None, url_params=None, escape_params=True, 131 content_type='application/atom+xml'): 132 """Constructor for a MockRequest 133 134 Args: 135 operation: str One of 'GET', 'POST', 'PUT', or 'DELETE' this is the 136 HTTP operation requested on the resource. 137 uri: str The URL describing the resource to be modified or feed to be 138 retrieved. This should include the protocol (http/https) and the host 139 (aka domain). For example, these are some valud full_uris: 140 'http://example.com', 'https://www.google.com/accounts/ClientLogin' 141 host: str (optional) The server name which will be placed at the 142 beginning of the URL if the uri parameter does not begin with 'http'. 143 Examples include 'example.com', 'www.google.com', 'www.blogger.com'. 144 ssl: boolean (optional) If true, the request URL will begin with https 145 instead of http. 146 data: ElementTree, filestream, list of parts, or other object which can be 147 converted to a string. (optional) 148 Should be set to None when performing a GET or PUT. 149 If data is a file-like object which can be read, the constructor 150 will read the entire file into memory. If the data is a list of 151 parts to be sent, each part will be evaluated and stored. 152 extra_headers: dict (optional) HTTP headers included in the request. 153 url_params: dict (optional) Key value pairs which should be added to 154 the URL as URL parameters in the request. For example uri='/', 155 url_parameters={'foo':'1','bar':'2'} could become '/?foo=1&bar=2'. 156 escape_params: boolean (optional) Perform URL escaping on the keys and 157 values specified in url_params. Defaults to True. 158 content_type: str (optional) Provides the MIME type of the data being 159 sent. 160 """ 161 self.operation = operation 162 self.uri = _ConstructFullUrlBase(uri, host=host, ssl=ssl) 163 self.data = data 164 self.extra_headers = extra_headers 165 self.url_params = url_params or {} 166 self.escape_params = escape_params 167 self.content_type = content_type 168 169 def ConcealSecrets(self, conceal_func): 170 """Conceal secret data in this request.""" 171 if self.extra_headers.has_key('Authorization'): 172 self.extra_headers['Authorization'] = conceal_func( 173 self.extra_headers['Authorization']) 174 175 def IsMatch(self, other_request): 176 """Check to see if the other_request is equivalent to this request. 177 178 Used to determine if a recording matches an incoming request so that a 179 recorded response should be sent to the client. 180 181 The matching is not exact, only the operation and URL are examined 182 currently. 183 184 Args: 185 other_request: MockRequest The request which we want to check this 186 (self) MockRequest against to see if they are equivalent. 187 """ 188 # More accurate matching logic will likely be required. 189 return (self.operation == other_request.operation and self.uri == 190 other_request.uri) 191 192 193def _ConstructFullUrlBase(uri, host=None, ssl=False): 194 """Puts URL components into the form http(s)://full.host.strinf/uri/path 195 196 Used to construct a roughly canonical URL so that URLs which begin with 197 'http://example.com/' can be compared to a uri of '/' when the host is 198 set to 'example.com' 199 200 If the uri contains 'http://host' already, the host and ssl parameters 201 are ignored. 202 203 Args: 204 uri: str The path component of the URL, examples include '/' 205 host: str (optional) The host name which should prepend the URL. Example: 206 'example.com' 207 ssl: boolean (optional) If true, the returned URL will begin with https 208 instead of http. 209 210 Returns: 211 String which has the form http(s)://example.com/uri/string/contents 212 """ 213 if uri.startswith('http'): 214 return uri 215 if ssl: 216 return 'https://%s%s' % (host, uri) 217 else: 218 return 'http://%s%s' % (host, uri) 219 220 221class MockHttpResponse(object): 222 """Returned from MockService crud methods as the server's response.""" 223 224 def __init__(self, body=None, status=None, reason=None, headers=None): 225 """Construct a mock HTTPResponse and set members. 226 227 Args: 228 body: str (optional) The HTTP body of the server's response. 229 status: int (optional) 230 reason: str (optional) 231 headers: dict (optional) 232 """ 233 self.body = body 234 self.status = status 235 self.reason = reason 236 self.headers = headers or {} 237 238 def read(self): 239 return self.body 240 241 def getheader(self, header_name): 242 return self.headers[header_name] 243