# Copyright 2014 Rackspace # # 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. import json import os import re import time from proboscis.asserts import fail from six.moves.urllib.parse import urlparse from troveclient.compat.client import TroveHTTPClient from trove.tests.config import CONFIG print_req = True def shorten_url(url): parsed = urlparse(url) if parsed.query: method_url = parsed.path + '?' + parsed.query else: method_url = parsed.path return method_url class SnippetWriter(object): def __init__(self, conf, get_replace_list): self.conf = conf self.get_replace_list = get_replace_list def output_request(self, user_details, name, url, output_headers, body, content_type, method, static_auth_token=True): headers = [] parsed = urlparse(url) method_url = shorten_url(url) headers.append("%s %s HTTP/1.1" % (method, method_url)) headers.append("User-Agent: %s" % output_headers['User-Agent']) headers.append("Host: %s" % parsed.netloc) # static_auth_token option for documentation purposes if static_auth_token: output_token = '87c6033c-9ff6-405f-943e-2deb73f278b7' else: output_token = output_headers['X-Auth-Token'] headers.append("X-Auth-Token: %s" % output_token) headers.append("Accept: %s" % output_headers['Accept']) print("OUTPUT HEADERS: %s" % output_headers) headers.append("Content-Type: %s" % output_headers['Content-Type']) self.write_file(user_details, name, "-%s-http.txt" % content_type, url, method, "request", output='\n'.join(headers)) pretty_body = self.format_body(body, content_type) self.write_file(user_details, name, ".%s" % content_type, url, method, "request", output=pretty_body) def output_response(self, user_details, name, content_type, url, method, resp, body): version = "1.1" # if resp.version == 11 else "1.0" lines = [ ["HTTP/%s %s %s" % (version, resp.status, resp.reason)], ["Content-Type: %s" % resp['content-type']], ] if 'via' in resp: lines.append(["Via: %s" % resp['via']]) lines.append(["Content-Length: %s" % resp['content-length']]) lines.append(["Date: Mon, 18 Mar 2013 19:09:17 GMT"]) if 'server' in resp: lines.append(["Server: %s" % resp["server"]]) new_lines = [x[0] for x in lines] joined_lines = '\n'.join(new_lines) self.write_file(user_details, name, "-%s-http.txt" % content_type, url, method, "response", output=joined_lines) if body: pretty_body = self.format_body(body, content_type) self.write_file(user_details, name, ".%s" % content_type, url, method, "response", output=pretty_body) def format_body(self, body, content_type): assert content_type == 'json' try: if self.conf['replace_dns_hostname']: before = r'\"hostname\": \"[a-zA-Z0-9-_\.]*\"' after = '\"hostname\": \"%s\"' % self.conf[ 'replace_dns_hostname'] body = re.sub(before, after, body) return json.dumps(json.loads(body), sort_keys=True, indent=4) except Exception: return body or '' def write_request_file(self, user_details, name, content_type, url, method, req_headers, request_body): if print_req: print("\t%s req url:%s" % (content_type, url)) print("\t%s req method:%s" % (content_type, method)) print("\t%s req headers:%s" % (content_type, req_headers)) print("\t%s req body:%s" % (content_type, request_body)) self.output_request(user_details, name, url, req_headers, request_body, content_type, method) def write_response_file(self, user_details, name, content_type, url, method, resp, resp_content): if print_req: print("\t%s resp:%s" % (content_type, resp)) print("\t%s resp content:%s" % (content_type, resp_content)) self.output_response(user_details, name, content_type, url, method, resp, resp_content) def write_file(self, user_details, name, content_type, url, method, in_or_out, output): output = output.replace(user_details['tenant'], '1234') if self.conf['replace_host']: output = output.replace(user_details['api_url'], self.conf['replace_host']) pre_host_port = urlparse(user_details['service_url']).netloc post_host = urlparse(self.conf['replace_host']).netloc output = output.replace(pre_host_port, post_host) output = output.replace("fake_host", "hostname") output = output.replace("FAKE_", "") for resource in self.get_replace_list(): output = output.replace(str(resource[0]), str(resource[1])) filename = "%s/db-%s-%s%s" % (self.conf['directory'], name.replace('_', '-'), in_or_out, content_type) self._write_file(filename, output) def _write_file(self, filename, output): empty = len(output.strip()) == 0 # Manipulate actual data to appease doc niceness checks actual = [line.rstrip() for line in output.split("\n")] if not empty and actual[len(actual) - 1] != '': actual.append("") def goofy_diff(a, b): diff = [] for i in range(len(a)): if i < len(b): if a[i].rstrip() != b[i].rstrip(): diff.append('Expected line %d :%s\n' ' Actual line %d :%s' % (i + 1, a[i], i + 1, b[i])) else: diff.append("Expected line %d :%s" % (i + 1, a[i])) for j in range(len(b) - len(a)): i2 = len(a) + j diff.append(" Actual line %d :%s" % (i2 + 1, b[i2])) return diff def write_actual_file(): # Always write the file. with open(filename, "w") as file: for line in actual: file.write("%s\n" % line) def assert_output_matches(): if os.path.isfile(filename): with open(filename, 'r') as original_file: original = original_file.read() if empty: fail('Error: output missing in new snippet generation ' 'for %s. Old content follows:\n"""%s"""' % (filename, original)) elif filename.endswith('.json'): assert_json_matches(original) else: assert_file_matches(original) elif not empty: fail('Error: new file necessary where there was no file ' 'before. Filename=%s\nContent follows:\n"""%s"""' % (filename, output)) def assert_file_matches(original): expected = original.split('\n') # Remove the last item which will look like a duplicated # file ending newline expected.pop() diff = '\n'.join(goofy_diff(expected, actual)) if diff: fail('Error: output files differ for %s:\n%s' % (filename, diff)) def order_json(json_obj): """Sort the json object so that it can be compared properly.""" if isinstance(json_obj, list): return sorted(order_json(elem) for elem in json_obj) if isinstance(json_obj, dict): return sorted( (key, order_json(value)) for key, value in json_obj.items()) else: return json_obj def assert_json_matches(original): try: expected_json = json.loads(original) actual_json = json.loads(output) except ValueError: fail('Invalid json!\nExpected: %s\nActual: %s' % (original, output)) if order_json(expected_json) != order_json(actual_json): # Re-Use the same failure output if the json is different assert_file_matches(original) if not os.environ.get('TESTS_FIX_EXAMPLES'): assert_output_matches() elif not empty: write_actual_file() # This method is mixed into the client class. # It requires the following fields: snippet_writer, content_type, and # "name," the last of which must be set before each call. def write_to_snippet(self, args, kwargs, resp, body): if self.name is None: raise RuntimeError("'name' not set before call.") url = args[0] method = args[1] request_headers = kwargs['headers'] request_body = kwargs.get('body', None) response_headers = resp response_body = body # Log request user_details = { 'api_url': self.service_url, 'service_url': self.service_url, 'tenant': self.tenant, } self.snippet_writer.write_request_file(user_details, self.name, self.content_type, url, method, request_headers, request_body) self.snippet_writer.write_response_file(user_details, self.name, self.content_type, url, method, response_headers, response_body) # Create a short url to assert against. short_url = url base_url = self.service_url for prefix in (base_url): if short_url.startswith(prefix): short_url = short_url[len(prefix):] self.old_info = { 'url': shorten_url(short_url), 'method': method, 'request_headers': request_headers, 'request_body': request_body, 'response_headers': response_headers, 'response_body': response_body } def add_fake_response_headers(headers): """ Fakes other items that would appear if you were using, just to make up an example, a proxy. """ conf = CONFIG.examples if 'via' in conf and 'via' not in headers: headers['via'] = conf['via'] if 'server' in conf and 'server' not in headers: headers['server'] = conf['server'] if 'date' not in headers: date_string = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime()) headers['date'] = date_string class JsonClient(TroveHTTPClient): content_type = 'json' def http_log(self, args, kwargs, resp, body): add_fake_response_headers(resp) self.pretty_log(args, kwargs, resp, body) def write_snippet(): return write_to_snippet(self, args, kwargs, resp, body) self.write_snippet = write_snippet