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'})