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