123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951 |
- # coding: utf-8
- # Import Python libs
- from __future__ import absolute_import
- import os
- import copy
- import hashlib
- # Import Salt Testing Libs
- from tests.integration import AdaptedConfigurationTestCaseMixin
- from tests.support.unit import TestCase, skipIf
- # Import Salt libs
- import salt.auth
- import salt.utils.event
- import salt.utils.json
- import salt.utils.yaml
- from salt.ext.six.moves import map, range # pylint: disable=import-error
- from tests.unit.utils.test_event import eventpublisher_process, SOCK_DIR # pylint: disable=import-error
- try:
- HAS_TORNADO = True
- except ImportError:
- HAS_TORNADO = False
- # Import 3rd-party libs
- # pylint: disable=import-error
- try:
- import tornado.escape
- import tornado.testing
- import tornado.concurrent
- from tornado.testing import AsyncTestCase, AsyncHTTPTestCase, gen_test
- from tornado.httpclient import HTTPRequest, HTTPError
- from tornado.websocket import websocket_connect
- import salt.netapi.rest_tornado as rest_tornado
- from salt.netapi.rest_tornado import saltnado
- HAS_TORNADO = True
- except ImportError:
- HAS_TORNADO = False
- # Create fake test case classes so we can properly skip the test case
- class AsyncTestCase(object):
- pass
- class AsyncHTTPTestCase(object):
- pass
- from salt.ext import six
- from salt.ext.six.moves.urllib.parse import urlencode, urlparse # pylint: disable=no-name-in-module
- # pylint: enable=import-error
- from tests.support.mock import NO_MOCK, NO_MOCK_REASON, MagicMock, patch
- @skipIf(not HAS_TORNADO, 'The tornado package needs to be installed') # pylint: disable=W0223
- class SaltnadoTestCase(TestCase, AdaptedConfigurationTestCaseMixin, AsyncHTTPTestCase):
- '''
- Mixin to hold some shared things
- '''
- content_type_map = {'json': 'application/json',
- 'json-utf8': 'application/json; charset=utf-8',
- 'yaml': 'application/x-yaml',
- 'text': 'text/plain',
- 'form': 'application/x-www-form-urlencoded',
- 'xml': 'application/xml',
- 'real-accept-header-json': 'application/json, text/javascript, */*; q=0.01',
- 'real-accept-header-yaml': 'application/x-yaml, text/yaml, */*; q=0.01'}
- auth_creds = (
- ('username', 'saltdev_api'),
- ('password', 'saltdev'),
- ('eauth', 'auto'))
- @property
- def auth_creds_dict(self):
- return dict(self.auth_creds)
- @property
- def opts(self):
- return self.get_temp_config('client_config')
- @property
- def mod_opts(self):
- return self.get_temp_config('minion')
- @property
- def auth(self):
- if not hasattr(self, '__auth'):
- self.__auth = salt.auth.LoadAuth(self.opts)
- return self.__auth
- @property
- def token(self):
- ''' Mint and return a valid token for auth_creds '''
- return self.auth.mk_token(self.auth_creds_dict)
- def setUp(self):
- super(SaltnadoTestCase, self).setUp()
- self.async_timeout_prev = os.environ.pop('ASYNC_TEST_TIMEOUT', None)
- os.environ['ASYNC_TEST_TIMEOUT'] = str(30)
- def tearDown(self):
- super(SaltnadoTestCase, self).tearDown()
- if self.async_timeout_prev is None:
- os.environ.pop('ASYNC_TEST_TIMEOUT', None)
- else:
- os.environ['ASYNC_TEST_TIMEOUT'] = self.async_timeout_prev
- if hasattr(self, 'http_server'):
- del self.http_server
- if hasattr(self, 'io_loop'):
- del self.io_loop
- if hasattr(self, '_app'):
- del self._app
- if hasattr(self, 'http_client'):
- del self.http_client
- if hasattr(self, '__port'):
- del self.__port
- if hasattr(self, '_AsyncHTTPTestCase__port'):
- del self._AsyncHTTPTestCase__port
- if hasattr(self, '__auth'):
- del self.__auth
- if hasattr(self, '_SaltnadoTestCase__auth'):
- del self._SaltnadoTestCase__auth
- if hasattr(self, '_test_generator'):
- del self._test_generator
- if hasattr(self, 'application'):
- del self.application
- def build_tornado_app(self, urls):
- application = tornado.web.Application(urls, debug=True)
- application.auth = self.auth
- application.opts = self.opts
- application.mod_opts = self.mod_opts
- return application
- def decode_body(self, response):
- if response is None:
- return response
- if six.PY2:
- return response
- if response.body:
- # Decode it
- if response.headers.get('Content-Type') == 'application/json':
- response._body = response.body.decode('utf-8')
- else:
- response._body = tornado.escape.native_str(response.body)
- return response
- def fetch(self, path, **kwargs):
- return self.decode_body(super(SaltnadoTestCase, self).fetch(path, **kwargs))
- class TestBaseSaltAPIHandler(SaltnadoTestCase):
- def get_app(self):
- class StubHandler(saltnado.BaseSaltAPIHandler): # pylint: disable=W0223
- def get(self, *args, **kwargs):
- return self.echo_stuff()
- def post(self):
- return self.echo_stuff()
- def echo_stuff(self):
- ret_dict = {'foo': 'bar'}
- attrs = ('token',
- 'start',
- 'connected',
- 'lowstate',
- )
- for attr in attrs:
- ret_dict[attr] = getattr(self, attr)
- self.write(self.serialize(ret_dict))
- urls = [('/', StubHandler),
- ('/(.*)', StubHandler)]
- return self.build_tornado_app(urls)
- def test_accept_content_type(self):
- '''
- Test the base handler's accept picking
- '''
- # send NO accept header, should come back with json
- response = self.fetch('/')
- self.assertEqual(response.headers['Content-Type'], self.content_type_map['json'])
- self.assertEqual(type(salt.utils.json.loads(response.body)), dict)
- # Request application/json
- response = self.fetch('/', headers={'Accept': self.content_type_map['json']})
- self.assertEqual(response.headers['Content-Type'], self.content_type_map['json'])
- self.assertEqual(type(salt.utils.json.loads(response.body)), dict)
- # Request application/x-yaml
- response = self.fetch('/', headers={'Accept': self.content_type_map['yaml']})
- self.assertEqual(response.headers['Content-Type'], self.content_type_map['yaml'])
- self.assertEqual(type(salt.utils.yaml.safe_load(response.body)), dict)
- # Request not supported content-type
- response = self.fetch('/', headers={'Accept': self.content_type_map['xml']})
- self.assertEqual(response.code, 406)
- # Request some JSON with a browser like Accept
- accept_header = self.content_type_map['real-accept-header-json']
- response = self.fetch('/', headers={'Accept': accept_header})
- self.assertEqual(response.headers['Content-Type'], self.content_type_map['json'])
- self.assertEqual(type(salt.utils.json.loads(response.body)), dict)
- # Request some YAML with a browser like Accept
- accept_header = self.content_type_map['real-accept-header-yaml']
- response = self.fetch('/', headers={'Accept': accept_header})
- self.assertEqual(response.headers['Content-Type'], self.content_type_map['yaml'])
- self.assertEqual(type(salt.utils.yaml.safe_load(response.body)), dict)
- def test_token(self):
- '''
- Test that the token is returned correctly
- '''
- token = salt.utils.json.loads(self.fetch('/').body)['token']
- self.assertIs(token, None)
- # send a token as a header
- response = self.fetch('/', headers={saltnado.AUTH_TOKEN_HEADER: 'foo'})
- token = salt.utils.json.loads(response.body)['token']
- self.assertEqual(token, 'foo')
- # send a token as a cookie
- response = self.fetch('/', headers={'Cookie': '{0}=foo'.format(saltnado.AUTH_COOKIE_NAME)})
- token = salt.utils.json.loads(response.body)['token']
- self.assertEqual(token, 'foo')
- # send both, make sure its the header
- response = self.fetch('/', headers={saltnado.AUTH_TOKEN_HEADER: 'foo',
- 'Cookie': '{0}=bar'.format(saltnado.AUTH_COOKIE_NAME)})
- token = salt.utils.json.loads(response.body)['token']
- self.assertEqual(token, 'foo')
- def test_deserialize(self):
- '''
- Send various encoded forms of lowstates (and bad ones) to make sure we
- handle deserialization correctly
- '''
- valid_lowstate = [{
- "client": "local",
- "tgt": "*",
- "fun": "test.fib",
- "arg": ["10"]
- },
- {
- "client": "runner",
- "fun": "jobs.lookup_jid",
- "jid": "20130603122505459265"
- }]
- # send as JSON
- response = self.fetch('/',
- method='POST',
- body=salt.utils.json.dumps(valid_lowstate),
- headers={'Content-Type': self.content_type_map['json']})
- self.assertEqual(valid_lowstate, salt.utils.json.loads(response.body)['lowstate'])
- # send yaml as json (should break)
- response = self.fetch('/',
- method='POST',
- body=salt.utils.yaml.safe_dump(valid_lowstate),
- headers={'Content-Type': self.content_type_map['json']})
- self.assertEqual(response.code, 400)
- # send as yaml
- response = self.fetch('/',
- method='POST',
- body=salt.utils.yaml.safe_dump(valid_lowstate),
- headers={'Content-Type': self.content_type_map['yaml']})
- self.assertEqual(valid_lowstate, salt.utils.json.loads(response.body)['lowstate'])
- # send json as yaml (works since yaml is a superset of json)
- response = self.fetch('/',
- method='POST',
- body=salt.utils.json.dumps(valid_lowstate),
- headers={'Content-Type': self.content_type_map['yaml']})
- self.assertEqual(valid_lowstate, salt.utils.json.loads(response.body)['lowstate'])
- # send json as text/plain
- response = self.fetch('/',
- method='POST',
- body=salt.utils.json.dumps(valid_lowstate),
- headers={'Content-Type': self.content_type_map['text']})
- self.assertEqual(valid_lowstate, salt.utils.json.loads(response.body)['lowstate'])
- # send form-urlencoded
- form_lowstate = (
- ('client', 'local'),
- ('tgt', '*'),
- ('fun', 'test.fib'),
- ('arg', '10'),
- ('arg', 'foo'),
- )
- response = self.fetch('/',
- method='POST',
- body=urlencode(form_lowstate),
- headers={'Content-Type': self.content_type_map['form']})
- returned_lowstate = salt.utils.json.loads(response.body)['lowstate']
- self.assertEqual(len(returned_lowstate), 1)
- returned_lowstate = returned_lowstate[0]
- self.assertEqual(returned_lowstate['client'], 'local')
- self.assertEqual(returned_lowstate['tgt'], '*')
- self.assertEqual(returned_lowstate['fun'], 'test.fib')
- self.assertEqual(returned_lowstate['arg'], ['10', 'foo'])
- # Send json with utf8 charset
- response = self.fetch('/',
- method='POST',
- body=salt.utils.json.dumps(valid_lowstate),
- headers={'Content-Type': self.content_type_map['json-utf8']})
- self.assertEqual(valid_lowstate, salt.utils.json.loads(response.body)['lowstate'])
- def test_get_lowstate(self):
- '''
- Test transformations low data of the function _get_lowstate
- '''
- valid_lowstate = [{
- u"client": u"local",
- u"tgt": u"*",
- u"fun": u"test.fib",
- u"arg": [u"10"]
- }]
- # Case 1. dictionary type of lowstate
- request_lowstate = {
- "client": "local",
- "tgt": "*",
- "fun": "test.fib",
- "arg": ["10"]
- }
- response = self.fetch('/',
- method='POST',
- body=salt.utils.json.dumps(request_lowstate),
- headers={'Content-Type': self.content_type_map['json']})
- self.assertEqual(valid_lowstate, salt.utils.json.loads(response.body)['lowstate'])
- # Case 2. string type of arg
- request_lowstate = {
- "client": "local",
- "tgt": "*",
- "fun": "test.fib",
- "arg": "10"
- }
- response = self.fetch('/',
- method='POST',
- body=salt.utils.json.dumps(request_lowstate),
- headers={'Content-Type': self.content_type_map['json']})
- self.assertEqual(valid_lowstate, salt.utils.json.loads(response.body)['lowstate'])
- # Case 3. Combine Case 1 and Case 2.
- request_lowstate = {
- "client": "local",
- "tgt": "*",
- "fun": "test.fib",
- "arg": "10"
- }
- # send as json
- response = self.fetch('/',
- method='POST',
- body=salt.utils.json.dumps(request_lowstate),
- headers={'Content-Type': self.content_type_map['json']})
- self.assertEqual(valid_lowstate, salt.utils.json.loads(response.body)['lowstate'])
- # send as yaml
- response = self.fetch('/',
- method='POST',
- body=salt.utils.yaml.safe_dump(request_lowstate),
- headers={'Content-Type': self.content_type_map['yaml']})
- self.assertEqual(valid_lowstate, salt.utils.json.loads(response.body)['lowstate'])
- # send as plain text
- response = self.fetch('/',
- method='POST',
- body=salt.utils.json.dumps(request_lowstate),
- headers={'Content-Type': self.content_type_map['text']})
- self.assertEqual(valid_lowstate, salt.utils.json.loads(response.body)['lowstate'])
- # send as form-urlencoded
- request_form_lowstate = (
- ('client', 'local'),
- ('tgt', '*'),
- ('fun', 'test.fib'),
- ('arg', '10'),
- )
- response = self.fetch('/',
- method='POST',
- body=urlencode(request_form_lowstate),
- headers={'Content-Type': self.content_type_map['form']})
- self.assertEqual(valid_lowstate, salt.utils.json.loads(response.body)['lowstate'])
- def test_cors_origin_wildcard(self):
- '''
- Check that endpoints returns Access-Control-Allow-Origin
- '''
- self._app.mod_opts['cors_origin'] = '*'
- headers = self.fetch('/').headers
- self.assertEqual(headers["Access-Control-Allow-Origin"], "*")
- def test_cors_origin_single(self):
- '''
- Check that endpoints returns the Access-Control-Allow-Origin when
- only one origins is set
- '''
- self._app.mod_opts['cors_origin'] = 'http://example.foo'
- # Example.foo is an authorized origin
- headers = self.fetch('/', headers={'Origin': 'http://example.foo'}).headers
- self.assertEqual(headers["Access-Control-Allow-Origin"], "http://example.foo")
- # Example2.foo is not an authorized origin
- headers = self.fetch('/', headers={'Origin': 'http://example2.foo'}).headers
- self.assertEqual(headers.get("Access-Control-Allow-Origin"), None)
- def test_cors_origin_multiple(self):
- '''
- Check that endpoints returns the Access-Control-Allow-Origin when
- multiple origins are set
- '''
- self._app.mod_opts['cors_origin'] = ['http://example.foo', 'http://foo.example']
- # Example.foo is an authorized origin
- headers = self.fetch('/', headers={'Origin': 'http://example.foo'}).headers
- self.assertEqual(headers["Access-Control-Allow-Origin"], "http://example.foo")
- # Example2.foo is not an authorized origin
- headers = self.fetch('/', headers={'Origin': 'http://example2.foo'}).headers
- self.assertEqual(headers.get("Access-Control-Allow-Origin"), None)
- def test_cors_preflight_request(self):
- '''
- Check that preflight request contains right headers
- '''
- self._app.mod_opts['cors_origin'] = '*'
- request_headers = 'X-Auth-Token, accept, content-type'
- preflight_headers = {'Access-Control-Request-Headers': request_headers,
- 'Access-Control-Request-Method': 'GET'}
- response = self.fetch('/', method='OPTIONS', headers=preflight_headers)
- headers = response.headers
- self.assertEqual(response.code, 204)
- self.assertEqual(headers['Access-Control-Allow-Headers'], request_headers)
- self.assertEqual(headers['Access-Control-Expose-Headers'], 'X-Auth-Token')
- self.assertEqual(headers['Access-Control-Allow-Methods'], 'OPTIONS, GET, POST')
- self.assertEqual(response.code, 204)
- def test_cors_origin_url_with_arguments(self):
- '''
- Check that preflight requests works with url with components
- like jobs or minions endpoints.
- '''
- self._app.mod_opts['cors_origin'] = '*'
- request_headers = 'X-Auth-Token, accept, content-type'
- preflight_headers = {'Access-Control-Request-Headers': request_headers,
- 'Access-Control-Request-Method': 'GET'}
- response = self.fetch('/1234567890', method='OPTIONS',
- headers=preflight_headers)
- headers = response.headers
- self.assertEqual(response.code, 204)
- self.assertEqual(headers["Access-Control-Allow-Origin"], "*")
- @skipIf(NO_MOCK, NO_MOCK_REASON)
- class TestWebhookSaltHandler(SaltnadoTestCase):
- def get_app(self):
- urls = [
- (r'/hook(/.*)?', saltnado.WebhookSaltAPIHandler),
- ]
- return self.build_tornado_app(urls)
- def test_hook_can_handle_get_parameters(self):
- with patch('salt.utils.event.get_event') as get_event:
- with patch.dict(self._app.mod_opts, {'webhook_disable_auth': True}):
- event = MagicMock()
- event.fire_event.return_value = True
- get_event.return_value = event
- response = self.fetch('/hook/my_service/?param=1¶m=2',
- body=salt.utils.json.dumps({}),
- method='POST',
- headers={'Content-Type': self.content_type_map['json']})
- self.assertEqual(response.code, 200, response.body)
- host = urlparse(response.effective_url).netloc
- event.fire_event.assert_called_once_with(
- {'headers': {'Content-Length': '2',
- 'Connection': 'close',
- 'Content-Type': 'application/json',
- 'Host': host,
- 'Accept-Encoding': 'gzip'},
- 'post': {},
- 'get': {'param': ['1', '2']}
- },
- 'salt/netapi/hook/my_service/',
- )
- class TestSaltAuthHandler(SaltnadoTestCase):
- def get_app(self):
- urls = [('/login', saltnado.SaltAuthHandler)]
- return self.build_tornado_app(urls)
- def test_get(self):
- '''
- We don't allow gets, so assert we get 401s
- '''
- response = self.fetch('/login')
- self.assertEqual(response.code, 401)
- def test_login(self):
- '''
- Test valid logins
- '''
- # Test in form encoded
- response = self.fetch('/login',
- method='POST',
- body=urlencode(self.auth_creds),
- headers={'Content-Type': self.content_type_map['form']})
- self.assertEqual(response.code, 200)
- response_obj = salt.utils.json.loads(response.body)['return'][0]
- self.assertEqual(sorted(response_obj['perms']), sorted(self.opts['external_auth']['auto'][self.auth_creds_dict['username']]))
- self.assertIn('token', response_obj) # TODO: verify that its valid?
- self.assertEqual(response_obj['user'], self.auth_creds_dict['username'])
- self.assertEqual(response_obj['eauth'], self.auth_creds_dict['eauth'])
- # Test in JSON
- response = self.fetch('/login',
- method='POST',
- body=salt.utils.json.dumps(self.auth_creds_dict),
- headers={'Content-Type': self.content_type_map['json']})
- self.assertEqual(response.code, 200)
- response_obj = salt.utils.json.loads(response.body)['return'][0]
- self.assertEqual(sorted(response_obj['perms']), sorted(self.opts['external_auth']['auto'][self.auth_creds_dict['username']]))
- self.assertIn('token', response_obj) # TODO: verify that its valid?
- self.assertEqual(response_obj['user'], self.auth_creds_dict['username'])
- self.assertEqual(response_obj['eauth'], self.auth_creds_dict['eauth'])
- # Test in YAML
- response = self.fetch('/login',
- method='POST',
- body=salt.utils.yaml.safe_dump(self.auth_creds_dict),
- headers={'Content-Type': self.content_type_map['yaml']})
- self.assertEqual(response.code, 200)
- response_obj = salt.utils.json.loads(response.body)['return'][0]
- self.assertEqual(sorted(response_obj['perms']), sorted(self.opts['external_auth']['auto'][self.auth_creds_dict['username']]))
- self.assertIn('token', response_obj) # TODO: verify that its valid?
- self.assertEqual(response_obj['user'], self.auth_creds_dict['username'])
- self.assertEqual(response_obj['eauth'], self.auth_creds_dict['eauth'])
- def test_login_missing_password(self):
- '''
- Test logins with bad/missing passwords
- '''
- bad_creds = []
- for key, val in six.iteritems(self.auth_creds_dict):
- if key == 'password':
- continue
- bad_creds.append((key, val))
- response = self.fetch('/login',
- method='POST',
- body=urlencode(bad_creds),
- headers={'Content-Type': self.content_type_map['form']})
- self.assertEqual(response.code, 400)
- def test_login_bad_creds(self):
- '''
- Test logins with bad/missing passwords
- '''
- bad_creds = []
- for key, val in six.iteritems(self.auth_creds_dict):
- if key == 'username':
- val = val + 'foo'
- if key == 'eauth':
- val = 'sharedsecret'
- bad_creds.append((key, val))
- response = self.fetch('/login',
- method='POST',
- body=urlencode(bad_creds),
- headers={'Content-Type': self.content_type_map['form']})
- self.assertEqual(response.code, 401)
- def test_login_invalid_data_structure(self):
- '''
- Test logins with either list or string JSON payload
- '''
- response = self.fetch('/login',
- method='POST',
- body=salt.utils.json.dumps(self.auth_creds),
- headers={'Content-Type': self.content_type_map['form']})
- self.assertEqual(response.code, 400)
- response = self.fetch('/login',
- method='POST',
- body=salt.utils.json.dumps(42),
- headers={'Content-Type': self.content_type_map['form']})
- self.assertEqual(response.code, 400)
- response = self.fetch('/login',
- method='POST',
- body=salt.utils.json.dumps('mystring42'),
- headers={'Content-Type': self.content_type_map['form']})
- self.assertEqual(response.code, 400)
- class TestSaltRunHandler(SaltnadoTestCase):
- def get_app(self):
- urls = [('/run', saltnado.RunSaltAPIHandler)]
- return self.build_tornado_app(urls)
- def test_authentication_exception_consistency(self):
- '''
- Test consistency of authentication exception of each clients.
- '''
- valid_response = {'return': ['Failed to authenticate']}
- clients = ['local', 'local_async', 'runner', 'runner_async']
- request_lowstates = map(lambda client: {"client": client,
- "tgt": "*",
- "fun": "test.fib",
- "arg": ["10"]},
- clients)
- for request_lowstate in request_lowstates:
- response = self.fetch('/run',
- method='POST',
- body=salt.utils.json.dumps(request_lowstate),
- headers={'Content-Type': self.content_type_map['json']})
- self.assertEqual(valid_response, salt.utils.json.loads(response.body))
- @skipIf(not HAS_TORNADO, 'The tornado package needs to be installed') # pylint: disable=W0223
- class TestWebsocketSaltAPIHandler(SaltnadoTestCase):
- def get_app(self):
- opts = copy.deepcopy(self.opts)
- opts.setdefault('rest_tornado', {})['websockets'] = True
- return rest_tornado.get_application(opts)
- @gen_test
- def test_websocket_handler_upgrade_to_websocket(self):
- response = yield self.http_client.fetch(self.get_url('/login'),
- method='POST',
- body=urlencode(self.auth_creds),
- headers={'Content-Type': self.content_type_map['form']})
- token = salt.utils.json.loads(self.decode_body(response).body)['return'][0]['token']
- url = 'ws://127.0.0.1:{0}/all_events/{1}'.format(self.get_http_port(), token)
- request = HTTPRequest(url, headers={'Origin': 'http://example.com',
- 'Host': 'example.com'})
- ws = yield websocket_connect(request)
- ws.write_message('websocket client ready')
- ws.close()
- @gen_test
- def test_websocket_handler_bad_token(self):
- """
- A bad token should returns a 401 during a websocket connect
- """
- token = 'A'*len(getattr(hashlib, self.opts.get('hash_type', 'md5'))().hexdigest())
- url = 'ws://127.0.0.1:{0}/all_events/{1}'.format(self.get_http_port(), token)
- request = HTTPRequest(url, headers={'Origin': 'http://example.com',
- 'Host': 'example.com'})
- try:
- ws = yield websocket_connect(request)
- except HTTPError as error:
- self.assertEqual(error.code, 401)
- @gen_test
- def test_websocket_handler_cors_origin_wildcard(self):
- self._app.mod_opts['cors_origin'] = '*'
- response = yield self.http_client.fetch(self.get_url('/login'),
- method='POST',
- body=urlencode(self.auth_creds),
- headers={'Content-Type': self.content_type_map['form']})
- token = salt.utils.json.loads(self.decode_body(response).body)['return'][0]['token']
- url = 'ws://127.0.0.1:{0}/all_events/{1}'.format(self.get_http_port(), token)
- request = HTTPRequest(url, headers={'Origin': 'http://foo.bar',
- 'Host': 'example.com'})
- ws = yield websocket_connect(request)
- ws.write_message('websocket client ready')
- ws.close()
- @gen_test
- def test_cors_origin_single(self):
- self._app.mod_opts['cors_origin'] = 'http://example.com'
- response = yield self.http_client.fetch(self.get_url('/login'),
- method='POST',
- body=urlencode(self.auth_creds),
- headers={'Content-Type': self.content_type_map['form']})
- token = salt.utils.json.loads(self.decode_body(response).body)['return'][0]['token']
- url = 'ws://127.0.0.1:{0}/all_events/{1}'.format(self.get_http_port(), token)
- # Example.com should works
- request = HTTPRequest(url, headers={'Origin': 'http://example.com',
- 'Host': 'example.com'})
- ws = yield websocket_connect(request)
- ws.write_message('websocket client ready')
- ws.close()
- # But foo.bar not
- request = HTTPRequest(url, headers={'Origin': 'http://foo.bar',
- 'Host': 'example.com'})
- try:
- ws = yield websocket_connect(request)
- except HTTPError as error:
- self.assertEqual(error.code, 403)
- @gen_test
- def test_cors_origin_multiple(self):
- self._app.mod_opts['cors_origin'] = ['http://example.com', 'http://foo.bar']
- response = yield self.http_client.fetch(self.get_url('/login'),
- method='POST',
- body=urlencode(self.auth_creds),
- headers={'Content-Type': self.content_type_map['form']})
- token = salt.utils.json.loads(self.decode_body(response).body)['return'][0]['token']
- url = 'ws://127.0.0.1:{0}/all_events/{1}'.format(self.get_http_port(), token)
- # Example.com should works
- request = HTTPRequest(url, headers={'Origin': 'http://example.com',
- 'Host': 'example.com'})
- ws = yield websocket_connect(request)
- ws.write_message('websocket client ready')
- ws.close()
- # Foo.bar too
- request = HTTPRequest(url, headers={'Origin': 'http://foo.bar',
- 'Host': 'example.com'})
- ws = yield websocket_connect(request)
- ws.write_message('websocket client ready')
- ws.close()
- @skipIf(not HAS_TORNADO, 'The tornado package needs to be installed')
- class TestSaltnadoUtils(AsyncTestCase):
- def test_any_future(self):
- '''
- Test that the Any Future does what we think it does
- '''
- # create a few futures
- futures = []
- for x in range(0, 3):
- future = tornado.concurrent.Future()
- future.add_done_callback(self.stop)
- futures.append(future)
- # create an any future, make sure it isn't immediately done
- any_ = saltnado.Any(futures)
- self.assertIs(any_.done(), False)
- # finish one, lets see who finishes
- futures[0].set_result('foo')
- self.wait()
- self.assertIs(any_.done(), True)
- self.assertIs(futures[0].done(), True)
- self.assertIs(futures[1].done(), False)
- self.assertIs(futures[2].done(), False)
- # make sure it returned the one that finished
- self.assertEqual(any_.result(), futures[0])
- futures = futures[1:]
- # re-wait on some other futures
- any_ = saltnado.Any(futures)
- futures[0].set_result('foo')
- self.wait()
- self.assertIs(any_.done(), True)
- self.assertIs(futures[0].done(), True)
- self.assertIs(futures[1].done(), False)
- @skipIf(not HAS_TORNADO, 'The tornado package needs to be installed')
- class TestEventListener(AsyncTestCase):
- def setUp(self):
- if not os.path.exists(SOCK_DIR):
- os.makedirs(SOCK_DIR)
- super(TestEventListener, self).setUp()
- def test_simple(self):
- '''
- Test getting a few events
- '''
- with eventpublisher_process():
- me = salt.utils.event.MasterEvent(SOCK_DIR)
- event_listener = saltnado.EventListener({}, # we don't use mod_opts, don't save?
- {'sock_dir': SOCK_DIR,
- 'transport': 'zeromq'})
- self._finished = False # fit to event_listener's behavior
- event_future = event_listener.get_event(self, 'evt1', callback=self.stop) # get an event future
- me.fire_event({'data': 'foo2'}, 'evt2') # fire an event we don't want
- me.fire_event({'data': 'foo1'}, 'evt1') # fire an event we do want
- self.wait() # wait for the future
- # check that we got the event we wanted
- self.assertTrue(event_future.done())
- self.assertEqual(event_future.result()['tag'], 'evt1')
- self.assertEqual(event_future.result()['data']['data'], 'foo1')
- def test_set_event_handler(self):
- '''
- Test subscribing events using set_event_handler
- '''
- with eventpublisher_process():
- me = salt.utils.event.MasterEvent(SOCK_DIR)
- event_listener = saltnado.EventListener({}, # we don't use mod_opts, don't save?
- {'sock_dir': SOCK_DIR,
- 'transport': 'zeromq'})
- self._finished = False # fit to event_listener's behavior
- event_future = event_listener.get_event(self,
- tag='evt',
- callback=self.stop,
- timeout=1,
- ) # get an event future
- me.fire_event({'data': 'foo'}, 'evt') # fire an event we do want
- self.wait()
- # check that we subscribed the event we wanted
- self.assertEqual(len(event_listener.timeout_map), 0)
- def test_timeout(self):
- '''
- Make sure timeouts work correctly
- '''
- with eventpublisher_process():
- event_listener = saltnado.EventListener({}, # we don't use mod_opts, don't save?
- {'sock_dir': SOCK_DIR,
- 'transport': 'zeromq'})
- self._finished = False # fit to event_listener's behavior
- event_future = event_listener.get_event(self,
- tag='evt1',
- callback=self.stop,
- timeout=1,
- ) # get an event future
- self.wait()
- self.assertTrue(event_future.done())
- with self.assertRaises(saltnado.TimeoutException):
- event_future.result()
- def test_clean_by_request(self):
- '''
- Make sure the method clean_by_request clean up every related data in EventListener
- request_future_1 : will be timeout-ed by clean_by_request(self)
- request_future_2 : will be finished by me.fire_event ...
- dummy_request_future_1 : will be finished by me.fire_event ...
- dummy_request_future_2 : will be timeout-ed by clean-by_request(dummy_request)
- '''
- class DummyRequest(object):
- '''
- Dummy request object to simulate the request object
- '''
- @property
- def _finished(self):
- '''
- Simulate _finished of the request object
- '''
- return False
- # Inner functions never permit modifying primitive values directly
- cnt = [0]
- def stop():
- '''
- To realize the scenario of this test, define a custom stop method to call
- self.stop after finished two events.
- '''
- cnt[0] += 1
- if cnt[0] == 2:
- self.stop()
- with eventpublisher_process():
- me = salt.utils.event.MasterEvent(SOCK_DIR)
- event_listener = saltnado.EventListener({}, # we don't use mod_opts, don't save?
- {'sock_dir': SOCK_DIR,
- 'transport': 'zeromq'})
- self.assertEqual(0, len(event_listener.tag_map))
- self.assertEqual(0, len(event_listener.request_map))
- self._finished = False # fit to event_listener's behavior
- dummy_request = DummyRequest()
- request_future_1 = event_listener.get_event(self, tag='evt1')
- request_future_2 = event_listener.get_event(self, tag='evt2', callback=lambda f: stop())
- dummy_request_future_1 = event_listener.get_event(dummy_request, tag='evt3', callback=lambda f: stop())
- dummy_request_future_2 = event_listener.get_event(dummy_request, timeout=10, tag='evt4')
- self.assertEqual(4, len(event_listener.tag_map))
- self.assertEqual(2, len(event_listener.request_map))
- me.fire_event({'data': 'foo2'}, 'evt2')
- me.fire_event({'data': 'foo3'}, 'evt3')
- self.wait()
- event_listener.clean_by_request(self)
- me.fire_event({'data': 'foo1'}, 'evt1')
- self.assertTrue(request_future_1.done())
- with self.assertRaises(saltnado.TimeoutException):
- request_future_1.result()
- self.assertTrue(request_future_2.done())
- self.assertEqual(request_future_2.result()['tag'], 'evt2')
- self.assertEqual(request_future_2.result()['data']['data'], 'foo2')
- self.assertTrue(dummy_request_future_1.done())
- self.assertEqual(dummy_request_future_1.result()['tag'], 'evt3')
- self.assertEqual(dummy_request_future_1.result()['data']['data'], 'foo3')
- self.assertFalse(dummy_request_future_2.done())
- self.assertEqual(2, len(event_listener.tag_map))
- self.assertEqual(1, len(event_listener.request_map))
- event_listener.clean_by_request(dummy_request)
- with self.assertRaises(saltnado.TimeoutException):
- dummy_request_future_2.result()
- self.assertEqual(0, len(event_listener.tag_map))
- self.assertEqual(0, len(event_listener.request_map))
|