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