github.com/jiasir/deis@v1.12.2/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 self.assertEqual(response.data, "No logs for {}".format(app_id)) 109 110 # test logs - 404 from deis-logger 111 mock_response.status_code = 404 112 response = self.client.get(url, HTTP_AUTHORIZATION="token {}".format(self.token)) 113 self.assertEqual(response.status_code, 204) 114 self.assertEqual(response.data, "No logs for {}".format(app_id)) 115 116 # test logs - unanticipated status code from deis-logger 117 mock_response.status_code = 400 118 response = self.client.get(url, HTTP_AUTHORIZATION="token {}".format(self.token)) 119 self.assertEqual(response.status_code, 500) 120 self.assertEqual(response.data, "Error accessing logs for {}".format(app_id)) 121 122 # test logs - success accessing deis-logger 123 mock_response.status_code = 200 124 mock_response.content = FAKE_LOG_DATA 125 response = self.client.get(url, HTTP_AUTHORIZATION="token {}".format(self.token)) 126 self.assertEqual(response.status_code, 200) 127 self.assertEqual(response.data, FAKE_LOG_DATA) 128 129 # test logs - HTTP request error while accessing deis-logger 130 mock_get.side_effect = requests.exceptions.RequestException('Boom!') 131 response = self.client.get(url, HTTP_AUTHORIZATION="token {}".format(self.token)) 132 self.assertEqual(response.status_code, 500) 133 self.assertEqual(response.data, "Error accessing logs for {}".format(app_id)) 134 135 # TODO: test run needs an initial build 136 137 @mock.patch('api.models.logger') 138 def test_app_release_notes_in_logs(self, mock_logger): 139 """Verifies that an app's release summary is dumped into the logs.""" 140 url = '/v1/apps' 141 body = {'id': 'autotest'} 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 # check app logs 146 exp_msg = "autotest created initial release" 147 exp_log_call = mock.call(logging.INFO, exp_msg) 148 mock_logger.log.has_calls(exp_log_call) 149 150 def test_app_errors(self): 151 app_id = 'autotest-errors' 152 url = '/v1/apps' 153 body = {'id': 'camelCase'} 154 response = self.client.post(url, json.dumps(body), content_type='application/json', 155 HTTP_AUTHORIZATION='token {}'.format(self.token)) 156 self.assertContains(response, 'App IDs can only contain [a-z0-9-]', status_code=400) 157 url = '/v1/apps' 158 body = {'id': app_id} 159 response = self.client.post(url, json.dumps(body), content_type='application/json', 160 HTTP_AUTHORIZATION='token {}'.format(self.token)) 161 self.assertEqual(response.status_code, 201) 162 app_id = response.data['id'] # noqa 163 url = '/v1/apps/{app_id}'.format(**locals()) 164 response = self.client.delete(url, 165 HTTP_AUTHORIZATION='token {}'.format(self.token)) 166 self.assertEquals(response.status_code, 204) 167 for endpoint in ('containers', 'config', 'releases', 'builds'): 168 url = '/v1/apps/{app_id}/{endpoint}'.format(**locals()) 169 response = self.client.get(url, 170 HTTP_AUTHORIZATION='token {}'.format(self.token)) 171 self.assertEquals(response.status_code, 404) 172 173 def test_app_reserved_names(self): 174 """Nobody should be able to create applications with names which are reserved.""" 175 url = '/v1/apps' 176 reserved_names = ['foo', 'bar'] 177 with self.settings(DEIS_RESERVED_NAMES=reserved_names): 178 for name in reserved_names: 179 body = {'id': name} 180 response = self.client.post(url, json.dumps(body), content_type='application/json', 181 HTTP_AUTHORIZATION='token {}'.format(self.token)) 182 self.assertContains( 183 response, 184 '{} is a reserved name.'.format(name), 185 status_code=400) 186 187 def test_app_structure_is_valid_json(self): 188 """Application structures should be valid JSON objects.""" 189 url = '/v1/apps' 190 response = self.client.post(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 191 self.assertEqual(response.status_code, 201) 192 app_id = response.data['id'] 193 self.assertIn('structure', response.data) 194 self.assertEqual(response.data['structure'], {}) 195 app = App.objects.get(id=app_id) 196 app.structure = {'web': 1} 197 app.save() 198 url = '/v1/apps/{}'.format(app_id) 199 response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 200 self.assertIn('structure', response.data) 201 self.assertEqual(response.data['structure'], {"web": 1}) 202 203 @mock.patch('requests.post', mock_status_ok) 204 @mock.patch('api.models.logger') 205 def test_admin_can_manage_other_apps(self, mock_logger): 206 """Administrators of Deis should be able to manage all applications. 207 """ 208 # log in as non-admin user and create an app 209 user = User.objects.get(username='autotest2') 210 token = Token.objects.get(user=user) 211 app_id = 'autotest' 212 url = '/v1/apps' 213 body = {'id': app_id} 214 response = self.client.post(url, json.dumps(body), content_type='application/json', 215 HTTP_AUTHORIZATION='token {}'.format(token)) 216 # log in as admin, check to see if they have access 217 url = '/v1/apps/{}'.format(app_id) 218 response = self.client.get(url, 219 HTTP_AUTHORIZATION='token {}'.format(self.token)) 220 self.assertEqual(response.status_code, 200) 221 # check app logs 222 exp_msg = "autotest2 created initial release" 223 exp_log_call = mock.call(logging.INFO, exp_msg) 224 mock_logger.log.has_calls(exp_log_call) 225 # TODO: test run needs an initial build 226 # delete the app 227 url = '/v1/apps/{}'.format(app_id) 228 response = self.client.delete(url, 229 HTTP_AUTHORIZATION='token {}'.format(self.token)) 230 self.assertEqual(response.status_code, 204) 231 232 def test_admin_can_see_other_apps(self): 233 """If a user creates an application, the administrator should be able 234 to see it. 235 """ 236 # log in as non-admin user and create an app 237 user = User.objects.get(username='autotest2') 238 token = Token.objects.get(user=user) 239 app_id = 'autotest' 240 url = '/v1/apps' 241 body = {'id': app_id} 242 response = self.client.post(url, json.dumps(body), content_type='application/json', 243 HTTP_AUTHORIZATION='token {}'.format(token)) 244 # log in as admin 245 response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 246 self.assertEqual(response.data['count'], 1) 247 248 def test_run_without_auth(self): 249 """If the administrator has not provided SSH private key for run commands, 250 make sure a friendly error message is provided on run""" 251 settings.SSH_PRIVATE_KEY = '' 252 url = '/v1/apps' 253 body = {'id': 'autotest'} 254 response = self.client.post(url, json.dumps(body), content_type='application/json', 255 HTTP_AUTHORIZATION='token {}'.format(self.token)) 256 self.assertEqual(response.status_code, 201) 257 app_id = response.data['id'] # noqa 258 # test run 259 url = '/v1/apps/{app_id}/run'.format(**locals()) 260 body = {'command': 'ls -al'} 261 response = self.client.post(url, json.dumps(body), content_type='application/json', 262 HTTP_AUTHORIZATION='token {}'.format(self.token)) 263 self.assertEquals(response.status_code, 400) 264 self.assertEquals(response.data, {'detail': 'Support for admin commands ' 265 'is not configured'}) 266 267 def test_run_without_release_should_error(self): 268 """ 269 A user should not be able to run a one-off command unless a release 270 is present. 271 """ 272 app_id = 'autotest' 273 url = '/v1/apps' 274 body = {'id': app_id} 275 response = self.client.post(url, json.dumps(body), content_type='application/json', 276 HTTP_AUTHORIZATION='token {}'.format(self.token)) 277 url = '/v1/apps/{}/run'.format(app_id) 278 body = {'command': 'ls -al'} 279 response = self.client.post(url, json.dumps(body), content_type='application/json', 280 HTTP_AUTHORIZATION='token {}'.format(self.token)) 281 self.assertEqual(response.status_code, 400) 282 self.assertEqual(response.data, {'detail': 'No build associated with this ' 283 'release to run this command'}) 284 285 def test_unauthorized_user_cannot_see_app(self): 286 """ 287 An unauthorized user should not be able to access an app's resources. 288 289 Since an unauthorized user can't access the application, these 290 tests should return a 403, but currently return a 404. FIXME! 291 """ 292 app_id = 'autotest' 293 base_url = '/v1/apps' 294 body = {'id': app_id} 295 response = self.client.post(base_url, json.dumps(body), content_type='application/json', 296 HTTP_AUTHORIZATION='token {}'.format(self.token)) 297 unauthorized_user = User.objects.get(username='autotest2') 298 unauthorized_token = Token.objects.get(user=unauthorized_user).key 299 url = '{}/{}/run'.format(base_url, app_id) 300 body = {'command': 'foo'} 301 response = self.client.post(url, json.dumps(body), content_type='application/json', 302 HTTP_AUTHORIZATION='token {}'.format(unauthorized_token)) 303 self.assertEqual(response.status_code, 403) 304 url = '{}/{}/logs'.format(base_url, app_id) 305 response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(unauthorized_token)) 306 self.assertEqual(response.status_code, 403) 307 url = '{}/{}'.format(base_url, app_id) 308 response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(unauthorized_token)) 309 self.assertEqual(response.status_code, 403) 310 response = self.client.delete(url, 311 HTTP_AUTHORIZATION='token {}'.format(unauthorized_token)) 312 self.assertEqual(response.status_code, 403) 313 314 def test_app_info_not_showing_wrong_app(self): 315 app_id = 'autotest' 316 base_url = '/v1/apps' 317 body = {'id': app_id} 318 response = self.client.post(base_url, json.dumps(body), content_type='application/json', 319 HTTP_AUTHORIZATION='token {}'.format(self.token)) 320 url = '{}/foo'.format(base_url) 321 response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 322 self.assertEqual(response.status_code, 404) 323 324 def test_app_transfer(self): 325 owner = User.objects.get(username='autotest2') 326 owner_token = Token.objects.get(user=owner).key 327 app_id = 'autotest' 328 base_url = '/v1/apps' 329 body = {'id': app_id} 330 response = self.client.post(base_url, json.dumps(body), content_type='application/json', 331 HTTP_AUTHORIZATION='token {}'.format(owner_token)) 332 # Transfer App 333 url = '{}/{}'.format(base_url, app_id) 334 new_owner = User.objects.get(username='autotest3') 335 new_owner_token = Token.objects.get(user=new_owner).key 336 body = {'owner': new_owner.username} 337 response = self.client.post(url, json.dumps(body), content_type='application/json', 338 HTTP_AUTHORIZATION='token {}'.format(owner_token)) 339 self.assertEqual(response.status_code, 200) 340 341 # Original user can no longer access it 342 response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(owner_token)) 343 self.assertEqual(response.status_code, 403) 344 345 # New owner can access it 346 response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(new_owner_token)) 347 self.assertEqual(response.status_code, 200) 348 self.assertEqual(response.data['owner'], new_owner.username) 349 350 # Collaborators can't transfer 351 body = {'username': owner.username} 352 perms_url = url+"/perms/" 353 response = self.client.post(perms_url, json.dumps(body), content_type='application/json', 354 HTTP_AUTHORIZATION='token {}'.format(new_owner_token)) 355 self.assertEqual(response.status_code, 201) 356 body = {'owner': self.user.username} 357 response = self.client.post(url, json.dumps(body), content_type='application/json', 358 HTTP_AUTHORIZATION='token {}'.format(owner_token)) 359 self.assertEqual(response.status_code, 403) 360 361 # Admins can transfer 362 body = {'owner': self.user.username} 363 response = self.client.post(url, json.dumps(body), content_type='application/json', 364 HTTP_AUTHORIZATION='token {}'.format(self.token)) 365 self.assertEqual(response.status_code, 200) 366 response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 367 self.assertEqual(response.status_code, 200) 368 self.assertEqual(response.data['owner'], self.user.username) 369 370 371 FAKE_LOG_DATA = """ 372 2013-08-15 12:41:25 [33454] [INFO] Starting gunicorn 17.5 373 2013-08-15 12:41:25 [33454] [INFO] Listening at: http://0.0.0.0:5000 (33454) 374 2013-08-15 12:41:25 [33454] [INFO] Using worker: sync 375 2013-08-15 12:41:25 [33457] [INFO] Booting worker with pid 33457 376 """