~jose/charms/precise/juju-gui/add-blank-defaults

« back to all changes in this revision

Viewing changes to server/guiserver/tests/test_handlers.py

  • Committer: Nicola Larosa
  • Date: 2013-07-31 15:45:55 UTC
  • mto: (60.27.1 integrate-builtin-server-2)
  • mto: This revision was merged to the branch mainline in revision 75.
  • Revision ID: nicola.larosa@canonical.com-20130731154555-aekmw8ur2dxutawi
Fix and add tests.

Show diffs side-by-side

added added

removed removed

Lines of Context:
16
16
 
17
17
"""Tests for the Juju GUI server handlers."""
18
18
 
19
 
import datetime
20
19
import json
21
20
import os
22
21
import shutil
23
22
import tempfile
24
23
 
25
 
from concurrent import futures
26
24
import mock
27
25
from tornado import (
28
26
    concurrent,
29
 
    escape,
30
 
    gen,
31
 
    httpclient,
32
27
    web,
33
28
)
34
29
from tornado.testing import (
42
37
from guiserver import (
43
38
    auth,
44
39
    clients,
45
 
    get_version,
46
40
    handlers,
47
41
    manage,
48
42
)
49
 
from guiserver.bundles import base
50
43
from guiserver.tests import helpers
51
44
 
52
45
 
53
46
class WebSocketHandlerTestMixin(object):
54
47
    """Base set up for all the WebSocketHandler test cases."""
55
48
 
56
 
    auth_backend = auth.get_backend(manage.DEFAULT_API_VERSION)
57
49
    hello_message = json.dumps({'hello': 'world'})
58
50
 
59
51
    def get_app(self):
65
57
        #   ws-client -> ws-server -> ws-forwarding-client -> ws-echo-server
66
58
        # Messages arriving to the echo server are returned back to the client:
67
59
        #   ws-echo-server -> ws-forwarding-client -> ws-server -> ws-client
68
 
        self.apiurl = self.get_wss_url('/echo')
69
 
        self.api_close_future = concurrent.Future()
70
 
        self.deployer = base.Deployer(
71
 
            self.apiurl, manage.DEFAULT_API_VERSION, io_loop=self.io_loop)
72
 
        self.tokens = auth.AuthenticationTokenHandler(io_loop=self.io_loop)
 
60
        self.echo_server_address = self.get_wss_url('/echo')
 
61
        self.echo_server_closed_future = concurrent.Future()
73
62
        echo_options = {
74
 
            'close_future': self.api_close_future,
 
63
            'close_future': self.echo_server_closed_future,
75
64
            'io_loop': self.io_loop,
76
65
        }
77
66
        ws_options = {
78
 
            'apiurl': self.apiurl,
79
 
            'auth_backend': self.auth_backend,
80
 
            'deployer': self.deployer,
 
67
            'apiurl': self.echo_server_address,
81
68
            'io_loop': self.io_loop,
82
 
            'tokens': self.tokens,
83
69
        }
84
70
        return web.Application([
85
71
            (r'/echo', helpers.EchoWebSocketHandler, echo_options),
86
72
            (r'/ws', handlers.WebSocketHandler, ws_options),
87
 
        ])
 
73
        ], auth_backend=auth.get_backend(manage.DEFAULT_API_VERSION))
88
74
 
89
75
    def make_client(self):
90
76
        """Return a WebSocket client ready to be connected to the server."""
104
90
            handler.ws_connection = mock.Mock()
105
91
        return handler
106
92
 
107
 
    @gen.coroutine
108
 
    def make_initialized_handler(
109
 
            self, apiurl=None, headers=None, mock_protocol=False):
110
 
        """Create and return an initialized WebSocketHandler instance."""
111
 
        if apiurl is None:
112
 
            apiurl = self.apiurl
113
 
        handler = self.make_handler(
114
 
            headers=headers, mock_protocol=mock_protocol)
115
 
        yield handler.initialize(
116
 
            apiurl, self.auth_backend, self.deployer, self.tokens,
117
 
            self.io_loop)
