github.com/dustinrc/deis@v1.10.1-0.20150917223407-0894a5fb979e/controller/api/tests/test_config.py (about)

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