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

     1  """
     2  Unit tests for the Deis api app.
     3  
     4  Run the tests with "./manage.py test api"
     5  """
     6  
     7  from __future__ import unicode_literals
     8  
     9  import json
    10  import logging
    11  import mock
    12  import os.path
    13  import requests
    14  
    15  from django.conf import settings
    16  from django.contrib.auth.models import User
    17  from django.test import TestCase
    18  from rest_framework.authtoken.models import Token
    19  
    20  from api.models import App
    21  
    22  
    23  def mock_import_repository_task(*args, **kwargs):
    24      resp = requests.Response()
    25      resp.status_code = 200
    26      resp._content_consumed = True
    27      return resp
    28  
    29  
    30  class AppTest(TestCase):
    31      """Tests creation of applications"""
    32  
    33      fixtures = ['tests.json']
    34  
    35      def setUp(self):
    36          self.user = User.objects.get(username='autotest')
    37          self.token = Token.objects.get(user=self.user).key
    38          # provide mock authentication used for run commands
    39          settings.SSH_PRIVATE_KEY = '<some-ssh-private-key>'
    40  
    41      def tearDown(self):
    42          # reset global vars for other tests
    43          settings.SSH_PRIVATE_KEY = ''
    44  
    45      def test_app(self):
    46          """
    47          Test that a user can create, read, update and delete an application
    48          """
    49          url = '/v1/apps'
    50          response = self.client.post(url, HTTP_AUTHORIZATION='token {}'.format(self.token))
    51          self.assertEqual(response.status_code, 201)
    52          app_id = response.data['id']  # noqa
    53          self.assertIn('id', response.data)
    54          response = self.client.get('/v1/apps',
    55                                     HTTP_AUTHORIZATION='token {}'.format(self.token))
    56          self.assertEqual(response.status_code, 200)
    57          self.assertEqual(len(response.data['results']), 1)
    58          url = '/v1/apps/{app_id}'.format(**locals())
    59          response = self.client.get(url,
    60                                     HTTP_AUTHORIZATION='token {}'.format(self.token))
    61          self.assertEqual(response.status_code, 200)
    62          body = {'id': 'new'}
    63          response = self.client.patch(url, json.dumps(body), content_type='application/json',
    64                                       HTTP_AUTHORIZATION='token {}'.format(self.token))
    65          self.assertEqual(response.status_code, 405)
    66          response = self.client.delete(url,
    67                                        HTTP_AUTHORIZATION='token {}'.format(self.token))
    68          self.assertEqual(response.status_code, 204)
    69  
    70      def test_response_data(self):
    71          """Test that the serialized response contains only relevant data."""
    72          body = {'id': 'test'}
    73          response = self.client.post('/v1/apps', json.dumps(body),
    74                                      content_type='application/json',
    75                                      HTTP_AUTHORIZATION='token {}'.format(self.token))
    76          for key in response.data:
    77              self.assertIn(key, ['uuid', 'created', 'updated', 'id', 'owner', 'url', 'structure'])
    78          expected = {
    79              'id': 'test',
    80              'owner': self.user.username,
    81              'url': 'test.deisapp.local',
    82              'structure': {}
    83          }
    84          self.assertDictContainsSubset(expected, response.data)
    85  
    86      def test_app_override_id(self):
    87          body = {'id': 'myid'}
    88          response = self.client.post('/v1/apps', json.dumps(body),
    89                                      content_type='application/json',
    90                                      HTTP_AUTHORIZATION='token {}'.format(self.token))
    91          self.assertEqual(response.status_code, 201)
    92          body = {'id': response.data['id']}
    93          response = self.client.post('/v1/apps', json.dumps(body),
    94                                      content_type='application/json',
    95                                      HTTP_AUTHORIZATION='token {}'.format(self.token))
    96          self.assertContains(response, 'This field must be unique.', status_code=400)
    97          return response
    98  
    99      def test_app_actions(self):
   100          url = '/v1/apps'
   101          body = {'id': 'autotest'}
   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          app_id = response.data['id']  # noqa
   106          # test logs
   107          if not os.path.exists(settings.DEIS_LOG_DIR):
   108              os.mkdir(settings.DEIS_LOG_DIR)
   109          path = os.path.join(settings.DEIS_LOG_DIR, app_id + '.log')
   110          # HACK: remove app lifecycle logs
   111          if os.path.exists(path):
   112              os.remove(path)
   113          url = '/v1/apps/{app_id}/logs'.format(**locals())
   114          response = self.client.get(url,
   115                                     HTTP_AUTHORIZATION='token {}'.format(self.token))
   116          self.assertEqual(response.status_code, 204)
   117          self.assertEqual(response.data, 'No logs for {}'.format(app_id))
   118          # write out some fake log data and try again
   119          with open(path, 'a') as f:
   120              f.write(FAKE_LOG_DATA)
   121          response = self.client.get(url,
   122                                     HTTP_AUTHORIZATION='token {}'.format(self.token))
   123          self.assertEqual(response.status_code, 200)
   124          self.assertEqual(response.data, FAKE_LOG_DATA)
   125  
   126          # test with log_lines
   127          response = self.client.get(url + "?log_lines=1",
   128                                     HTTP_AUTHORIZATION='token {}'.format(self.token))
   129          self.assertEqual(response.status_code, 200)
   130          self.assertEqual(response.data, FAKE_LOG_DATA.splitlines(True)[4])
   131  
   132          os.remove(path)
   133          # TODO: test run needs an initial build
   134  
   135      @mock.patch('api.models.logger')
   136      def test_app_release_notes_in_logs(self, mock_logger):
   137          """Verifies that an app's release summary is dumped into the logs."""
   138          url = '/v1/apps'
   139          body = {'id': 'autotest'}
   140          response = self.client.post(url, json.dumps(body), content_type='application/json',
   141                                      HTTP_AUTHORIZATION='token {}'.format(self.token))
   142          self.assertEqual(response.status_code, 201)
   143          # check app logs
   144          exp_msg = "autotest created initial release"
   145          exp_log_call = mock.call(logging.INFO, exp_msg)
   146          mock_logger.log.has_calls(exp_log_call)
   147  
   148      def test_app_errors(self):
   149          app_id = 'autotest-errors'
   150          url = '/v1/apps'
   151          body = {'id': 'camelCase'}
   152          response = self.client.post(url, json.dumps(body), content_type='application/json',
   153                                      HTTP_AUTHORIZATION='token {}'.format(self.token))
   154          self.assertContains(response, 'App IDs can only contain [a-z0-9-]', status_code=400)
   155          url = '/v1/apps'
   156          body = {'id': app_id}
   157          response = self.client.post(url, json.dumps(body), content_type='application/json',
   158                                      HTTP_AUTHORIZATION='token {}'.format(self.token))
   159          self.assertEqual(response.status_code, 201)
   160          app_id = response.data['id']  # noqa
   161          url = '/v1/apps/{app_id}'.format(**locals())
   162          response = self.client.delete(url,
   163                                        HTTP_AUTHORIZATION='token {}'.format(self.token))
   164          self.assertEquals(response.status_code, 204)
   165          for endpoint in ('containers', 'config', 'releases', 'builds'):
   166              url = '/v1/apps/{app_id}/{endpoint}'.format(**locals())
   167              response = self.client.get(url,
   168                                         HTTP_AUTHORIZATION='token {}'.format(self.token))
   169              self.assertEquals(response.status_code, 404)
   170  
   171      def test_app_reserved_names(self):
   172          """Nobody should be able to create applications with names which are reserved."""
   173          url = '/v1/apps'
   174          reserved_names = ['foo', 'bar']
   175          with self.settings(DEIS_RESERVED_NAMES=reserved_names):
   176              for name in reserved_names:
   177                  body = {'id': name}
   178                  response = self.client.post(url, json.dumps(body), content_type='application/json',
   179                                              HTTP_AUTHORIZATION='token {}'.format(self.token))
   180                  self.assertContains(
   181                      response,
   182                      '{} is a reserved name.'.format(name),
   183                      status_code=400)
   184  
   185      def test_app_structure_is_valid_json(self):
   186          """Application structures should be valid JSON objects."""
   187          url = '/v1/apps'
   188          response = self.client.post(url, HTTP_AUTHORIZATION='token {}'.format(self.token))
   189          self.assertEqual(response.status_code, 201)
   190          app_id = response.data['id']
   191          self.assertIn('structure', response.data)
   192          self.assertEqual(response.data['structure'], {})
   193          app = App.objects.get(id=app_id)
   194          app.structure = {'web': 1}
   195          app.save()
   196          url = '/v1/apps/{}'.format(app_id)
   197          response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(self.token))
   198          self.assertIn('structure', response.data)
   199          self.assertEqual(response.data['structure'], {"web": 1})
   200  
   201      @mock.patch('requests.post', mock_import_repository_task)
   202      @mock.patch('api.models.logger')
   203      def test_admin_can_manage_other_apps(self, mock_logger):
   204          """Administrators of Deis should be able to manage all applications.
   205          """
   206          # log in as non-admin user and create an app
   207          user = User.objects.get(username='autotest2')
   208          token = Token.objects.get(user=user)
   209          app_id = 'autotest'
   210          url = '/v1/apps'
   211          body = {'id': app_id}
   212          response = self.client.post(url, json.dumps(body), content_type='application/json',
   213                                      HTTP_AUTHORIZATION='token {}'.format(token))
   214          # log in as admin, check to see if they have access
   215          url = '/v1/apps/{}'.format(app_id)
   216          response = self.client.get(url,
   217                                     HTTP_AUTHORIZATION='token {}'.format(self.token))
   218          self.assertEqual(response.status_code, 200)
   219          # check app logs
   220          exp_msg = "autotest2 created initial release"
   221          exp_log_call = mock.call(logging.INFO, exp_msg)
   222          mock_logger.log.has_calls(exp_log_call)
   223          # TODO: test run needs an initial build
   224          # delete the app
   225          url = '/v1/apps/{}'.format(app_id)
   226          response = self.client.delete(url,
   227                                        HTTP_AUTHORIZATION='token {}'.format(self.token))
   228          self.assertEqual(response.status_code, 204)
   229  
   230      def test_admin_can_see_other_apps(self):
   231          """If a user creates an application, the administrator should be able
   232          to see it.
   233          """
   234          # log in as non-admin user and create an app
   235          user = User.objects.get(username='autotest2')
   236          token = Token.objects.get(user=user)
   237          app_id = 'autotest'
   238          url = '/v1/apps'
   239          body = {'id': app_id}
   240          response = self.client.post(url, json.dumps(body), content_type='application/json',
   241                                      HTTP_AUTHORIZATION='token {}'.format(token))
   242          # log in as admin
   243          response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(self.token))
   244          self.assertEqual(response.data['count'], 1)
   245  
   246      def test_run_without_auth(self):
   247          """If the administrator has not provided SSH private key for run commands,
   248          make sure a friendly error message is provided on run"""
   249          settings.SSH_PRIVATE_KEY = ''
   250          url = '/v1/apps'
   251          body = {'id': 'autotest'}
   252          response = self.client.post(url, json.dumps(body), content_type='application/json',
   253                                      HTTP_AUTHORIZATION='token {}'.format(self.token))
   254          self.assertEqual(response.status_code, 201)
   255          app_id = response.data['id']  # noqa
   256          # test run
   257          url = '/v1/apps/{app_id}/run'.format(**locals())
   258          body = {'command': 'ls -al'}
   259          response = self.client.post(url, json.dumps(body), content_type='application/json',
   260                                      HTTP_AUTHORIZATION='token {}'.format(self.token))
   261          self.assertEquals(response.status_code, 400)
   262          self.assertEquals(response.data, {'detail': 'Support for admin commands '
   263                                                      'is not configured'})
   264  
   265      def test_run_without_release_should_error(self):
   266          """
   267          A user should not be able to run a one-off command unless a release
   268          is present.
   269          """
   270          app_id = 'autotest'
   271          url = '/v1/apps'
   272          body = {'id': app_id}
   273          response = self.client.post(url, json.dumps(body), content_type='application/json',
   274                                      HTTP_AUTHORIZATION='token {}'.format(self.token))
   275          url = '/v1/apps/{}/run'.format(app_id)
   276          body = {'command': 'ls -al'}
   277          response = self.client.post(url, json.dumps(body), content_type='application/json',
   278                                      HTTP_AUTHORIZATION='token {}'.format(self.token))
   279          self.assertEqual(response.status_code, 400)
   280          self.assertEqual(response.data, {'detail': 'No build associated with this '
   281                                                     'release to run this command'})
   282  
   283      def test_unauthorized_user_cannot_see_app(self):
   284          """
   285          An unauthorized user should not be able to access an app's resources.
   286  
   287          Since an unauthorized user can't access the application, these
   288          tests should return a 403, but currently return a 404. FIXME!
   289          """
   290          app_id = 'autotest'
   291          base_url = '/v1/apps'
   292          body = {'id': app_id}
   293          response = self.client.post(base_url, json.dumps(body), content_type='application/json',
   294                                      HTTP_AUTHORIZATION='token {}'.format(self.token))
   295          unauthorized_user = User.objects.get(username='autotest2')
   296          unauthorized_token = Token.objects.get(user=unauthorized_user).key
   297          url = '{}/{}/run'.format(base_url, app_id)
   298          body = {'command': 'foo'}
   299          response = self.client.post(url, json.dumps(body), content_type='application/json',
   300                                      HTTP_AUTHORIZATION='token {}'.format(unauthorized_token))
   301          self.assertEqual(response.status_code, 403)
   302          url = '{}/{}/logs'.format(base_url, app_id)
   303          response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(unauthorized_token))
   304          self.assertEqual(response.status_code, 403)
   305          url = '{}/{}'.format(base_url, app_id)
   306          response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(unauthorized_token))
   307          self.assertEqual(response.status_code, 403)
   308          response = self.client.delete(url,
   309                                        HTTP_AUTHORIZATION='token {}'.format(unauthorized_token))
   310          self.assertEqual(response.status_code, 403)
   311  
   312      def test_app_info_not_showing_wrong_app(self):
   313          app_id = 'autotest'
   314          base_url = '/v1/apps'
   315          body = {'id': app_id}
   316          response = self.client.post(base_url, json.dumps(body), content_type='application/json',
   317                                      HTTP_AUTHORIZATION='token {}'.format(self.token))
   318          url = '{}/foo'.format(base_url)
   319          response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(self.token))
   320          self.assertEqual(response.status_code, 404)
   321  
   322      def test_app_transfer(self):
   323          owner = User.objects.get(username='autotest2')
   324          owner_token = Token.objects.get(user=owner).key
   325          app_id = 'autotest'
   326          base_url = '/v1/apps'
   327          body = {'id': app_id}
   328          response = self.client.post(base_url, json.dumps(body), content_type='application/json',
   329                                      HTTP_AUTHORIZATION='token {}'.format(owner_token))
   330          # Transfer App
   331          url = '{}/{}'.format(base_url, app_id)
   332          new_owner = User.objects.get(username='autotest3')
   333          new_owner_token = Token.objects.get(user=new_owner).key
   334          body = {'owner': new_owner.username}
   335          response = self.client.post(url, json.dumps(body), content_type='application/json',
   336                                      HTTP_AUTHORIZATION='token {}'.format(owner_token))
   337          self.assertEqual(response.status_code, 200)
   338  
   339          # Original user can no longer access it
   340          response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(owner_token))
   341          self.assertEqual(response.status_code, 403)
   342  
   343          # New owner can access it
   344          response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(new_owner_token))
   345          self.assertEqual(response.status_code, 200)
   346          self.assertEqual(response.data['owner'], new_owner.username)
   347  
   348          # Collaborators can't transfer
   349          body = {'username': owner.username}
   350          perms_url = url+"/perms/"
   351          response = self.client.post(perms_url, json.dumps(body), content_type='application/json',
   352                                      HTTP_AUTHORIZATION='token {}'.format(new_owner_token))
   353          self.assertEqual(response.status_code, 201)
   354          body = {'owner': self.user.username}
   355          response = self.client.post(url, json.dumps(body), content_type='application/json',
   356                                      HTTP_AUTHORIZATION='token {}'.format(owner_token))
   357          self.assertEqual(response.status_code, 403)
   358  
   359          # Admins can transfer
   360          body = {'owner': self.user.username}
   361          response = self.client.post(url, json.dumps(body), content_type='application/json',
   362                                      HTTP_AUTHORIZATION='token {}'.format(self.token))
   363          self.assertEqual(response.status_code, 200)
   364          response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(self.token))
   365          self.assertEqual(response.status_code, 200)
   366          self.assertEqual(response.data['owner'], self.user.username)
   367  
   368  
   369  FAKE_LOG_DATA = """
   370  2013-08-15 12:41:25 [33454] [INFO] Starting gunicorn 17.5
   371  2013-08-15 12:41:25 [33454] [INFO] Listening at: http://0.0.0.0:5000 (33454)
   372  2013-08-15 12:41:25 [33454] [INFO] Using worker: sync
   373  2013-08-15 12:41:25 [33457] [INFO] Booting worker with pid 33457
   374  """