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