1 # -*- coding: utf-8 -*-
2 # pylint: disable=too-many-arguments
3 from __future__
import absolute_import
9 from typing
import Any
, Dict
10 from unittest
.mock
import Mock
13 from cherrypy
._cptools
import HandlerWrapperTool
14 from cherrypy
.test
import helper
15 from mgr_module
import HandleCommandResult
16 from pyfakefs
import fake_filesystem
19 from ..controllers
import generate_controller_routes
, json_error_page
20 from ..controllers
._version
import APIVersion
21 from ..module
import Module
22 from ..plugins
import PLUGIN_MANAGER
, debug
, feature_toggles
# noqa
23 from ..services
.auth
import AuthManagerTool
24 from ..services
.exception
import dashboard_exception_handler
25 from ..tools
import RequestLoggingTool
27 PLUGIN_MANAGER
.hook
.init()
28 PLUGIN_MANAGER
.hook
.register_commands()
31 logger
= logging
.getLogger('tests')
34 class ModuleTestClass(Module
):
35 """Dashboard module subclass for testing the module methods."""
37 def __init__(self
) -> None:
40 def _unconfigure_logging(self
) -> None:
44 class CmdException(Exception):
45 def __init__(self
, retcode
, message
):
46 super(CmdException
, self
).__init
__(message
)
47 self
.retcode
= retcode
50 class KVStoreMockMixin(object):
54 def mock_set_module_option(cls
, attr
, val
):
55 cls
.CONFIG_KEY_DICT
[attr
] = val
58 def mock_get_module_option(cls
, attr
, default
=None):
59 return cls
.CONFIG_KEY_DICT
.get(attr
, default
)
62 def mock_kv_store(cls
):
63 cls
.CONFIG_KEY_DICT
.clear()
64 mgr
.set_module_option
.side_effect
= cls
.mock_set_module_option
65 mgr
.get_module_option
.side_effect
= cls
.mock_get_module_option
67 mgr
.set_store
.side_effect
= cls
.mock_set_module_option
68 mgr
.get_store
.side_effect
= cls
.mock_get_module_option
71 def get_key(cls
, key
):
72 return cls
.CONFIG_KEY_DICT
.get(key
, None)
75 # pylint: disable=protected-access
76 class CLICommandTestMixin(KVStoreMockMixin
):
77 _dashboard_module
= ModuleTestClass()
80 def exec_cmd(cls
, cmd
, **kwargs
):
81 inbuf
= kwargs
['inbuf'] if 'inbuf' in kwargs
else None
82 cmd_dict
= {'prefix': 'dashboard {}'.format(cmd
)}
83 cmd_dict
.update(kwargs
)
85 result
= HandleCommandResult(*cls
._dashboard
_module
._handle
_command
(inbuf
, cmd_dict
))
88 raise CmdException(result
.retval
, result
.stderr
)
90 return json
.loads(result
.stdout
)
95 class FakeFsMixin(object):
96 fs
= fake_filesystem
.FakeFilesystem()
97 f_open
= fake_filesystem
.FakeFileOpen(fs
)
98 f_os
= fake_filesystem
.FakeOsModule(fs
)
99 builtins_open
= 'builtins.open'
102 class ControllerTestCase(helper
.CPWebCase
):
103 _endpoints_cache
= {}
106 def setup_controllers(cls
, ctrl_classes
, base_url
='', cp_config
: Dict
[str, Any
] = None):
107 if not isinstance(ctrl_classes
, list):
108 ctrl_classes
= [ctrl_classes
]
109 mapper
= cherrypy
.dispatch
.RoutesDispatcher()
111 for ctrl
in ctrl_classes
:
113 'tools.dashboard_exception_handler.on': True,
114 'tools.authenticate.on': False
117 ctrl
._cp
_config
.update(cp_config
)
120 # We need to cache the controller endpoints because
121 # BaseController#endpoints method is not idempontent
122 # and a controller might be needed by more than one
124 if ctrl
not in cls
._endpoints
_cache
:
125 ctrl_endpoints
= ctrl
.endpoints()
126 cls
._endpoints
_cache
[ctrl
] = ctrl_endpoints
128 ctrl_endpoints
= cls
._endpoints
_cache
[ctrl
]
129 for endpoint
in ctrl_endpoints
:
131 endpoint_list
.append(endpoint
)
132 endpoint_list
= sorted(endpoint_list
, key
=lambda e
: e
.url
)
133 for endpoint
in endpoint_list
:
134 generate_controller_routes(endpoint
, mapper
, base_url
)
137 cherrypy
.tree
.mount(None, config
={
138 base_url
: {'request.dispatch': mapper
}})
140 _request_logging
= False
145 cherrypy
.tools
.authenticate
= AuthManagerTool()
146 cherrypy
.tools
.dashboard_exception_handler
= HandlerWrapperTool(dashboard_exception_handler
,
148 cherrypy
.config
.update({
149 'error_page.default': json_error_page
,
150 'tools.json_in.on': True,
151 'tools.json_in.force': False
153 PLUGIN_MANAGER
.hook
.configure_cherrypy(config
=cherrypy
.config
)
155 if cls
._request
_logging
:
156 cherrypy
.tools
.request_logging
= RequestLoggingTool()
157 cherrypy
.config
.update({'tools.request_logging.on': True})
160 def tearDownClass(cls
):
161 if cls
._request
_logging
:
162 cherrypy
.config
.update({'tools.request_logging.on': False})
164 def _request(self
, url
, method
, data
=None, headers
=None, version
=APIVersion
.DEFAULT
):
168 h
= [('Accept', version
.to_mime_type()),
169 ('Content-Length', '0')]
174 if version
is not None:
175 h
= [('Accept', version
.to_mime_type()),
176 ('Content-Type', 'application/json'),
177 ('Content-Length', str(len(b
)))]
180 h
= [('Content-Type', 'application/json'),
181 ('Content-Length', str(len(b
)))]
185 self
.getPage(url
, method
=method
, body
=b
, headers
=h
)
187 def _get(self
, url
, headers
=None, version
=APIVersion
.DEFAULT
):
188 self
._request
(url
, 'GET', headers
=headers
, version
=version
)
190 def _post(self
, url
, data
=None, version
=APIVersion
.DEFAULT
):
191 self
._request
(url
, 'POST', data
, version
=version
)
193 def _delete(self
, url
, data
=None, version
=APIVersion
.DEFAULT
):
194 self
._request
(url
, 'DELETE', data
, version
=version
)
196 def _put(self
, url
, data
=None, version
=APIVersion
.DEFAULT
):
197 self
._request
(url
, 'PUT', data
, version
=version
)
199 def _task_request(self
, method
, url
, data
, timeout
, version
=APIVersion
.DEFAULT
):
200 self
._request
(url
, method
, data
, version
=version
)
201 if self
.status
!= '202 Accepted':
202 logger
.info("task finished immediately")
205 res
= self
.json_body()
206 self
.assertIsInstance(res
, dict)
207 self
.assertIn('name', res
)
208 self
.assertIn('metadata', res
)
210 task_name
= res
['name']
211 task_metadata
= res
['metadata']
213 # pylint: disable=protected-access
214 class Waiter(threading
.Thread
):
215 def __init__(self
, task_name
, task_metadata
, tc
):
216 super(Waiter
, self
).__init
__()
217 self
.task_name
= task_name
218 self
.task_metadata
= task_metadata
219 self
.ev
= threading
.Event()
226 while running
and not self
.abort
:
227 logger
.info("task (%s, %s) is still executing", self
.task_name
,
230 self
.tc
._get
('/api/task?name={}'.format(self
.task_name
), version
=version
)
231 res
= self
.tc
.json_body()
232 for task
in res
['finished_tasks']:
233 if task
['metadata'] == self
.task_metadata
:
239 thread
= Waiter(task_name
, task_metadata
, self
)
241 status
= thread
.ev
.wait(timeout
)
246 raise Exception("Waiting for task ({}, {}) to finish timed out"
247 .format(task_name
, task_metadata
))
248 logger
.info("task (%s, %s) finished", task_name
, task_metadata
)
249 if thread
.res_task
['success']:
250 self
.body
= json
.dumps(thread
.res_task
['ret_value'])
252 self
.status
= '201 Created'
253 elif method
== 'PUT':
254 self
.status
= '200 OK'
255 elif method
== 'DELETE':
256 self
.status
= '204 No Content'
259 if 'status' in thread
.res_task
['exception']:
260 self
.status
= thread
.res_task
['exception']['status']
263 self
.body
= json
.dumps(thread
.res_task
['exception'])
265 def _task_post(self
, url
, data
=None, timeout
=60, version
=APIVersion
.DEFAULT
):
266 self
._task
_request
('POST', url
, data
, timeout
, version
=version
)
268 def _task_delete(self
, url
, timeout
=60, version
=APIVersion
.DEFAULT
):
269 self
._task
_request
('DELETE', url
, None, timeout
, version
=version
)
271 def _task_put(self
, url
, data
=None, timeout
=60, version
=APIVersion
.DEFAULT
):
272 self
._task
_request
('PUT', url
, data
, timeout
, version
=version
)
275 body_str
= self
.body
.decode('utf-8') if isinstance(self
.body
, bytes
) else self
.body
276 return json
.loads(body_str
)
278 def assertJsonBody(self
, data
, msg
=None): # noqa: N802
279 """Fail if value != self.body."""
280 json_body
= self
.json_body()
281 if data
!= json_body
:
283 msg
= 'expected body:\n%r\n\nactual body:\n%r' % (
285 self
._handlewebError
(msg
)
287 def assertInJsonBody(self
, data
, msg
=None): # noqa: N802
288 json_body
= self
.json_body()
289 if data
not in json_body
:
291 msg
= 'expected %r to be in %r' % (data
, json_body
)
292 self
._handlewebError
(msg
)
296 """Test class for returning predefined values"""
299 def get_mgr_no_services(cls
):
300 mgr
.get
= Mock(return_value
={})
306 def get_daemons(cls
):
307 mgr
.get
= Mock(return_value
={'services': {'rgw': {'daemons': {
309 'addr': '192.168.178.3:49774/1534999298',
311 'frontend_config#0': 'beast port=8000',
313 'realm_name': 'realm1',
314 'zonegroup_name': 'zonegroup1',
319 'addr': '[2001:db8:85a3::8a2e:370:7334]:49774/1534999298',
321 'frontend_config#0': 'civetweb port=8002',
323 'realm_name': 'realm2',
324 'zonegroup_name': 'zonegroup2',
331 def get_settings(cls
):
333 'RGW_API_ACCESS_KEY': 'fake-access-key',
334 'RGW_API_SECRET_KEY': 'fake-secret-key',
336 mgr
.get_module_option
= Mock(side_effect
=settings
.get
)