| OLD | NEW |
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # coding: utf-8 | 2 # coding: utf-8 |
| 3 # Copyright 2013 The LUCI Authors. All rights reserved. | 3 # Copyright 2013 The LUCI Authors. All rights reserved. |
| 4 # Use of this source code is governed under the Apache License, Version 2.0 | 4 # Use of this source code is governed under the Apache License, Version 2.0 |
| 5 # that can be found in the LICENSE file. | 5 # that can be found in the LICENSE file. |
| 6 | 6 |
| 7 import datetime | 7 import datetime |
| 8 import itertools | 8 import itertools |
| 9 import json | 9 import json |
| 10 import logging | 10 import logging |
| (...skipping 30 matching lines...) Expand all Loading... |
| 41 }) | 41 }) |
| 42 | 42 |
| 43 def tearDown(self): | 43 def tearDown(self): |
| 44 try: | 44 try: |
| 45 template.reset() | 45 template.reset() |
| 46 finally: | 46 finally: |
| 47 super(AppTestBase, self).tearDown() | 47 super(AppTestBase, self).tearDown() |
| 48 | 48 |
| 49 | 49 |
| 50 class FrontendTest(AppTestBase): | 50 class FrontendTest(AppTestBase): |
| 51 def test_bots(self): | |
| 52 self.set_as_admin() | |
| 53 | |
| 54 # Add bots to display. | |
| 55 state = { | |
| 56 'dict': {'random': 'values'}, | |
| 57 'float': 0., | |
| 58 'list': ['of', 'things'], | |
| 59 'str': u'uni', | |
| 60 } | |
| 61 bot_management.bot_event( | |
| 62 event_type='bot_connected', bot_id='id1', | |
| 63 external_ip='8.8.4.4', authenticated_as='bot:whitelisted-ip', | |
| 64 dimensions={'id': ['id1']}, state=state, version='123456789', | |
| 65 quarantined=False, task_id=None, task_name=None) | |
| 66 bot_management.bot_event( | |
| 67 event_type='bot_connected', bot_id='id2', | |
| 68 external_ip='8.8.8.8', authenticated_as='bot:whitelisted-ip', | |
| 69 dimensions={'id': ['id2']}, state={'ram': 65}, version='123456789', | |
| 70 quarantined=False, task_id=None, task_name=None) | |
| 71 | |
| 72 response = self.app.get('/restricted/bots', status=200) | |
| 73 self.assertGreater(len(response.body), 1000) | |
| 74 | |
| 75 def test_delete_bot(self): | |
| 76 self.set_as_admin() | |
| 77 | |
| 78 bot_management.bot_event( | |
| 79 event_type='bot_connected', bot_id='id1', | |
| 80 external_ip='8.8.4.4', authenticated_as='bot:whitelisted-ip', | |
| 81 dimensions={'id': ['id1']}, state={'foo': 'bar'}, version='123456789', | |
| 82 quarantined=False, task_id=None, task_name=None) | |
| 83 response = self.app.get('/restricted/bots', status=200) | |
| 84 self.assertTrue('id1' in response.body) | |
| 85 | |
| 86 response = self.app.post( | |
| 87 '/restricted/bot/id1/delete', | |
| 88 params={}, | |
| 89 headers={'X-XSRF-Token': self.get_xsrf_token()}) | |
| 90 self.assertFalse('id1' in response.body) | |
| 91 | |
| 92 response = self.app.get('/restricted/bots', status=200) | |
| 93 self.assertFalse('id1' in response.body) | |
| 94 | 51 |
| 95 def test_root(self): | 52 def test_root(self): |
| 96 response = self.app.get('/', status=200) | 53 response = self.app.get('/', status=200) |
| 97 self.assertGreater(len(response.body), 600) | 54 self.assertGreater(len(response.body), 600) |
| 98 | 55 |
| 99 def testAllSwarmingHandlersAreSecured(self): | 56 def testAllSwarmingHandlersAreSecured(self): |
| 100 # Test that all handlers are accessible only to authenticated user or | 57 # Test that all handlers are accessible only to authenticated user or |
| 101 # bots. Assumes all routes are defined with plain paths (i.e. | 58 # bots. Assumes all routes are defined with plain paths (i.e. |
| 102 # '/some/handler/path' and not regexps). | 59 # '/some/handler/path' and not regexps). |
| 103 | 60 |
| (...skipping 11 matching lines...) Expand all Loading... |
| 115 '/api/config/v1/validate', | 72 '/api/config/v1/validate', |
| 116 '/auth', | 73 '/auth', |
| 117 '/ereporter2/api/v1/on_error', | 74 '/ereporter2/api/v1/on_error', |
| 118 '/stats', | 75 '/stats', |
| 119 '/api/swarming/v1/server/permissions', | 76 '/api/swarming/v1/server/permissions', |
| 120 '/swarming/api/v1/client/list', | 77 '/swarming/api/v1/client/list', |
| 121 '/swarming/api/v1/bot/server_ping', | 78 '/swarming/api/v1/bot/server_ping', |
| 122 '/swarming/api/v1/stats/summary/<resolution:[a-z]+>', | 79 '/swarming/api/v1/stats/summary/<resolution:[a-z]+>', |
| 123 '/swarming/api/v1/stats/dimensions/<dimensions:.+>/<resolution:[a-z]+>', | 80 '/swarming/api/v1/stats/dimensions/<dimensions:.+>/<resolution:[a-z]+>', |
| 124 '/swarming/api/v1/stats/user/<user:.+>/<resolution:[a-z]+>', | 81 '/swarming/api/v1/stats/user/<user:.+>/<resolution:[a-z]+>', |
| 82 '/user/tasks', |
| 83 '/restricted/bots', |
| 125 ]) | 84 ]) |
| 126 | 85 |
| 127 # Grab the set of all routes. | 86 # Grab the set of all routes. |
| 128 app = self.app.app | 87 app = self.app.app |
| 129 routes = set(app.router.match_routes) | 88 routes = set(app.router.match_routes) |
| 130 routes.update(app.router.build_routes.itervalues()) | 89 routes.update(app.router.build_routes.itervalues()) |
| 131 | 90 |
| 132 # Get all routes that are not protected by GAE auth mechanism. | 91 # Get all routes that are not protected by GAE auth mechanism. |
| 133 routes_to_check = [ | 92 routes_to_check = [ |
| 134 route for route in routes | 93 route for route in routes |
| (...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 183 '/swarming/api/v1/stats/dimensions/%s/days' % quoted, | 142 '/swarming/api/v1/stats/dimensions/%s/days' % quoted, |
| 184 '/swarming/api/v1/stats/dimensions/%s/hours' % quoted, | 143 '/swarming/api/v1/stats/dimensions/%s/hours' % quoted, |
| 185 '/swarming/api/v1/stats/dimensions/%s/minutes' % quoted, | 144 '/swarming/api/v1/stats/dimensions/%s/minutes' % quoted, |
| 186 ) | 145 ) |
| 187 for url in urls: | 146 for url in urls: |
| 188 self.app.get(url, status=403) | 147 self.app.get(url, status=403) |
| 189 self.set_as_user() | 148 self.set_as_user() |
| 190 for url in urls: | 149 for url in urls: |
| 191 self.app.get(url, status=200) | 150 self.app.get(url, status=200) |
| 192 | 151 |
| 193 def test_task_list_empty(self): | 152 def test_task_redirect(self): |
| 194 # Just assert it doesn't throw. | |
| 195 self.set_as_privileged_user() | |
| 196 self.app.get('/user/tasks', status=200) | |
| 197 self.app.get('/user/task/12345', status=404) | |
| 198 | |
| 199 def test_add_task_and_list_user(self): | |
| 200 # Add a task via the API as a user, then assert it can be viewed. | |
| 201 self.set_as_user() | |
| 202 _, task_id = self.client_create_task_raw() | |
| 203 | |
| 204 self.set_as_privileged_user() | |
| 205 self.app.get('/user/tasks', status=200) | |
| 206 self.app.get('/user/task/%s' % task_id, status=200) | |
| 207 | |
| 208 self.set_as_bot() | |
| 209 self.do_handshake() | |
| 210 reaped = self.bot_poll() | |
| 211 self.bot_complete_task(task_id=reaped['manifest']['task_id']) | |
| 212 # Add unicode chars. | |
| 213 | |
| 214 # This can only work once a bot reaped the task. | |
| 215 self.set_as_privileged_user() | |
| 216 self.app.get('/user/task/%s' % reaped['manifest']['task_id'], status=200) | |
| 217 | |
| 218 def test_task_deduped(self): | |
| 219 self.set_as_user() | |
| 220 _, task_id_1 = self.client_create_task_raw(properties=dict(idempotent=True)) | |
| 221 | |
| 222 self.set_as_bot() | |
| 223 task_id_bot = self.bot_run_task() | |
| 224 self.assertEqual(task_id_1, task_id_bot[:-1] + '0') | |
| 225 self.assertEqual('1', task_id_bot[-1:]) | |
| 226 | |
| 227 # Create a second task. Results will be returned immediately without the bot | |
| 228 # running anything. | |
| 229 self.set_as_user() | |
| 230 _, task_id_2 = self.client_create_task_raw( | |
| 231 name='ho', properties=dict(idempotent=True)) | |
| 232 | |
| 233 self.set_as_bot() | |
| 234 resp = self.bot_poll() | |
| 235 self.assertEqual('sleep', resp['cmd']) | |
| 236 | |
| 237 self.set_as_privileged_user() | |
| 238 # Look at the results. It's the same as the previous run, even if task_id_2 | |
| 239 # was never executed. | |
| 240 response = self.app.get('/user/task/%s' % task_id_2, status=200) | |
| 241 self.assertTrue( | |
| 242 u'rÉsult string'.encode('utf-8') in response.body, response.body) | |
| 243 self.assertTrue('Was deduped from' in response.body, response.body) | |
| 244 | |
| 245 def test_task_denied(self): | |
| 246 # Add a task via the API as a user, then assert it can't be viewed by | |
| 247 # anonymous user. | |
| 248 self.set_as_user() | |
| 249 _, task_id = self.client_create_task_raw() | |
| 250 | |
| 251 # Redirect to login page. | |
| 252 self.set_as_anonymous() | 153 self.set_as_anonymous() |
| 253 self.app.get('/user/tasks', status=302) | 154 self.app.get('/user/tasks', status=302) |
| 254 self.app.get('/user/task/%s' % task_id, status=302) | 155 self.app.get('/user/task/123', status=302) |
| 255 | 156 |
| 256 @staticmethod | 157 def test_bot_redirect(self): |
| 257 def _sort_state_product(): | 158 self.set_as_anonymous() |
| 258 sort_choices = [i[0] for i in handlers_frontend.TasksHandler.SORT_CHOICES] | 159 self.app.get('/restricted/bots', status=302) |
| 259 state_choices = sum( | 160 self.app.get('/restricted/bot/bot321', status=302) |
| 260 ([i[0] for i in j] | |
| 261 for j in handlers_frontend.TasksHandler.STATE_CHOICES), | |
| 262 []) | |
| 263 return itertools.product(sort_choices, state_choices) | |
| 264 | |
| 265 def test_task_list_query(self): | |
| 266 # Try all the combinations of task queries to ensure the index exist. | |
| 267 self.set_as_privileged_user() | |
| 268 self.client_create_task_raw() | |
| 269 for sort, state in self._sort_state_product(): | |
| 270 url = '/user/tasks?sort=%s&state=%s' % (sort, state) | |
| 271 # See require_index in ../components/support/test_case.py in case of | |
| 272 # NeedIndexError. Do not use status=200 so the output is printed in case | |
| 273 # of failure. | |
| 274 resp = self.app.get(url, expect_errors=True) | |
| 275 self.assertEqual(200, resp.status_code, (resp.body, sort, state)) | |
| 276 | |
| 277 self.app.get('/user/tasks?sort=foo', status=400) | |
| 278 self.app.get('/user/tasks?state=foo', status=400) | |
| 279 | |
| 280 def test_task_search_task_tag(self): | |
| 281 # Try all the combinations of task queries to ensure the index exist. | |
| 282 self.set_as_privileged_user() | |
| 283 self.client_create_task_raw() | |
| 284 self.set_as_bot() | |
| 285 reaped = self.bot_poll() | |
| 286 self.bot_complete_task(task_id=reaped['manifest']['task_id']) | |
| 287 self.set_as_privileged_user() | |
| 288 self.app.get('/user/tasks?task_tag=yo:dawg', status=200) | |
| 289 for sort, state in self._sort_state_product(): | |
| 290 url = '/user/tasks?sort=%s&state=%s' % (sort, state) | |
| 291 self.app.get(url + '&task_tag=yo:dawg', status=200) | |
| 292 | |
| 293 def test_task_cancel(self): | |
| 294 self.set_as_privileged_user() | |
| 295 _, task_id = self.client_create_task_raw() | |
| 296 | |
| 297 self.set_as_admin() | |
| 298 # Just ensure it doesn't crash when it shows the 'Cancel' button. | |
| 299 self.app.get('/user/tasks') | |
| 300 | |
| 301 xsrf_token = self.get_xsrf_token() | |
| 302 self.app.post( | |
| 303 '/user/task/%s/cancel' % task_id, {'xsrf_token': xsrf_token}) | |
| 304 | |
| 305 # Ensure there's no task available anymore by polling. | |
| 306 self.set_as_bot() | |
| 307 reaped = self.bot_poll('bot1') | |
| 308 self.assertEqual('sleep', reaped['cmd']) | |
| 309 | |
| 310 def test_task_retry(self): | |
| 311 self.set_as_privileged_user() | |
| 312 _, task_id = self.client_create_task_raw() | |
| 313 xsrf_token = self.get_xsrf_token() | |
| 314 resp = self.app.post( | |
| 315 '/user/task/%s/retry' % task_id, {'xsrf_token': xsrf_token}) | |
| 316 self.assertEqual(302, resp.status_code) | |
| 317 prefix = 'http://localhost/user/task/' | |
| 318 self.assertTrue(resp.location.startswith(prefix)) | |
| 319 new_task_id = resp.location[len(prefix):] | |
| 320 self.assertNotEqual(new_task_id, task_id) | |
| 321 | |
| 322 # Both tasks are scheduled. | |
| 323 self.set_as_bot() | |
| 324 reaped = self.bot_poll('bot1') | |
| 325 self.assertEqual('run', reaped['cmd']) | |
| 326 self.assertEqual(task_id[:-1] + '1', reaped['manifest']['task_id']) | |
| 327 reaped = self.bot_poll('bot2') | |
| 328 self.assertEqual('run', reaped['cmd']) | |
| 329 self.assertEqual(new_task_id[:-1] + '1', reaped['manifest']['task_id']) | |
| 330 | |
| 331 def test_bot_list_empty(self): | |
| 332 # Just assert it doesn't throw. | |
| 333 self.set_as_admin() | |
| 334 self.app.get('/restricted/bots', status=200) | |
| 335 self.app.get('/restricted/bot/unknown_bot', status=200) | |
| 336 | |
| 337 def test_bot_listing(self): | |
| 338 # Create a task, create 2 bots, one with a task assigned, the other without. | |
| 339 self.set_as_admin() | |
| 340 self.client_create_task_raw() | |
| 341 | |
| 342 self.set_as_bot() | |
| 343 self.bot_poll('bot1') | |
| 344 self.bot_poll('bot2') | |
| 345 params = self.do_handshake('bot2') | |
| 346 params['event'] = 'bot_log' | |
| 347 params['message'] = 'for the best' | |
| 348 self.assertEqual({}, self.post_json('/swarming/api/v1/bot/event', params)) | |
| 349 | |
| 350 self.set_as_admin() | |
| 351 response = self.app.get('/restricted/bots', status=200) | |
| 352 next_page_re = re.compile(r'<a\s+href="(.+?)">Next page</a>') | |
| 353 self.assertFalse(next_page_re.search(response.body)) | |
| 354 self.app.get('/restricted/bot/bot1', status=200) | |
| 355 response = self.app.get('/restricted/bot/bot2', status=200) | |
| 356 self.assertIn('for the best', response.body) | |
| 357 | |
| 358 response = self.app.get('/restricted/bots?limit=1', status=200) | |
| 359 url = next_page_re.search(response.body).group(1) | |
| 360 self.assertTrue( | |
| 361 url.startswith('/restricted/bots?limit=1&sort_by=__key__&cursor='), url) | |
| 362 response = self.app.get(url, status=200) | |
| 363 self.assertFalse(next_page_re.search(response.body)) | |
| 364 | |
| 365 # Test more complex indexes. | |
| 366 for sort_by in handlers_frontend.BotsListHandler.ACCEPTABLE_BOTS_SORTS: | |
| 367 response = self.app.get( | |
| 368 '/restricted/bots?limit=1&sort_by=%s' % sort_by, status=200) | |
| 369 self.assertTrue(next_page_re.search(response.body), sort_by) | |
| 370 response = self.app.get( | |
| 371 '/restricted/bots?limit=1&sort_by=%s&dimensions=os:Amiga%%0Aid:bot1' % | |
| 372 sort_by, | |
| 373 status=200) | |
| 374 # The bot1 should be in the response. | |
| 375 self.assertTrue('pool' in response.body, sort_by) | |
| 376 self.assertTrue('default' in response.body, sort_by) | |
| 377 | 161 |
| 378 | 162 |
| 379 class FrontendAdminTest(AppTestBase): | 163 class FrontendAdminTest(AppTestBase): |
| 380 # Admin-specific management pages. | 164 # Admin-specific management pages. |
| 381 def test_bootstrap_default(self): | 165 def test_bootstrap_default(self): |
| 382 self.set_as_bot() | 166 self.set_as_bot() |
| 383 self.mock(bot_code, 'generate_bootstrap_token', lambda: 'bootstrap-token') | 167 self.mock(bot_code, 'generate_bootstrap_token', lambda: 'bootstrap-token') |
| 384 actual = self.app.get('/bootstrap').body | 168 actual = self.app.get('/bootstrap').body |
| 385 path = os.path.join(self.APP_DIR, 'swarming_bot', 'config', 'bootstrap.py') | 169 path = os.path.join(self.APP_DIR, 'swarming_bot', 'config', 'bootstrap.py') |
| 386 with open(path, 'rb') as f: | 170 with open(path, 'rb') as f: |
| (...skipping 181 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 568 url+arg, headers={'X-AppEngine-QueueName': 'bogus name'}, status=403) | 352 url+arg, headers={'X-AppEngine-QueueName': 'bogus name'}, status=403) |
| 569 | 353 |
| 570 | 354 |
| 571 if __name__ == '__main__': | 355 if __name__ == '__main__': |
| 572 if '-v' in sys.argv: | 356 if '-v' in sys.argv: |
| 573 unittest.TestCase.maxDiff = None | 357 unittest.TestCase.maxDiff = None |
| 574 logging.basicConfig( | 358 logging.basicConfig( |
| 575 level=logging.DEBUG if '-v' in sys.argv else logging.CRITICAL, | 359 level=logging.DEBUG if '-v' in sys.argv else logging.CRITICAL, |
| 576 format='%(levelname)-7s %(filename)s:%(lineno)3d %(message)s') | 360 format='%(levelname)-7s %(filename)s:%(lineno)3d %(message)s') |
| 577 unittest.main() | 361 unittest.main() |
| OLD | NEW |