118
 
        raise gen.Return(handler)
119
 
 
120
93
 
121
94
class TestWebSocketHandlerConnection(
122
95
        WebSocketHandlerTestMixin, helpers.WSSTestMixin, LogTrapTestCase,
134
107
    def test_initialization(self):
135
108
        # A WebSocket client is created and connected when the handler is
136
109
        # initialized.
137
 
        handler = yield self.make_initialized_handler()
 
110
        handler = self.make_handler()
 
111
        yield handler.initialize(self.echo_server_address, self.io_loop)
138
112
        self.assertTrue(handler.connected)
139
113
        self.assertTrue(handler.juju_connected)
140
114
        self.assertIsInstance(
146
120
    def test_juju_connection_failure(self):
147
121
        # If the connection to the Juju API server does not succeed, an
148
122
        # error is reported and the client is disconnected.
 
123
        handler = self.make_handler()
149
124
        expected_log = '.*unable to connect to the Juju API'
150
125
        with ExpectLog('', expected_log, required=True):
151
 
            handler = yield self.make_initialized_handler(
152
 
                apiurl='wss://127.0.0.1/no-such')
 
126
            yield handler.initialize(
 
127
                'wss://127.0.0.1/does-not-exist', self.io_loop)
153
128
        self.assertFalse(handler.connected)
154
129
        self.assertFalse(handler.juju_connected)
155
130
 
156
131
    @gen_test
157
132
    def test_juju_connection_propagated_request_headers(self):
158
133
        # The Origin header is propagated to the client connection.
159
 
        expected = {'Origin': 'https://example.com'}
160
 
        handler = yield self.make_initialized_handler(headers=expected)
 
134
        handler = self.make_handler(headers={'Origin': 'https://example.com'})
 
135
        yield handler.initialize(self.echo_server_address, self.io_loop)
161
136
        headers = handler.juju_connection.request.headers
162
137
        self.assertIn('Origin', headers)
163
 
        self.assertEqual(expected['Origin'], headers['Origin'])
 
138
        self.assertEqual('https://example.com', headers['Origin'])
164
139
 
165
140
    @gen_test
166
141
    def test_juju_connection_default_request_headers(self):
167
142
        # The default Origin header is included in the client connection
168
143
        # handshake if not found in the original request.
169
 
        handler = yield self.make_initialized_handler()
 
144
        handler = self.make_handler()
 
145
        yield handler.initialize(self.echo_server_address, self.io_loop)
170
146
        headers = handler.juju_connection.request.headers
171
147
        self.assertIn('Origin', headers)
172
148
        self.assertEqual(self.get_url('/echo'), headers['Origin'])
173
149
 
174
 
    @gen_test
175
150
    def test_client_callback(self):
176
151
        # The WebSocket client is created passing the proper callback.
 
152
        handler = self.make_handler()
177
153
        with self.mock_websocket_connect() as mock_websocket_connect:
178
 
            handler = yield self.make_initialized_handler()
 
154
            handler.initialize(self.echo_server_address, self.io_loop)
179
155
        self.assertEqual(1, mock_websocket_connect.call_count)
180
156
        self.assertIn(
181
157
            handler.on_juju_message, mock_websocket_connect.call_args[0])
185
161
        # The proxy connection is terminated when the client disconnects.
186
162
        client = yield self.make_client()
187
163
        yield client.close()
188
 
        yield self.api_close_future
 
164
        yield self.echo_server_closed_future
189
165
 
190
166
    @gen_test
191
167
    def test_connection_closed_by_server(self):
195
171
        expected_log = '.*Juju API unexpectedly disconnected'
196
172
        with ExpectLog('', expected_log, required=True):
197
173
            # Fire the Future in order to force an echo server disconnection.
198
 
            self.api_close_future.set_result(None)
 
174
            self.echo_server_closed_future.set_result(None)
199
175
            message = yield client.read_message()
200
176
        self.assertIsNone(message)
201
177
 
202
 
    def test_select_subprotocol(self):
203
 
        # The first sub-protocol is returned by the handler method.
204
 
        handler = self.make_handler()
205
 
        subprotocol = handler.select_subprotocol(['foo', 'bar'])
206
 
        self.assertEqual('foo', subprotocol)
207
 
 
208
178
 
209
179
class TestWebSocketHandlerProxy(
210
180
        WebSocketHandlerTestMixin, helpers.WSSTestMixin, LogTrapTestCase,
213
183
    @mock.patch('guiserver.clients.WebSocketClientConnection')
214
184
    def test_from_browser_to_juju(self, mock_juju_connection):
215
185
        # A message from the browser is forwarded to the remote server.
216
 
        handler = yield self.make_initialized_handler()
 
186
        handler = self.make_handler()
 
187
        yield handler.initialize(self.echo_server_address, self.io_loop)
217
188
        handler.on_message(self.hello_message)
218
189
        mock_juju_connection.write_message.assert_called_once_with(
219
190
            self.hello_message)
220
191
 
221
 
    @gen_test
222
192
    def test_from_juju_to_browser(self):
223
193
        # A message from the remote server is returned to the browser.
224
 
        handler = yield self.make_initialized_handler()
 
194
        handler = self.make_handler()
 
195
        handler.initialize(self.echo_server_address, self.io_loop)
225
196
        with mock.patch('guiserver.handlers.WebSocketHandler.write_message'):
226
197
            handler.on_juju_message(self.hello_message)
227
198
            handler.write_message.assert_called_once_with(self.hello_message)
234
205
        mock_path = 'guiserver.clients.WebSocketClientConnection.write_message'
235
206
        with mock.patch(mock_path) as mock_write_message:
236
207
            initialization = handler.initialize(
237
 
                self.apiurl, self.auth_backend, self.deployer, self.tokens,
238
 
                io_loop=self.io_loop)
 
208
                self.echo_server_address, self.io_loop)
239
209
            handler.on_message(self.hello_message)
240
210
            self.assertFalse(mock_write_message.called)
241
211
            yield initialization
251
221
        self.assertEqual(self.hello_message, message)
252
222
 
253
223
    @gen_test
254
 
    def test_end_to_end_proxy_non_ascii(self):
255
 
        # Non-ascii messages are correctly forwarded from the client to the
256
 
        # echo server and back to the client.
257
 
        snowman = u'{"Here is a snowman\u00a1": "\u2603"}'
258
 
        client = yield self.make_client()
259
 
        client.write_message(snowman)
260
 
        message = yield client.read_message()
261
 
        self.assertEqual(snowman, message)
262
 
 
263
 
    @gen_test
264
224
    def test_invalid_json(self):
265
225
        # A warning is logged if the message is not valid JSON.
266
226
        client = yield self.make_client()
267
 
        expected_log = "JSON decoder: message is not valid JSON: u'not-json'"
 
227
        expected_log = 'JSON decoder: message is not valid JSON: not-json'
268
228
        with ExpectLog('', expected_log, required=True):
269
229
            client.write_message('not-json')
270
230
            yield client.read_message()
273
233
    def test_not_a_dict(self):
274
234
        # A warning is logged if the decoded message is not a dict.
275
235
        client = yield self.make_client()
276
 
        expected_log = 'JSON decoder: message is not a dict: u\'"not-a-dict"\''
 
236
        expected_log = 'JSON decoder: message is not a dict: "not-a-dict"'
277
237
        with ExpectLog('', expected_log, required=True):
278
238
            client.write_message('"not-a-dict"')
279
239
            yield client.read_message()
286
246
    def setUp(self):
287
247
        super(TestWebSocketHandlerAuthentication, self).setUp()
288
248
        self.handler = self.make_handler(mock_protocol=True)
289
 
        self.handler.initialize(
290
 
            self.apiurl, self.auth_backend, self.deployer, self.tokens,
291
 
            io_loop=self.io_loop)
 
249
        self.handler.initialize(self.echo_server_address, self.io_loop)
292
250
 
293
251
    def send_login_request(self):
294
252
        """Create a login request and send it to the handler."""
333
291
        self.assertFalse(self.handler.user.is_authenticated)
334
292
        self.assertFalse(self.handler.auth.in_progress())
335
293
 
336
 
    @mock.patch('uuid.uuid4', mock.Mock(return_value=mock.Mock(hex='DEFACED')))
337
 
    @mock.patch('datetime.datetime',
338
 
                mock.Mock(
339
 
                    **{'utcnow.return_value':
340
 
                       datetime.datetime(2013, 11, 21, 21)}))
341
 
    def test_token_request(self):
342
 
        # It supports requesting a token when authenticated.
343
 
        self.handler.user.username = 'user'
344
 
        self.handler.user.password = 'passwd'
345
 
        self.handler.user.is_authenticated = True
346
 
        request = json.dumps(
347
 
            dict(RequestId=42, Type='GUIToken', Request='Create'))
348
 
        self.handler.on_message(request)
349
 
        message = self.handler.ws_connection.write_message.call_args[0][0]
350
 
        self.assertEqual(
351
 
            dict(
352
 
                RequestId=42,
353
 
                Response=dict(
354
 
                    Token='DEFACED',
355
 
                    Created='2013-11-21T21:00:00Z',
356
 
                    Expires='2013-11-21T21:02:00Z'
357
 
                )
358
 
            ),
359
 
            json.loads(message))
360
 
        self.assertFalse(self.handler.juju_connected)
361
 
        self.assertEqual(0, len(self.handler._juju_message_queue))
362
 
 
363
 
    def test_unauthenticated_token_request(self):
364
 
        # When not authenticated, the request is passed on to Juju for error.
365
 
        self.assertFalse(self.handler.user.is_authenticated)
366
 
        request = json.dumps(
367
 
            dict(RequestId=42, Type='GUIToken', Request='Create'))
368
 
        self.handler.on_message(request)
369
 
        message = self.handler.ws_connection.write_message.call_args[0][0]
370
 
        self.assertEqual(
371
 
            dict(
372
 
                RequestId=42,
373
 
                Error='tokens can only be created by authenticated users.',
374
 
                ErrorCode='unauthorized access',
375
 
                Response={},
376
 
            ),
377
 
            json.loads(message))
378
 
        self.assertFalse(self.handler.juju_connected)
379
 
        self.assertEqual(0, len(self.handler._juju_message_queue))
380
 
 
381
 
    def test_token_authentication_success(self):
382
 
        # It supports authenticating with a token.
383
 
        request = self.make_token_login_request(
384
 
            self.tokens, username='user', password='passwd')
385
 
        with mock.patch.object(self.io_loop,
386
 
                               'remove_timeout') as mock_remove_timeout:
387
 
            self.handler.on_message(json.dumps(request))
388
 
            mock_remove_timeout.assert_called_once_with('handle')
389
 
        self.assertEqual(
390
 
            self.make_login_request(
391
 
                request_id=42, username='user', password='passwd'),
392
 
            json.loads(self.handler._juju_message_queue[0]))
393
 
        self.assertTrue(self.handler.auth.in_progress())
394
 
        self.send_login_response(True)
395
 
        self.assertEqual(
396
 
            dict(RequestId=42,
397
 
                 Response={'AuthTag': 'user', 'Password': 'passwd'}),
398
 
            json.loads(
399
 
                self.handler.ws_connection.write_message.call_args[0][0]))
400
 
 
401
 
    def test_token_authentication_failure(self):
402
 
        # It correctly handles a token that will not authenticate.
403
 
        request = self.make_token_login_request(
404
 
            self.tokens, username='user', password='passwd')
405
 
        with mock.patch.object(self.io_loop,
406
 
                               'remove_timeout') as mock_remove_timeout:
407
 
            self.handler.on_message(json.dumps(request))
408
 
            mock_remove_timeout.assert_called_once_with('handle')
409
 
        self.send_login_response(False)
410
 
        message = self.handler.ws_connection.write_message.call_args[0][0]
411
 
        self.assertEqual(
412
 
            'invalid entity name or password',
413
 
            json.loads(message)['Error'])
414
 
 
415
 
    def test_unknown_authentication_token(self):
416
 
        # It correctly handles an unknown token.
417
 
        request = self.make_token_login_request()
418
 
        self.handler.on_message(json.dumps(request))
419
 
        message = self.handler.ws_connection.write_message.call_args[0][0]
420
 
        self.assertEqual(
421
 
            'unknown, fulfilled, or expired token',
422
 
            json.loads(message)['Error'])
423
 
        self.assertFalse(self.handler.juju_connected)
424
 
        self.assertEqual(0, len(self.handler._juju_message_queue))
425
 
 
426
 
 
427
 
class TestWebSocketHandlerBundles(
428
 
        WebSocketHandlerTestMixin, helpers.WSSTestMixin,
429
 
        helpers.BundlesTestMixin, LogTrapTestCase, AsyncHTTPSTestCase):
430
 
 
431
 
    @gen_test
432
 
    def test_bundle_import_process(self):
433
 
        # The bundle import process is correctly started and completed.
434
 
        write_message_path = 'guiserver.handlers.wrap_write_message'
435
 
        with mock.patch(write_message_path) as mock_write_message:
436
 
            handler = yield self.make_initialized_handler()
437
 
        # Simulate the user is authenticated.
438
 
        handler.user.is_authenticated = True
439
 
        # Start a bundle import.
440
 
        request = self.make_deployment_request('Import', encoded=True)
441
 
        with self.patch_validate(), self.patch_import_bundle():
442
 
            yield handler.on_message(request)
443
 
        expected = self.make_deployment_response(response={'DeploymentId': 0})
444
 
        mock_write_message().assert_called_once_with(expected)
445
 
        # Start observing the deployment progress.
446
 
        request = self.make_deployment_request('Watch', encoded=True)
447
 
        yield handler.on_message(request)
448
 
        expected = self.make_deployment_response(response={'WatcherId': 0})
449
 
        mock_write_message().assert_called_with(expected)
450
 
        # Get the two next changes: in the first one the deployment has been
451
 
        # started, in the second one it is completed. This way the test runner
452
 
        # can safely stop the IO loop (no remaining Future callbacks).
453
 
        request = self.make_deployment_request('Next', encoded=True)
454
 
        yield handler.on_message(request)
455
 
        yield handler.on_message(request)
456
 
 
457
 
    @gen_test
458
 
    def test_not_authenticated(self):
459
 
        # The bundle deployment support is only activated for logged in users.
460
 
        client = yield self.make_client()
461
 
        request = self.make_deployment_request('Import', encoded=True)
462
 
        client.write_message(request)
463
 
        expected = self.make_deployment_response(
464
 
            error='unauthorized access: no user logged in')
465
 
        response = yield client.read_message()
466
 
        self.assertEqual(expected, json.loads(response))
467
 
 
468
 
 
469
 
class TestIndexHandler(LogTrapTestCase, AsyncHTTPTestCase):
 
294
 
 
295
class TestIndexHandler(AsyncHTTPTestCase, LogTrapTestCase):
470
296
 
471
297
    def setUp(self):
472
298
        # Set up a static path with an index.html in it.
502
328
        self.ensure_index('/:flag:/activated/?my=query')
503
329
 
504
330
 
505
 
class TestProxyHandler(LogTrapTestCase, AsyncHTTPTestCase):
506
 
 
507
 
    target_url = 'https://api.example.com:17070'
508
 
    request_headers = {
509
 
        'Accept-Encoding': 'gzip',
510
 
        'Authorization': 'Basic auth',
511
 
    }
512
 
    response_headers = {
513
 
        'Cache-Control': 'no-cache',
514
 
        'Content-Type': 'text/html',
515
 
        'Date': 'Tue, 15 Nov 1994 08:12:31 GMT',
516
 
        'Location': 'http://example.com/location',
517
 
        'Server': 'Apache/2.4.1 (Unix)',
518
 
        'WWW-Authenticate': 'Basic',
519
 
    }
520
 
 
521
 
    def get_app(self):
522
 
        # Set up an application exposing the proxy handler.
523
 
        options = {'target_url': self.target_url}
524
 
        return web.Application([
525
 
            (r'^/base/(.*)', handlers.ProxyHandler, options)])
526
 
 
527
 
    def assert_include_headers(self, expected, headers):
528
 
        """Ensure the expected headers are included in the given ones."""
529
 
        for key, value in expected.items():
530
 
            self.assertIn(key, headers)
531
 
            self.assertEqual(value, headers[key])
532
 
 
533
 
    def patch_http_client(self, response):
534
 
        """Patch the asynchronous HTTP client used to fetch remote resources.
535
 
 
536
 
        The patched client returns a future whose result is the given response
537
 
        object. If the response is an HTTPError exception, the future will
538
 
        raise the given exception.
539
 
        """
540
 
        future = futures.Future()
541
 
        if isinstance(response, httpclient.HTTPError):
542
 
            future.set_exception(response)
543
 
        else:
544
 
            future.set_result(response)
545
 
        mock_client = mock.Mock()
546
 
        mock_client().fetch.return_value = future
547
 
        mock_client.reset_mock()
548
 
        return mock.patch('tornado.httpclient.AsyncHTTPClient', mock_client)
549
 
 
550
 
    def test_get_request(self):
551
 
        # GET requests are properly sent to the target URL. Responses are
552
 
        # propagated back to the client.
553
 
        remote_response = helpers.make_response(
554
 
            200, body='ok', headers=self.response_headers)
555
 
        with self.patch_http_client(remote_response) as mock_client:
556
 
            response = self.fetch(
557
 
                '/base/remote-path/', headers=self.request_headers)
558
 
        # The remote response is propagated to the original client.
559
 
        self.assertEqual(200, response.code)
560
 
        self.assertEqual('ok', response.body)
561
 
        self.assert_include_headers(self.response_headers, response.headers)
562
 
        # An asynchronous HTTP client has been correctly created.
563
 
        mock_client.assert_called_once_with()
564
 
        # The client's fetch method has been used to fetch the remote resource.
565
 
        mock_fetch = mock_client().fetch
566
 
        self.assertEqual(1, mock_fetch.call_count)
567
 
        # The request to the target URL is a clone of the original request.
568
 
        remote_request = mock_fetch.call_args[0][0]
569
 
        self.assertEqual('GET', remote_request.method)
570
 
        self.assertEqual(self.target_url + '/remote-path/', remote_request.url)
571
 
        self.assert_include_headers(
572
 
            self.request_headers, remote_request.headers)
573
 
        # Certificates are automatically accepted.
574
 
        self.assertFalse(remote_request.validate_cert)
575
 
 
576
 
    def test_post_request(self):
577
 
        # POST requests are properly sent to the target URL.
578
 
        remote_response = helpers.make_response(
579
 
            200, body='ok', headers=self.response_headers)
580
 
        with self.patch_http_client(remote_response) as mock_client:
581
 
            response = self.fetch(
582
 
                '/base/remote-path/', method='POST',
583
 
                headers=self.request_headers, body='original body')
584
 
        self.assertEqual(200, response.code)
585
 
        self.assertEqual('ok', response.body)
586
 
        self.assert_include_headers(self.response_headers, response.headers)
587
 
        # The client's fetch method has been used to fetch the remote resource.
588
 
        mock_fetch = mock_client().fetch
589
 
        self.assertEqual(1, mock_fetch.call_count)
590
 
        # The request to the target URL is a clone of the original request.
591
 
        remote_request = mock_fetch.call_args[0][0]
592
 
        self.assertEqual('POST', remote_request.method)
593
 
        self.assert_include_headers(
594
 
            self.request_headers, remote_request.headers)
595
 
        # Also the body is propagated.
596
 
        self.assertEqual('original body', remote_request.body)
597
 
 
598
 
    def test_remote_path(self):
599
 
        # The corresponding path on the remote server is properly generated.
600
 
        remote_response = helpers.make_response(200)
601
 
        with self.patch_http_client(remote_response) as mock_client:
602
 
            self.fetch('/base/path1/path2?arg1=valu1&arg2=value2')
603
 
        mock_fetch = mock_client().fetch
604
 
        remote_request = mock_fetch.call_args[0][0]
605
 
        # The remote path reflects the requested one: the /base/ namespace is
606
 
        # correctly removed.
607
 
        self.assertEqual(
608
 
            self.target_url + '/path1/path2?arg1=valu1&arg2=value2',
609
 
            remote_request.url)
610
 
 
611
 
    def test_error_response(self):
612
 
        # Error responses are returned to the original client.
613
 
        remote_response = helpers.make_response(400, body='try harder')
614
 
        with self.patch_http_client(remote_response):
615
 
            response = self.fetch('/base/remote-path/')
616
 
        self.assertEqual(400, response.code)
617
 
        self.assertEqual('try harder', response.body)
618
 
        self.assertEqual('Bad Request', response.reason)
619
 
 
620
 
    def test_internal_server_error(self):
621
 
        # A 500 error is returned if an HTTP error occurs during the remote
622
 
        # request/response process.
623
 
        error = httpclient.HTTPError(500, message='bad wolf')
624
 
        with self.patch_http_client(error):
625
 
            response = self.fetch('/base/remote-path/')
626
 
        self.assertEqual(500, response.code)
627
 
        self.assertEqual(
628
 
            'Internal server error:\n'
629
 
            'error fetching data from '
630
 
            'https://api.example.com:17070/remote-path/: '
631
 
            'HTTP 500: bad wolf', response.body)
632
 
        self.assertEqual('Internal Server Error', response.reason)
633
 
 
634
 
 
635
 
class TestInfoHandler(LogTrapTestCase, AsyncHTTPTestCase):
636
 
 
637
 
    def get_app(self):
638
 
        mock_deployer = mock.Mock()
639
 
        mock_deployer.status.return_value = 'deployments status'
640
 
        options = {
641
 
            'apiurl': 'wss://api.example.com:17070',
642
 
            'apiversion': 'clojure',
643
 
            'deployer': mock_deployer,
644
 
            'sandbox': False,
645
 
            'start_time': 10,
646
 
        }
647
 
        return web.Application([(r'^/info', handlers.InfoHandler, options)])
648
 
 
649
 
    @mock.patch('time.time', mock.Mock(return_value=52))
650
 
    def test_info(self):
651
 
        # The handler correctly returns information about the GUI server.
652
 
        expected = {
653
 
            'apiurl': 'wss://api.example.com:17070',
654
 
            'apiversion': 'clojure',
655
 
            'debug': False,
656
 
            'deployer': 'deployments status',
657
 
            'sandbox': False,
658
 
            'uptime': 42,
659
 
            'version': get_version(),
660
 
        }
661
 
        response = self.fetch('/info')
662
 
        self.assertEqual(200, response.code)
663
 
        self.assertEqual(
664
 
            'application/json; charset=UTF-8',
665
 
            response.headers['Content-Type'])
666
 
        info = escape.json_decode(response.body)
667
 
        self.assertEqual(expected, info)
668
 
 
669
 
 
670
 
class TestHttpsRedirectHandler(LogTrapTestCase, AsyncHTTPTestCase):
 
331
class TestHttpsRedirectHandler(AsyncHTTPTestCase, LogTrapTestCase):
671
332
 
672
333
    def get_app(self):
673
334
        return web.Application([(r'.*', handlers.HttpsRedirectHandler)])