github.com/dustinrc/deis@v1.10.1-0.20150917223407-0894a5fb979e/controller/api/tests/test_config.py (about) 1 # -*- coding: utf-8 -*- 2 """ 3 Unit tests for the Deis api app. 4 5 Run the tests with "./manage.py test api" 6 """ 7 8 from __future__ import unicode_literals 9 10 import json 11 import logging 12 import mock 13 import requests 14 15 from django.contrib.auth.models import User 16 from django.test import TransactionTestCase 17 import etcd 18 from rest_framework.authtoken.models import Token 19 20 import api.exceptions 21 from api.models import App, Config 22 23 24 def mock_status_ok(*args, **kwargs): 25 resp = requests.Response() 26 resp.status_code = 200 27 resp._content_consumed = True 28 return resp 29 30 31 def mock_status_not_found(*args, **kwargs): 32 resp = requests.Response() 33 resp.status_code = 404 34 resp._content_consumed = True 35 return resp 36 37 38 def mock_request_connection_error(*args, **kwargs): 39 raise requests.exceptions.ConnectionError("connection error") 40 41 42 class MockEtcdClient: 43 44 def __init__(self, app): 45 self.app = app 46 47 def get(self, key, *args, **kwargs): 48 node = { 49 'key': '/deis/services/{}/{}_v2.web.1'.format(self.app, self.app), 50 'value': '127.0.0.1:1234' 51 } 52 return etcd.EtcdResult(None, node) 53 54 55 class ConfigTest(TransactionTestCase): 56 57 """Tests setting and updating config values""" 58 59 fixtures = ['tests.json'] 60 61 def setUp(self): 62 self.user = User.objects.get(username='autotest') 63 self.token = Token.objects.get(user=self.user).key 64 url = '/v1/apps' 65 response = self.client.post(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 66 self.assertEqual(response.status_code, 201) 67 self.app = App.objects.all()[0] 68 69 @mock.patch('requests.post', mock_status_ok) 70 def test_config(self): 71 """ 72 Test that config is auto-created for a new app and that 73 config can be updated using a PATCH 74 """ 75 url = '/v1/apps' 76 response = self.client.post(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 77 self.assertEqual(response.status_code, 201) 78 app_id = response.data['id'] 79 # check to see that an initial/empty config was created 80 url = "/v1/apps/{app_id}/config".format(**locals()) 81 response = self.client.get(url, 82 HTTP_AUTHORIZATION='token {}'.format(self.token)) 83 self.assertEqual(response.status_code, 200) 84 self.assertIn('values', response.data) 85 self.assertEqual(response.data['values'], {}) 86 config1 = response.data 87 # set an initial config value 88 body = {'values': json.dumps({'NEW_URL1': 'http://localhost:8080/'})} 89 response = self.client.post(url, json.dumps(body), content_type='application/json', 90 HTTP_AUTHORIZATION='token {}'.format(self.token)) 91 self.assertEqual(response.status_code, 201) 92 config2 = response.data 93 self.assertNotEqual(config1['uuid'], config2['uuid']) 94 self.assertIn('NEW_URL1', response.data['values']) 95 # read the config 96 response = self.client.get(url, 97 HTTP_AUTHORIZATION='token {}'.format(self.token)) 98 self.assertEqual(response.status_code, 200) 99 config3 = response.data 100 self.assertEqual(config2, config3) 101 self.assertIn('NEW_URL1', response.data['values']) 102 # set an additional config value 103 body = {'values': json.dumps({'NEW_URL2': 'http://localhost:8080/'})} 104 response = self.client.post(url, json.dumps(body), content_type='application/json', 105 HTTP_AUTHORIZATION='token {}'.format(self.token)) 106 self.assertEqual(response.status_code, 201) 107 config3 = response.data 108 self.assertNotEqual(config2['uuid'], config3['uuid']) 109 self.assertIn('NEW_URL1', response.data['values']) 110 self.assertIn('NEW_URL2', response.data['values']) 111 # read the config again 112 response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 113 self.assertEqual(response.status_code, 200) 114 config4 = response.data 115 self.assertEqual(config3, config4) 116 self.assertIn('NEW_URL1', response.data['values']) 117 self.assertIn('NEW_URL2', response.data['values']) 118 # unset a config value 119 body = {'values': json.dumps({'NEW_URL2': None})} 120 response = self.client.post(url, json.dumps(body), content_type='application/json', 121 HTTP_AUTHORIZATION='token {}'.format(self.token)) 122 self.assertEqual(response.status_code, 201) 123 config5 = response.data 124 self.assertNotEqual(config4['uuid'], config5['uuid']) 125 self.assertNotIn('NEW_URL2', json.dumps(response.data['values'])) 126 # unset all config values 127 body = {'values': json.dumps({'NEW_URL1': None})} 128 response = self.client.post(url, json.dumps(body), content_type='application/json', 129 HTTP_AUTHORIZATION='token {}'.format(self.token)) 130 self.assertEqual(response.status_code, 201) 131 self.assertNotIn('NEW_URL1', json.dumps(response.data['values'])) 132 # disallow put/patch/delete 133 response = self.client.put(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 134 self.assertEqual(response.status_code, 405) 135 response = self.client.patch(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 136 self.assertEqual(response.status_code, 405) 137 response = self.client.delete(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 138 self.assertEqual(response.status_code, 405) 139 return config5 140 141 @mock.patch('requests.post', mock_status_ok) 142 def test_response_data(self): 143 """Test that the serialized response contains only relevant data.""" 144 body = {'id': 'test'} 145 response = self.client.post('/v1/apps', json.dumps(body), 146 content_type='application/json', 147 HTTP_AUTHORIZATION='token {}'.format(self.token)) 148 url = "/v1/apps/test/config" 149 # set an initial config value 150 body = {'values': json.dumps({'PORT': '5000'})} 151 response = self.client.post(url, json.dumps(body), content_type='application/json', 152 HTTP_AUTHORIZATION='token {}'.format(self.token)) 153 for key in response.data: 154 self.assertIn(key, ['uuid', 'owner', 'created', 'updated', 'app', 'values', 'memory', 155 'cpu', 'tags']) 156 expected = { 157 'owner': self.user.username, 158 'app': 'test', 159 'values': {'PORT': '5000'}, 160 'memory': {}, 161 'cpu': {}, 162 'tags': {} 163 } 164 self.assertDictContainsSubset(expected, response.data) 165 166 @mock.patch('requests.post', mock_status_ok) 167 def test_response_data_types_converted(self): 168 """Test that config data is converted into the correct type.""" 169 body = {'id': 'test'} 170 response = self.client.post('/v1/apps', json.dumps(body), 171 content_type='application/json', 172 HTTP_AUTHORIZATION='token {}'.format(self.token)) 173 url = "/v1/apps/test/config" 174 175 body = {'values': json.dumps({'PORT': 5000}), 'cpu': json.dumps({'web': '1024'})} 176 response = self.client.post(url, json.dumps(body), content_type='application/json', 177 HTTP_AUTHORIZATION='token {}'.format(self.token)) 178 self.assertEqual(response.status_code, 201) 179 for key in response.data: 180 self.assertIn(key, ['uuid', 'owner', 'created', 'updated', 'app', 'values', 'memory', 181 'cpu', 'tags']) 182 expected = { 183 'owner': self.user.username, 184 'app': 'test', 185 'values': {'PORT': '5000'}, 186 'memory': {}, 187 'cpu': {'web': 1024}, 188 'tags': {} 189 } 190 self.assertDictContainsSubset(expected, response.data) 191 192 body = {'cpu': json.dumps({'web': 'this will fail'})} 193 response = self.client.post(url, json.dumps(body), content_type='application/json', 194 HTTP_AUTHORIZATION='token {}'.format(self.token)) 195 self.assertEqual(response.status_code, 400) 196 self.assertIn('CPU shares must be an integer', response.data['cpu']) 197 198 @mock.patch('requests.post', mock_status_ok) 199 def test_config_set_same_key(self): 200 """ 201 Test that config sets on the same key function properly 202 """ 203 url = '/v1/apps' 204 response = self.client.post(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 205 self.assertEqual(response.status_code, 201) 206 app_id = response.data['id'] 207 url = "/v1/apps/{app_id}/config".format(**locals()) 208 # set an initial config value 209 body = {'values': json.dumps({'PORT': '5000'})} 210 response = self.client.post(url, json.dumps(body), content_type='application/json', 211 HTTP_AUTHORIZATION='token {}'.format(self.token)) 212 self.assertEqual(response.status_code, 201) 213 self.assertIn('PORT', response.data['values']) 214 # reset same config value 215 body = {'values': json.dumps({'PORT': '5001'})} 216 response = self.client.post(url, json.dumps(body), content_type='application/json', 217 HTTP_AUTHORIZATION='token {}'.format(self.token)) 218 self.assertEqual(response.status_code, 201) 219 self.assertIn('PORT', response.data['values']) 220 self.assertEqual(response.data['values']['PORT'], '5001') 221 222 @mock.patch('requests.post', mock_status_ok) 223 def test_config_set_unicode(self): 224 """ 225 Test that config sets with unicode values are accepted. 226 """ 227 url = '/v1/apps' 228 response = self.client.post(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 229 self.assertEqual(response.status_code, 201) 230 app_id = response.data['id'] 231 url = "/v1/apps/{app_id}/config".format(**locals()) 232 # set an initial config value 233 body = {'values': json.dumps({'POWERED_BY': 'Деис'})} 234 response = self.client.post(url, json.dumps(body), content_type='application/json', 235 HTTP_AUTHORIZATION='token {}'.format(self.token)) 236 self.assertEqual(response.status_code, 201) 237 self.assertIn('POWERED_BY', response.data['values']) 238 # reset same config value 239 body = {'values': json.dumps({'POWERED_BY': 'Кроликов'})} 240 response = self.client.post(url, json.dumps(body), content_type='application/json', 241 HTTP_AUTHORIZATION='token {}'.format(self.token)) 242 self.assertEqual(response.status_code, 201) 243 self.assertIn('POWERED_BY', response.data['values']) 244 self.assertEqual(response.data['values']['POWERED_BY'], 'Кроликов') 245 # set an integer to test unicode regression 246 body = {'values': json.dumps({'INTEGER': 1})} 247 response = self.client.post(url, json.dumps(body), content_type='application/json', 248 HTTP_AUTHORIZATION='token {}'.format(self.token)) 249 self.assertEqual(response.status_code, 201) 250 self.assertIn('INTEGER', response.data['values']) 251 self.assertEqual(response.data['values']['INTEGER'], '1') 252 253 @mock.patch('requests.post', mock_status_ok) 254 def test_config_str(self): 255 """Test the text representation of a node.""" 256 config5 = self.test_config() 257 config = Config.objects.get(uuid=config5['uuid']) 258 self.assertEqual(str(config), "{}-{}".format(config5['app'], config5['uuid'][:7])) 259 260 @mock.patch('requests.post', mock_status_ok) 261 def test_admin_can_create_config_on_other_apps(self): 262 """If a non-admin creates an app, an administrator should be able to set config 263 values for that app. 264 """ 265 user = User.objects.get(username='autotest2') 266 token = Token.objects.get(user=user).key 267 url = '/v1/apps' 268 response = self.client.post(url, HTTP_AUTHORIZATION='token {}'.format(token)) 269 self.assertEqual(response.status_code, 201) 270 app_id = response.data['id'] 271 url = "/v1/apps/{app_id}/config".format(**locals()) 272 # set an initial config value 273 body = {'values': json.dumps({'PORT': '5000'})} 274 response = self.client.post(url, json.dumps(body), content_type='application/json', 275 HTTP_AUTHORIZATION='token {}'.format(self.token)) 276 self.assertEqual(response.status_code, 201) 277 self.assertIn('PORT', response.data['values']) 278 return response 279 280 @mock.patch('requests.post', mock_status_ok) 281 def test_limit_memory(self): 282 """ 283 Test that limit is auto-created for a new app and that 284 limits can be updated using a PATCH 285 """ 286 url = '/v1/apps' 287 response = self.client.post(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 288 self.assertEqual(response.status_code, 201) 289 app_id = response.data['id'] 290 url = '/v1/apps/{app_id}/config'.format(**locals()) 291 # check default limit 292 response = self.client.get(url, content_type='application/json', 293 HTTP_AUTHORIZATION='token {}'.format(self.token)) 294 self.assertEqual(response.status_code, 200) 295 self.assertIn('memory', response.data) 296 self.assertEqual(response.data['memory'], {}) 297 # regression test for https://github.com/deis/deis/issues/1563 298 self.assertNotIn('"', response.data['memory']) 299 # set an initial limit 300 mem = {'web': '1G'} 301 body = {'memory': json.dumps(mem)} 302 response = self.client.post(url, json.dumps(body), content_type='application/json', 303 HTTP_AUTHORIZATION='token {}'.format(self.token)) 304 self.assertEqual(response.status_code, 201) 305 limit1 = response.data 306 # check memory limits 307 response = self.client.get(url, content_type='application/json', 308 HTTP_AUTHORIZATION='token {}'.format(self.token)) 309 self.assertEqual(response.status_code, 200) 310 self.assertIn('memory', response.data) 311 memory = response.data['memory'] 312 self.assertIn('web', memory) 313 self.assertEqual(memory['web'], '1G') 314 # set an additional value 315 body = {'memory': json.dumps({'worker': '512M'})} 316 response = self.client.post(url, json.dumps(body), content_type='application/json', 317 HTTP_AUTHORIZATION='token {}'.format(self.token)) 318 self.assertEqual(response.status_code, 201) 319 limit2 = response.data 320 self.assertNotEqual(limit1['uuid'], limit2['uuid']) 321 memory = response.data['memory'] 322 self.assertIn('worker', memory) 323 self.assertEqual(memory['worker'], '512M') 324 self.assertIn('web', memory) 325 self.assertEqual(memory['web'], '1G') 326 # read the limit again 327 response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 328 self.assertEqual(response.status_code, 200) 329 limit3 = response.data 330 self.assertEqual(limit2, limit3) 331 memory = response.data['memory'] 332 self.assertIn('worker', memory) 333 self.assertEqual(memory['worker'], '512M') 334 self.assertIn('web', memory) 335 self.assertEqual(memory['web'], '1G') 336 # regression test for https://github.com/deis/deis/issues/1613 337 # ensure that config:set doesn't wipe out previous limits 338 body = {'values': json.dumps({'NEW_URL2': 'http://localhost:8080/'})} 339 response = self.client.post(url, json.dumps(body), content_type='application/json', 340 HTTP_AUTHORIZATION='token {}'.format(self.token)) 341 self.assertEqual(response.status_code, 201) 342 self.assertIn('NEW_URL2', response.data['values']) 343 # read the limit again 344 response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 345 self.assertEqual(response.status_code, 200) 346 memory = response.data['memory'] 347 self.assertIn('worker', memory) 348 self.assertEqual(memory['worker'], '512M') 349 self.assertIn('web', memory) 350 self.assertEqual(memory['web'], '1G') 351 # unset a value 352 body = {'memory': json.dumps({'worker': None})} 353 response = self.client.post(url, json.dumps(body), content_type='application/json', 354 HTTP_AUTHORIZATION='token {}'.format(self.token)) 355 self.assertEqual(response.status_code, 201) 356 limit4 = response.data 357 self.assertNotEqual(limit3['uuid'], limit4['uuid']) 358 self.assertNotIn('worker', json.dumps(response.data['memory'])) 359 # disallow put/patch/delete 360 response = self.client.put(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 361 self.assertEqual(response.status_code, 405) 362 response = self.client.patch(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 363 self.assertEqual(response.status_code, 405) 364 response = self.client.delete(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 365 self.assertEqual(response.status_code, 405) 366 return limit4 367 368 @mock.patch('requests.post', mock_status_ok) 369 def test_limit_cpu(self): 370 """ 371 Test that CPU limits can be set 372 """ 373 url = '/v1/apps' 374 response = self.client.post(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 375 self.assertEqual(response.status_code, 201) 376 app_id = response.data['id'] 377 url = '/v1/apps/{app_id}/config'.format(**locals()) 378 # check default limit 379 response = self.client.get(url, content_type='application/json', 380 HTTP_AUTHORIZATION='token {}'.format(self.token)) 381 self.assertEqual(response.status_code, 200) 382 self.assertIn('cpu', response.data) 383 self.assertEqual(response.data['cpu'], {}) 384 # regression test for https://github.com/deis/deis/issues/1563 385 self.assertNotIn('"', response.data['cpu']) 386 # set an initial limit 387 body = {'cpu': json.dumps({'web': '1024'})} 388 response = self.client.post(url, json.dumps(body), content_type='application/json', 389 HTTP_AUTHORIZATION='token {}'.format(self.token)) 390 self.assertEqual(response.status_code, 201) 391 limit1 = response.data 392 # check memory limits 393 response = self.client.get(url, content_type='application/json', 394 HTTP_AUTHORIZATION='token {}'.format(self.token)) 395 self.assertEqual(response.status_code, 200) 396 self.assertIn('cpu', response.data) 397 cpu = response.data['cpu'] 398 self.assertIn('web', cpu) 399 self.assertEqual(cpu['web'], 1024) 400 # set an additional value 401 body = {'cpu': json.dumps({'worker': '512'})} 402 response = self.client.post(url, json.dumps(body), content_type='application/json', 403 HTTP_AUTHORIZATION='token {}'.format(self.token)) 404 self.assertEqual(response.status_code, 201) 405 limit2 = response.data 406 self.assertNotEqual(limit1['uuid'], limit2['uuid']) 407 cpu = response.data['cpu'] 408 self.assertIn('worker', cpu) 409 self.assertEqual(cpu['worker'], 512) 410 self.assertIn('web', cpu) 411 self.assertEqual(cpu['web'], 1024) 412 # read the limit again 413 response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 414 self.assertEqual(response.status_code, 200) 415 limit3 = response.data 416 self.assertEqual(limit2, limit3) 417 cpu = response.data['cpu'] 418 self.assertIn('worker', cpu) 419 self.assertEqual(cpu['worker'], 512) 420 self.assertIn('web', cpu) 421 self.assertEqual(cpu['web'], 1024) 422 # unset a value 423 body = {'memory': json.dumps({'worker': None})} 424 response = self.client.post(url, json.dumps(body), content_type='application/json', 425 HTTP_AUTHORIZATION='token {}'.format(self.token)) 426 self.assertEqual(response.status_code, 201) 427 limit4 = response.data 428 self.assertNotEqual(limit3['uuid'], limit4['uuid']) 429 self.assertNotIn('worker', json.dumps(response.data['memory'])) 430 # disallow put/patch/delete 431 response = self.client.put(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 432 self.assertEqual(response.status_code, 405) 433 response = self.client.patch(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 434 self.assertEqual(response.status_code, 405) 435 response = self.client.delete(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 436 self.assertEqual(response.status_code, 405) 437 return limit4 438 439 @mock.patch('requests.post', mock_status_ok) 440 def test_tags(self): 441 """ 442 Test that tags can be set on an application 443 """ 444 url = '/v1/apps' 445 response = self.client.post(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 446 self.assertEqual(response.status_code, 201) 447 app_id = response.data['id'] 448 url = '/v1/apps/{app_id}/config'.format(**locals()) 449 # check default 450 response = self.client.get(url, content_type='application/json', 451 HTTP_AUTHORIZATION='token {}'.format(self.token)) 452 self.assertEqual(response.status_code, 200) 453 self.assertIn('tags', response.data) 454 self.assertEqual(response.data['tags'], {}) 455 # set some tags 456 body = {'tags': json.dumps({'environ': 'dev'})} 457 response = self.client.post(url, json.dumps(body), content_type='application/json', 458 HTTP_AUTHORIZATION='token {}'.format(self.token)) 459 self.assertEqual(response.status_code, 201) 460 tags1 = response.data 461 # check tags again 462 response = self.client.get(url, content_type='application/json', 463 HTTP_AUTHORIZATION='token {}'.format(self.token)) 464 self.assertEqual(response.status_code, 200) 465 self.assertIn('tags', response.data) 466 tags = response.data['tags'] 467 self.assertIn('environ', tags) 468 self.assertEqual(tags['environ'], 'dev') 469 # set an additional value 470 body = {'tags': json.dumps({'rack': '1'})} 471 response = self.client.post(url, json.dumps(body), content_type='application/json', 472 HTTP_AUTHORIZATION='token {}'.format(self.token)) 473 self.assertEqual(response.status_code, 201) 474 tags2 = response.data 475 self.assertNotEqual(tags1['uuid'], tags2['uuid']) 476 tags = response.data['tags'] 477 self.assertIn('rack', tags) 478 self.assertEqual(tags['rack'], '1') 479 self.assertIn('environ', tags) 480 self.assertEqual(tags['environ'], 'dev') 481 # read the limit again 482 response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 483 self.assertEqual(response.status_code, 200) 484 tags3 = response.data 485 self.assertEqual(tags2, tags3) 486 tags = response.data['tags'] 487 self.assertIn('rack', tags) 488 self.assertEqual(tags['rack'], '1') 489 self.assertIn('environ', tags) 490 self.assertEqual(tags['environ'], 'dev') 491 # unset a value 492 body = {'tags': json.dumps({'rack': None})} 493 response = self.client.post(url, json.dumps(body), content_type='application/json', 494 HTTP_AUTHORIZATION='token {}'.format(self.token)) 495 self.assertEqual(response.status_code, 201) 496 tags4 = response.data 497 self.assertNotEqual(tags3['uuid'], tags4['uuid']) 498 self.assertNotIn('rack', json.dumps(response.data['tags'])) 499 # set invalid values 500 body = {'tags': json.dumps({'valid': 'in\nvalid'})} 501 response = self.client.post(url, json.dumps(body), content_type='application/json', 502 HTTP_AUTHORIZATION='token {}'.format(self.token)) 503 self.assertEqual(response.status_code, 400) 504 body = {'tags': json.dumps({'in.valid': 'valid'})} 505 response = self.client.post(url, json.dumps(body), content_type='application/json', 506 HTTP_AUTHORIZATION='token {}'.format(self.token)) 507 self.assertEqual(response.status_code, 400) 508 # disallow put/patch/delete 509 response = self.client.put(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 510 self.assertEqual(response.status_code, 405) 511 response = self.client.patch(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 512 self.assertEqual(response.status_code, 405) 513 response = self.client.delete(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 514 self.assertEqual(response.status_code, 405) 515 516 def test_config_owner_is_requesting_user(self): 517 """ 518 Ensure that setting the config value is owned by the requesting user 519 See https://github.com/deis/deis/issues/2650 520 """ 521 response = self.test_admin_can_create_config_on_other_apps() 522 self.assertEqual(response.data['owner'], self.user.username) 523 524 def test_unauthorized_user_cannot_modify_config(self): 525 """ 526 An unauthorized user should not be able to modify other config. 527 528 Since an unauthorized user can't access the application, these 529 requests should return a 403. 530 """ 531 app_id = 'autotest' 532 base_url = '/v1/apps' 533 body = {'id': app_id} 534 response = self.client.post(base_url, json.dumps(body), content_type='application/json', 535 HTTP_AUTHORIZATION='token {}'.format(self.token)) 536 unauthorized_user = User.objects.get(username='autotest2') 537 unauthorized_token = Token.objects.get(user=unauthorized_user).key 538 url = '{}/{}/config'.format(base_url, app_id) 539 body = {'values': {'FOO': 'bar'}} 540 response = self.client.post(url, json.dumps(body), content_type='application/json', 541 HTTP_AUTHORIZATION='token {}'.format(unauthorized_token)) 542 self.assertEqual(response.status_code, 403) 543 544 def _test_app_healthcheck(self): 545 # post a new build, expecting it to pass as usual 546 url = "/v1/apps/{self.app}/builds".format(**locals()) 547 body = {'image': 'autotest/example'} 548 response = self.client.post(url, json.dumps(body), content_type='application/json', 549 HTTP_AUTHORIZATION='token {}'.format(self.token)) 550 self.assertEqual(response.status_code, 201) 551 # mock out the etcd client 552 api.models._etcd_client = MockEtcdClient(self.app) 553 # set an initial healthcheck url. 554 url = "/v1/apps/{self.app}/config".format(**locals()) 555 body = {'values': json.dumps({'HEALTHCHECK_URL': '/'})} 556 return self.client.post(url, json.dumps(body), content_type='application/json', 557 HTTP_AUTHORIZATION='token {}'.format(self.token)) 558 559 @mock.patch('requests.get', mock_status_ok) 560 @mock.patch('time.sleep', lambda func: func) 561 def test_app_healthcheck_good(self): 562 """ 563 If a user deploys an app with a config value set for HEALTHCHECK_URL, the controller 564 should check that the application responds with a 200 OK. 565 """ 566 response = self._test_app_healthcheck() 567 self.assertEqual(response.status_code, 201) 568 self.assertEqual(self.app.release_set.latest().version, 3) 569 570 @mock.patch('requests.get', mock_status_not_found) 571 @mock.patch('api.models.get_etcd_client', lambda func: func) 572 @mock.patch('time.sleep', lambda func: func) 573 @mock.patch('api.models.logger') 574 def test_app_healthcheck_bad(self, mock_logger): 575 """ 576 If a user deploys an app with a config value set for HEALTHCHECK_URL, the controller 577 should check that the application responds with a 200 OK. If it's down, the app should be 578 rolled back. 579 """ 580 response = self._test_app_healthcheck() 581 self.assertEqual(response.status_code, 503) 582 self.assertEqual( 583 response.data, 584 {'detail': 'aborting, app containers failed to respond to health check'}) 585 # check that only the build and initial release exist 586 self.assertEqual(self.app.release_set.latest().version, 2) 587 # assert that the reason why the containers failed was because 588 # they failed the health check 4 times; we do this by looking 589 # at logs-- there may be a better way 590 exp_msg = "{}: app failed health check (got '404', expected: '200'); trying again in 0.0 \ 591 seconds".format(self.app.id) 592 exp_log_call = mock.call(logging.WARNING, exp_msg) 593 log_calls = mock_logger.log.mock_calls 594 self.assertEqual(log_calls.count(exp_log_call), 3) 595 exp_msg = "{}: app failed health check (got '404', expected: '200')".format(self.app.id) 596 exp_log_call = mock.call(logging.WARNING, exp_msg) 597 self.assertEqual(log_calls.count(exp_log_call), 1) 598 599 @mock.patch('requests.get', mock_status_not_found) 600 @mock.patch('api.models.get_etcd_client', lambda func: func) 601 @mock.patch('time.sleep') 602 def test_app_backoff_interval(self, mock_time): 603 """ 604 Ensure that when a healthcheck fails, a backoff strategy is used before trying again. 605 """ 606 # post a new build, expecting it to pass as usual 607 url = "/v1/apps/{self.app}/builds".format(**locals()) 608 body = {'image': 'autotest/example'} 609 response = self.client.post(url, json.dumps(body), content_type='application/json', 610 HTTP_AUTHORIZATION='token {}'.format(self.token)) 611 self.assertEqual(response.status_code, 201) 612 # mock out the etcd client 613 api.models._etcd_client = MockEtcdClient(self.app) 614 # set an initial healthcheck url. 615 url = "/v1/apps/{self.app}/config".format(**locals()) 616 body = {'values': json.dumps({'HEALTHCHECK_URL': '/'})} 617 return self.client.post(url, json.dumps(body), content_type='application/json', 618 HTTP_AUTHORIZATION='token {}'.format(self.token)) 619 self.assertEqual(mock_time.call_count, 5) 620 621 @mock.patch('requests.get', mock_status_ok) 622 @mock.patch('time.sleep') 623 def test_app_healthcheck_initial_delay(self, mock_time): 624 """ 625 Ensure that when an initial delay is set, the request will sleep for x seconds, where 626 x is the number of seconds in the initial timeout. 627 """ 628 # post a new build, expecting it to pass as usual 629 url = "/v1/apps/{self.app}/builds".format(**locals()) 630 body = {'image': 'autotest/example'} 631 response = self.client.post(url, json.dumps(body), content_type='application/json', 632 HTTP_AUTHORIZATION='token {}'.format(self.token)) 633 self.assertEqual(response.status_code, 201) 634 # mock out the etcd client 635 api.models._etcd_client = MockEtcdClient(self.app) 636 # set an initial healthcheck url. 637 url = "/v1/apps/{self.app}/config".format(**locals()) 638 body = {'values': json.dumps({'HEALTHCHECK_URL': '/'})} 639 return self.client.post(url, json.dumps(body), content_type='application/json', 640 HTTP_AUTHORIZATION='token {}'.format(self.token)) 641 # mock_time increments by one each time its called, so we should expect 2 calls to 642 # mock_time; one for the call in the code, and one for this invocation. 643 mock_time.assert_called_with(0) 644 app = App.objects.all()[0] 645 url = "/v1/apps/{app}/config".format(**locals()) 646 body = {'values': json.dumps({'HEALTHCHECK_INITIAL_DELAY': 10})} 647 self.client.post(url, json.dumps(body), content_type='application/json', 648 HTTP_AUTHORIZATION='token {}'.format(self.token)) 649 mock_time.assert_called_with(10) 650 651 @mock.patch('requests.get') 652 @mock.patch('time.sleep', lambda func: func) 653 def test_app_healthcheck_timeout(self, mock_request): 654 """ 655 Ensure when a timeout value is set, the controller respects that value 656 when making a request. 657 """ 658 self._test_app_healthcheck() 659 app = App.objects.all()[0] 660 url = "/v1/apps/{app}/config".format(**locals()) 661 body = {'values': json.dumps({'HEALTHCHECK_TIMEOUT': 10})} 662 self.client.post(url, json.dumps(body), content_type='application/json', 663 HTTP_AUTHORIZATION='token {}'.format(self.token)) 664 mock_request.assert_called_with('http://127.0.0.1:1234/', timeout=10) 665 666 @mock.patch('requests.get', mock_request_connection_error) 667 @mock.patch('time.sleep', lambda func: func) 668 def test_app_healthcheck_connection_error(self): 669 """ 670 If a user deploys an app with a config value set for HEALTHCHECK_URL but the app 671 returns a connection error, the controller should continue checking until either the app 672 responds or the app fails to respond within the timeout. 673 674 NOTE (bacongobbler): the Docker userland proxy listens for connections and returns a 675 ConnectionError, hence the unit test. 676 """ 677 response = self._test_app_healthcheck() 678 self.assertEqual(response.status_code, 503) 679 self.assertEqual( 680 response.data, 681 {'detail': 'aborting, app containers failed to respond to health check'})