10847.2.1
by Gary Poster
log out from bzr and openid after logging out from Launchpad |
1 |
# Copyright 2010 Canonical Ltd. This software is licensed under the
|
2 |
# GNU Affero General Public License version 3 (see the file LICENSE).
|
|
3 |
||
12329.1.3
by John Arbash Meinel
Log the failures as 'INFO' and test that they get logged. |
4 |
import cStringIO |
12329.1.1
by John Arbash Meinel
Fix bug #701329, don't generate an OOPS for socket-level errors. |
5 |
import errno |
12329.1.3
by John Arbash Meinel
Log the failures as 'INFO' and test that they get logged. |
6 |
import logging |
10847.2.1
by Gary Poster
log out from bzr and openid after logging out from Launchpad |
7 |
import unittest |
8 |
import urllib |
|
12329.1.1
by John Arbash Meinel
Fix bug #701329, don't generate an OOPS for socket-level errors. |
9 |
import socket |
12329.1.3
by John Arbash Meinel
Log the failures as 'INFO' and test that they get logged. |
10 |
import re |
10847.2.1
by Gary Poster
log out from bzr and openid after logging out from Launchpad |
11 |
|
12 |
import lazr.uri |
|
13 |
import wsgi_intercept |
|
14 |
from wsgi_intercept.urllib2_intercept import install_opener, uninstall_opener |
|
15 |
import wsgi_intercept.zope_testbrowser |
|
12329.1.1
by John Arbash Meinel
Fix bug #701329, don't generate an OOPS for socket-level errors. |
16 |
from paste import httpserver |
10847.2.1
by Gary Poster
log out from bzr and openid after logging out from Launchpad |
17 |
from paste.httpexceptions import HTTPExceptionHandler |
12329.1.1
by John Arbash Meinel
Fix bug #701329, don't generate an OOPS for socket-level errors. |
18 |
import zope.event |
10847.2.1
by Gary Poster
log out from bzr and openid after logging out from Launchpad |
19 |
|
20 |
from canonical.config import config |
|
12329.1.1
by John Arbash Meinel
Fix bug #701329, don't generate an OOPS for socket-level errors. |
21 |
from canonical.launchpad.webapp.errorlog import ErrorReport, ErrorReportEvent |
10847.2.1
by Gary Poster
log out from bzr and openid after logging out from Launchpad |
22 |
from canonical.launchpad.webapp.vhosts import allvhosts |
11666.3.5
by Curtis Hovey
Import layers from canonical.testing.layers. |
23 |
from canonical.testing.layers import DatabaseFunctionalLayer |
12561.2.1
by John Arbash Meinel
Fix bug #732481. We have to pass on start_response even when there is no body. |
24 |
from launchpad_loggerhead.app import ( |
25 |
_oops_html_template, |
|
26 |
oops_middleware, |
|
27 |
RootApp, |
|
28 |
)
|
|
10847.2.1
by Gary Poster
log out from bzr and openid after logging out from Launchpad |
29 |
from launchpad_loggerhead.session import SessionHandler |
30 |
from lp.testing import TestCase |
|
31 |
||
32 |
SESSION_VAR = 'lh.session' |
|
33 |
||
34 |
# See sourcecode/launchpad-loggerhead/start-loggerhead.py for the production
|
|
35 |
# mechanism for getting the secret.
|
|
36 |
SECRET = 'secret' |
|
37 |
||
38 |
||
39 |
def session_scribbler(app, test): |
|
40 |
"""Squirrel away the session variable."""
|
|
41 |
def scribble(environ, start_response): |
|
42 |
test.session = environ[SESSION_VAR] # Yay for mutables. |
|
43 |
return app(environ, start_response) |
|
44 |
return scribble |
|
45 |
||
46 |
||
47 |
def dummy_destination(environ, start_response): |
|
48 |
"""Return a fake response."""
|
|
49 |
start_response('200 OK', [('Content-type','text/plain')]) |
|
50 |
return ['This is a dummy destination.\n'] |
|
51 |
||
52 |
||
53 |
class SimpleLogInRootApp(RootApp): |
|
54 |
"""A mock root app that doesn't require open id."""
|
|
55 |
def _complete_login(self, environ, start_response): |
|
56 |
environ[SESSION_VAR]['user'] = 'bob' |
|
57 |
start_response('200 OK', [('Content-type','text/plain')]) |
|
58 |
return ['\n'] |
|
59 |
||
60 |
||
61 |
class TestLogout(TestCase): |
|
62 |
layer = DatabaseFunctionalLayer |
|
63 |
||
64 |
def intercept(self, uri, app): |
|
65 |
"""Install wsgi interceptors for the uri, app tuple."""
|
|
66 |
if isinstance(uri, basestring): |
|
67 |
uri = lazr.uri.URI(uri) |
|
68 |
port = uri.port |
|
69 |
if port is None: |
|
70 |
if uri.scheme == 'http': |
|
71 |
port = 80 |
|
72 |
elif uri.scheme == 'https': |
|
73 |
port = 443 |
|
74 |
else: |
|
75 |
raise NotImplementedError(uri.scheme) |
|
76 |
else: |
|
77 |
port = int(port) |
|
78 |
wsgi_intercept.add_wsgi_intercept(uri.host, port, lambda: app) |
|
79 |
self.intercepted.append((uri.host, port)) |
|
80 |
||
81 |
def setUp(self): |
|
82 |
TestCase.setUp(self) |
|
83 |
self.intercepted = [] |
|
84 |
self.session = None |
|
85 |
self.root = app = SimpleLogInRootApp(SESSION_VAR) |
|
86 |
app = session_scribbler(app, self) |
|
87 |
app = HTTPExceptionHandler(app) |
|
88 |
app = SessionHandler(app, SESSION_VAR, SECRET) |
|
89 |
self.cookie_name = app.cookie_handler.cookie_name |
|
90 |
self.intercept(config.codehosting.codebrowse_root, app) |
|
91 |
self.intercept(config.codehosting.secure_codebrowse_root, app) |
|
92 |
self.intercept(allvhosts.configs['mainsite'].rooturl, |
|
93 |
dummy_destination) |
|
94 |
install_opener() |
|
95 |
self.browser = wsgi_intercept.zope_testbrowser.WSGI_Browser() |
|
96 |
# We want to pretend we are not a robot, or else mechanize will honor
|
|
97 |
# robots.txt.
|
|
98 |
self.browser.mech_browser.set_handle_robots(False) |
|
99 |
self.browser.open( |
|
100 |
config.codehosting.secure_codebrowse_root + '+login') |
|
101 |
||
102 |
def tearDown(self): |
|
103 |
uninstall_opener() |
|
104 |
for host, port in self.intercepted: |
|
105 |
wsgi_intercept.remove_wsgi_intercept(host, port) |
|
106 |
TestCase.tearDown(self) |
|
107 |
||
108 |
def testLoggerheadLogout(self): |
|
109 |
# We start logged in as 'bob'.
|
|
110 |
self.assertEqual(self.session['user'], 'bob') |
|
111 |
self.browser.open( |
|
112 |
config.codehosting.secure_codebrowse_root + 'favicon.ico') |
|
113 |
self.assertEqual(self.session['user'], 'bob') |
|
114 |
self.failUnless(self.browser.cookies.get(self.cookie_name)) |
|
115 |
||
116 |
# When we visit +logout, our session is gone.
|
|
117 |
self.browser.open( |
|
118 |
config.codehosting.secure_codebrowse_root + '+logout') |
|
119 |
self.assertEqual(self.session, {}) |
|
120 |
||
121 |
# By default, we have been redirected to the Launchpad root.
|
|
122 |
self.assertEqual( |
|
123 |
self.browser.url, allvhosts.configs['mainsite'].rooturl) |
|
124 |
||
125 |
# The session cookie still exists, because of how
|
|
126 |
# paste.auth.cookie works (see
|
|
127 |
# http://trac.pythonpaste.org/pythonpaste/ticket/139 ) but the user
|
|
128 |
# does in fact have an empty session now.
|
|
129 |
self.browser.open( |
|
130 |
config.codehosting.secure_codebrowse_root + 'favicon.ico') |
|
131 |
self.assertEqual(self.session, {}) |
|
132 |
||
133 |
def testLoggerheadLogoutRedirect(self): |
|
134 |
# When we visit +logout with a 'next_to' value in the query string,
|
|
135 |
# the logout page will redirect to the given URI. As of this
|
|
136 |
# writing, this is used by Launchpad to redirect to our OpenId
|
|
137 |
# provider (see canonical.launchpad.tests.test_login.
|
|
138 |
# TestLoginAndLogout.test_CookieLogoutPage).
|
|
139 |
||
140 |
# Here, we will have a more useless example of the basic machinery.
|
|
141 |
dummy_root = 'http://dummy.dev/' |
|
142 |
self.intercept(dummy_root, dummy_destination) |
|
143 |
self.browser.open( |
|
144 |
config.codehosting.secure_codebrowse_root + |
|
145 |
'+logout?' + |
|
146 |
urllib.urlencode(dict(next_to=dummy_root + '+logout'))) |
|
147 |
||
148 |
# We are logged out, as before.
|
|
149 |
self.assertEqual(self.session, {}) |
|
150 |
||
151 |
# Now, though, we are redirected to the ``next_to`` destination.
|
|
152 |
self.assertEqual(self.browser.url, dummy_root + '+logout') |
|
153 |
self.assertEqual(self.browser.contents, |
|
154 |
'This is a dummy destination.\n') |
|
155 |
||
156 |
||
12329.1.1
by John Arbash Meinel
Fix bug #701329, don't generate an OOPS for socket-level errors. |
157 |
class TestOopsMiddleware(TestCase): |
158 |
||
12561.2.1
by John Arbash Meinel
Fix bug #732481. We have to pass on start_response even when there is no body. |
159 |
def setUp(self): |
160 |
super(TestOopsMiddleware, self).setUp() |
|
161 |
self.start_response_called = False |
|
162 |
||
12329.1.3
by John Arbash Meinel
Log the failures as 'INFO' and test that they get logged. |
163 |
def assertContainsRe(self, haystack, needle_re, flags=0): |
164 |
"""Assert that a contains something matching a regular expression."""
|
|
165 |
# There is: self.assertTextMatchesExpressionIgnoreWhitespace
|
|
166 |
# but it does weird things with whitespace, and gives
|
|
167 |
# unhelpful error messages when it fails, so this is copied
|
|
168 |
# from bzrlib
|
|
169 |
if not re.search(needle_re, haystack, flags): |
|
170 |
if '\n' in haystack or len(haystack) > 60: |
|
171 |
# a long string, format it in a more readable way
|
|
172 |
raise AssertionError( |
|
173 |
'pattern "%s" not found in\n"""\\\n%s"""\n' |
|
174 |
% (needle_re, haystack)) |
|
175 |
else: |
|
176 |
raise AssertionError('pattern "%s" not found in "%s"' |
|
177 |
% (needle_re, haystack)) |
|
178 |
||
179 |
def catchLogEvents(self): |
|
180 |
"""Any log events that are triggered get written to self.log_stream"""
|
|
181 |
logger = logging.getLogger('lp-loggerhead') |
|
182 |
logger.setLevel(logging.DEBUG) |
|
183 |
self.log_stream = cStringIO.StringIO() |
|
184 |
handler = logging.StreamHandler(self.log_stream) |
|
185 |
handler.setLevel(logging.DEBUG) |
|
186 |
logger.addHandler(handler) |
|
187 |
self.addCleanup(logger.removeHandler, handler) |
|
188 |
||
12329.1.1
by John Arbash Meinel
Fix bug #701329, don't generate an OOPS for socket-level errors. |
189 |
def runtime_failing_app(self, environ, start_response): |
190 |
if False: |
|
191 |
yield None |
|
192 |
raise RuntimeError('just a generic runtime error.') |
|
193 |
||
194 |
def socket_failing_app(self, environ, start_response): |
|
195 |
if False: |
|
196 |
yield None |
|
197 |
raise socket.error(errno.EPIPE, 'Connection closed') |
|
198 |
||
12561.2.1
by John Arbash Meinel
Fix bug #732481. We have to pass on start_response even when there is no body. |
199 |
def logging_start_response(self, status, response_headers, exc_info=None): |
200 |
self._response_chunks = [] |
|
201 |
def _write(chunk): |
|
202 |
self._response_chunks.append(chunk) |
|
203 |
self.start_response_called = True |
|
204 |
return _write |
|
12329.1.1
by John Arbash Meinel
Fix bug #701329, don't generate an OOPS for socket-level errors. |
205 |
|
12329.1.2
by John Arbash Meinel
Extend the test a little bit. |
206 |
def success_app(self, environ, start_response): |
207 |
writer = start_response('200 OK', {}) |
|
208 |
writer('Successfull\n') |
|
209 |
return [] |
|
210 |
||
211 |
def failing_start_response(self, status, response_headers, exc_info=None): |
|
212 |
def fail_write(chunk): |
|
213 |
raise socket.error(errno.EPIPE, 'Connection closed') |
|
12561.2.1
by John Arbash Meinel
Fix bug #732481. We have to pass on start_response even when there is no body. |
214 |
self.start_response_called = True |
12329.1.2
by John Arbash Meinel
Extend the test a little bit. |
215 |
return fail_write |
216 |
||
12540.1.1
by John Arbash Meinel
Fix bug #726985. GeneratorExit should also not be considered an OOPS-worthy failure. |
217 |
def multi_yielding_app(self, environ, start_response): |
218 |
writer = start_response('200 OK', {}) |
|
219 |
yield 'content\n' |
|
220 |
yield 'I want\n' |
|
221 |
yield 'to give to the user\n' |
|
222 |
||
12561.2.1
by John Arbash Meinel
Fix bug #732481. We have to pass on start_response even when there is no body. |
223 |
def no_body_app(self, environ, start_response): |
224 |
writer = start_response('200 OK', {}) |
|
225 |
return [] |
|
226 |
||
12540.1.1
by John Arbash Meinel
Fix bug #726985. GeneratorExit should also not be considered an OOPS-worthy failure. |
227 |
def _get_default_environ(self): |
228 |
return {'wsgi.version': (1, 0), |
|
229 |
'wsgi.url_scheme': 'http', |
|
230 |
'PATH_INFO': '/test/path', |
|
231 |
'REQUEST_METHOD': 'GET', |
|
232 |
'SERVER_NAME': 'localhost', |
|
233 |
'SERVER_PORT': '8080', |
|
234 |
}
|
|
235 |
||
12329.1.2
by John Arbash Meinel
Extend the test a little bit. |
236 |
def wrap_and_run(self, app, failing_write=False): |
12329.1.1
by John Arbash Meinel
Fix bug #701329, don't generate an OOPS for socket-level errors. |
237 |
app = oops_middleware(app) |
238 |
# Just random env data, rather than setting up a whole wsgi stack just
|
|
239 |
# to pass in values for this dict
|
|
12540.1.1
by John Arbash Meinel
Fix bug #726985. GeneratorExit should also not be considered an OOPS-worthy failure. |
240 |
environ = self._get_default_environ() |
12329.1.2
by John Arbash Meinel
Extend the test a little bit. |
241 |
if failing_write: |
242 |
result = list(app(environ, self.failing_start_response)) |
|
243 |
else: |
|
12561.2.1
by John Arbash Meinel
Fix bug #732481. We have to pass on start_response even when there is no body. |
244 |
result = list(app(environ, self.logging_start_response)) |
12329.1.1
by John Arbash Meinel
Fix bug #701329, don't generate an OOPS for socket-level errors. |
245 |
return result |
246 |
||
247 |
def test_exception_triggers_oops(self): |
|
248 |
res = self.wrap_and_run(self.runtime_failing_app) |
|
249 |
# After the exception was raised, we should also have gotten an oops
|
|
250 |
# event
|
|
12329.1.4
by John Arbash Meinel
Use TestCase.oopses rather than a custom event collector. (Feedback from wgrant.) |
251 |
self.assertEqual(1, len(self.oopses)) |
252 |
oops = self.oopses[0] |
|
253 |
self.assertEqual('RuntimeError', oops.type) |
|
12561.2.1
by John Arbash Meinel
Fix bug #732481. We have to pass on start_response even when there is no body. |
254 |
# runtime_failing_app doesn't call start_response, but oops_middleware
|
255 |
# does because it tries to send the OOPS information to the user.
|
|
256 |
self.assertTrue(self.start_response_called) |
|
257 |
self.assertEqual(_oops_html_template % {'oopsid': oops.id}, |
|
258 |
''.join(self._response_chunks)) |
|
12329.1.1
by John Arbash Meinel
Fix bug #701329, don't generate an OOPS for socket-level errors. |
259 |
|
260 |
def test_ignores_socket_exceptions(self): |
|
12329.1.3
by John Arbash Meinel
Log the failures as 'INFO' and test that they get logged. |
261 |
self.catchLogEvents() |
12329.1.1
by John Arbash Meinel
Fix bug #701329, don't generate an OOPS for socket-level errors. |
262 |
res = self.wrap_and_run(self.socket_failing_app) |
12329.1.4
by John Arbash Meinel
Use TestCase.oopses rather than a custom event collector. (Feedback from wgrant.) |
263 |
self.assertEqual(0, len(self.oopses)) |
12329.1.3
by John Arbash Meinel
Log the failures as 'INFO' and test that they get logged. |
264 |
self.assertContainsRe(self.log_stream.getvalue(), |
265 |
'Caught socket exception from <unknown>:.*Connection closed') |
|
12561.2.1
by John Arbash Meinel
Fix bug #732481. We have to pass on start_response even when there is no body. |
266 |
# start_response doesn't get called because the app fails first,
|
267 |
# and oops_middleware knows not to do anything with a closed socket.
|
|
268 |
self.assertFalse(self.start_response_called) |
|
12329.1.1
by John Arbash Meinel
Fix bug #701329, don't generate an OOPS for socket-level errors. |
269 |
|
12329.1.2
by John Arbash Meinel
Extend the test a little bit. |
270 |
def test_ignores_writer_failures(self): |
12329.1.3
by John Arbash Meinel
Log the failures as 'INFO' and test that they get logged. |
271 |
self.catchLogEvents() |
12329.1.2
by John Arbash Meinel
Extend the test a little bit. |
272 |
res = self.wrap_and_run(self.success_app, failing_write=True) |
12329.1.4
by John Arbash Meinel
Use TestCase.oopses rather than a custom event collector. (Feedback from wgrant.) |
273 |
self.assertEqual(0, len(self.oopses)) |
12329.1.3
by John Arbash Meinel
Log the failures as 'INFO' and test that they get logged. |
274 |
self.assertContainsRe(self.log_stream.getvalue(), |
275 |
'Caught socket exception from <unknown>:.*Connection closed') |
|
12561.2.1
by John Arbash Meinel
Fix bug #732481. We have to pass on start_response even when there is no body. |
276 |
# success_app calls start_response, so this should get passed on.
|
277 |
self.assertTrue(self.start_response_called) |
|
12329.1.2
by John Arbash Meinel
Extend the test a little bit. |
278 |
|
12540.1.1
by John Arbash Meinel
Fix bug #726985. GeneratorExit should also not be considered an OOPS-worthy failure. |
279 |
def test_stopping_early_no_oops(self): |
280 |
# See bug #726985.
|
|
281 |
# If content is being streamed, and the pipe closes, we'll get a
|
|
282 |
# 'GeneratorExit', because it is closed before finishing. This doesn't
|
|
283 |
# need to become an OOPS.
|
|
284 |
self.catchLogEvents() |
|
285 |
app = oops_middleware(self.multi_yielding_app) |
|
286 |
environ = self._get_default_environ() |
|
12561.2.1
by John Arbash Meinel
Fix bug #732481. We have to pass on start_response even when there is no body. |
287 |
result = app(environ, self.logging_start_response) |
12540.1.1
by John Arbash Meinel
Fix bug #726985. GeneratorExit should also not be considered an OOPS-worthy failure. |
288 |
self.assertEqual('content\n', result.next()) |
289 |
# At this point, we intentionally kill the app and the response, so
|
|
290 |
# that they will get GeneratorExit
|
|
291 |
del app, result |
|
292 |
self.assertEqual([], self.oopses) |
|
293 |
self.assertContainsRe(self.log_stream.getvalue(), |
|
294 |
'Caught GeneratorExit from <unknown>') |
|
12561.2.1
by John Arbash Meinel
Fix bug #732481. We have to pass on start_response even when there is no body. |
295 |
# Body content was yielded, we must have called start_response
|
296 |
self.assertTrue(self.start_response_called) |
|
297 |
||
298 |
def test_no_body_calls_start_response(self): |
|
299 |
# See bug #732481, even if we don't have a body, if we have headers to
|
|
300 |
# send, we must call start_response
|
|
301 |
result = self.wrap_and_run(self.no_body_app) |
|
302 |
self.assertEqual([], result) |
|
303 |
self.assertTrue(self.start_response_called) |
|
304 |
# Output content is empty because of no_body_app
|
|
305 |
self.assertEqual('', ''.join(self._response_chunks)) |
|
12540.1.1
by John Arbash Meinel
Fix bug #726985. GeneratorExit should also not be considered an OOPS-worthy failure. |
306 |
|
12329.1.1
by John Arbash Meinel
Fix bug #701329, don't generate an OOPS for socket-level errors. |
307 |
|
10847.2.1
by Gary Poster
log out from bzr and openid after logging out from Launchpad |
308 |
def test_suite(): |
12707.1.1
by Robert Collins
Backout running of loggerhead tests during LP test runs; inappropriate coupling |
309 |
return unittest.TestLoader().loadTestsFromName(__name__) |