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