| #!/usr/bin/env python |
| # |
| # Copyright 2007 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. |
| # |
| """Utilities for testing WSGI apps.""" |
| |
| |
| |
| import cStringIO |
| import unittest |
| import wsgiref.headers |
| |
| from google.appengine.tools.devappserver2 import request_rewriter |
| |
| |
| class WSGITestCase(unittest.TestCase): |
| """Base class for tests involving requests to a WSGI application.""" |
| |
| @staticmethod |
| def _normalize_headers(headers): |
| """Normalize a headers set to a list with lowercased names. |
| |
| Args: |
| headers: A sequence of pairs, a dict or a wsgiref.headers.Headers object. |
| |
| Returns: |
| headers, converted to a sequence of pairs (if it was not already), with |
| all of the header names lowercased. |
| """ |
| if (isinstance(headers, dict) or |
| isinstance(headers, wsgiref.headers.Headers)): |
| headers = headers.items() |
| |
| return [(name.lower(), value) for name, value in headers] |
| |
| def assertHeadersEqual(self, expected, actual, msg=None): |
| """Tests whether two sets of HTTP headers are equivalent. |
| |
| The header sets expected and actual are equal if they both have exactly the |
| same set of header name/value pairs. Header names are considered |
| case-insensitive, but header values are case sensitive. The order does not |
| matter, but duplicate headers (headers of the same name) must be the same in |
| both. |
| |
| Note: This should be used instead of assertEqual, assertItemsEqual or |
| assertDictEqual. Using standard asserts for lists and dicts will be case |
| sensitive. Using assert for wsgiref.headers.Headers does reference equality. |
| |
| Args: |
| expected: A sequence of pairs, a dict or a wsgiref.headers.Headers object. |
| actual: A sequence of pairs, a dict or a wsgiref.headers.Headers object. |
| msg: A custom error message to display, if the test fails. |
| |
| Raises: |
| AssertionError if expected and actual are not equal. |
| """ |
| expected = self._normalize_headers(expected) |
| actual = self._normalize_headers(actual) |
| |
| for name, value in actual: |
| self.assertIsInstance(name, str, 'header name %r must be a str' % name) |
| self.assertIsInstance(name, str, 'header value %r must be a str' % value) |
| self.assertItemsEqual(expected, actual, msg) |
| |
| def assertResponse(self, |
| expected_status, |
| expected_headers, |
| expected_content, |
| fn, |
| *args, |
| **kwargs): |
| """Calls fn(*args, <start_response>, **kwargs) and checks the result. |
| |
| Args: |
| expected_status: The expected HTTP status returned e.g. '200 OK'. |
| expected_headers: A dict, list or wsgiref.headers.Headers representing the |
| expected generated HTTP headers e.g. {'Content-type': 'text/plain'}. |
| expected_content: The expected content generated e.g. 'Hello World'. |
| fn: The function to test. This function will be called with |
| fn(*args, start_response=<func>, **kwargs) where the start_response |
| function verifies that it is called with the correct status and |
| headers. |
| *args: The positional arguments passed to fn. |
| **kwargs: The keyword arguments passed to fn. |
| """ |
| |
| # Buffer for 'write' callable |
| write_buffer = cStringIO.StringIO() |
| |
| def start_response(status, headers, exc_info=None): |
| self.assertEqual(expected_status, status) |
| self.assertHeadersEqual(expected_headers, headers) |
| self.assertEqual(None, exc_info) |
| return write_buffer.write |
| |
| args += (start_response,) |
| response = fn(*args, **kwargs) |
| # We sometimes use a function that might, but should not, return None |
| # (URLHandler.handle_authorization). Explicitly test for this to avoid a |
| # confusing TypeError on the following line. |
| self.assertNotEqual(None, response, '%r(*args, **kwargs) != None' % fn) |
| response = ''.join(response) |
| self.assertIsInstance( |
| response, str, 'response %r must be a str' % response[:10]) |
| self.assertMultiLineEqual(expected_content, |
| write_buffer.getvalue() + response) |
| |
| |
| class RewriterTestCase(WSGITestCase): |
| """Base class for test cases that test rewriter functionality.""" |
| |
| rewriter_middleware = staticmethod( |
| request_rewriter.frontend_rewriter_middleware) |
| |
| def assert_rewritten_response(self, expected_status, expected_headers, |
| expected_body, application, environ=None): |
| """Tests that a rewritten application produces the expected response. |
| |
| This applies the response rewriter chain to application and then tests the |
| result. |
| |
| Args: |
| expected_status: The expected HTTP status returned e.g. '200 OK'. |
| expected_headers: A dict, list or wsgiref.headers.Headers representing the |
| expected generated HTTP headers e.g. {'Content-type': 'text/plain'}. |
| expected_body: The expected content generated e.g. 'Hello World'. |
| application: A WSGI application that will be called and have its response |
| rewritten. |
| environ: Optional environment to pass to the application. |
| """ |
| if environ is None: |
| environ = {} |
| wrapped_application = self.rewriter_middleware(application) |
| self.assertResponse(expected_status, expected_headers, expected_body, |
| wrapped_application, environ) |
| |
| |
| def constant_app(status, headers, body): |
| """Creates a WSGI application that produces a constant response. |
| |
| Args: |
| status: A status code and message as a string. (e.g., '200 OK'.) |
| headers: A list of tuples containing the response headers. |
| body: A string containing the response body. |
| |
| Returns: |
| A WSGI application, as defined in PEP-333. |
| """ |
| |
| def application(unused_environ, start_response): |
| start_response(status, headers) |
| return [body] |
| |
| return application |