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