~launchpad-pqm/launchpad/devel

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__)