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)
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'])
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'])
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)
181
157
handler.on_juju_message, mock_websocket_connect.call_args[0])
333
291
self.assertFalse(self.handler.user.is_authenticated)
334
292
self.assertFalse(self.handler.auth.in_progress())
336
@mock.patch('uuid.uuid4', mock.Mock(return_value=mock.Mock(hex='DEFACED')))
337
@mock.patch('datetime.datetime',
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]
355
Created='2013-11-21T21:00:00Z',
356
Expires='2013-11-21T21:02:00Z'
360
self.assertFalse(self.handler.juju_connected)
361
self.assertEqual(0, len(self.handler._juju_message_queue))
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]
373
Error='tokens can only be created by authenticated users.',
374
ErrorCode='unauthorized access',
378
self.assertFalse(self.handler.juju_connected)
379
self.assertEqual(0, len(self.handler._juju_message_queue))
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')
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)
397
Response={'AuthTag': 'user', 'Password': 'passwd'}),
399
self.handler.ws_connection.write_message.call_args[0][0]))
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]
412
'invalid entity name or password',
413
json.loads(message)['Error'])
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]
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))
427
class TestWebSocketHandlerBundles(
428
WebSocketHandlerTestMixin, helpers.WSSTestMixin,
429
helpers.BundlesTestMixin, LogTrapTestCase, AsyncHTTPSTestCase):
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)
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))
469
class TestIndexHandler(LogTrapTestCase, AsyncHTTPTestCase):
295
class TestIndexHandler(AsyncHTTPTestCase, LogTrapTestCase):
472
298
# Set up a static path with an index.html in it.
502
328
self.ensure_index('/:flag:/activated/?my=query')
505
class TestProxyHandler(LogTrapTestCase, AsyncHTTPTestCase):
507
target_url = 'https://api.example.com:17070'
509
'Accept-Encoding': 'gzip',
510
'Authorization': 'Basic auth',
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',
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)])
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])
533
def patch_http_client(self, response):
534
"""Patch the asynchronous HTTP client used to fetch remote resources.
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.
540
future = futures.Future()
541
if isinstance(response, httpclient.HTTPError):
542
future.set_exception(response)
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)
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)
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)
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
608
self.target_url + '/path1/path2?arg1=valu1&arg2=value2',
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)
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)
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)
635
class TestInfoHandler(LogTrapTestCase, AsyncHTTPTestCase):
638
mock_deployer = mock.Mock()
639
mock_deployer.status.return_value = 'deployments status'
641
'apiurl': 'wss://api.example.com:17070',
642
'apiversion': 'clojure',
643
'deployer': mock_deployer,
647
return web.Application([(r'^/info', handlers.InfoHandler, options)])
649
@mock.patch('time.time', mock.Mock(return_value=52))
651
# The handler correctly returns information about the GUI server.
653
'apiurl': 'wss://api.example.com:17070',
654
'apiversion': 'clojure',
656
'deployer': 'deployments status',
659
'version': get_version(),
661
response = self.fetch('/info')
662
self.assertEqual(200, response.code)
664
'application/json; charset=UTF-8',
665
response.headers['Content-Type'])
666
info = escape.json_decode(response.body)
667
self.assertEqual(expected, info)
670
class TestHttpsRedirectHandler(LogTrapTestCase, AsyncHTTPTestCase):
331
class TestHttpsRedirectHandler(AsyncHTTPTestCase, LogTrapTestCase):
672
333
def get_app(self):
673
334
return web.Application([(r'.*', handlers.HttpsRedirectHandler)])