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 """