1515from django .test import TransactionTestCase
1616from rest_framework .authtoken .models import Token
1717
18- from api .models import Config
18+ from api .models import App , Config
1919
2020
21- def mock_import_repository_task (* args , ** kwargs ):
21+ def mock_status_ok (* args , ** kwargs ):
2222 resp = requests .Response ()
2323 resp .status_code = 200
2424 resp ._content_consumed = True
2525 return resp
2626
2727
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_timed_out (* args , ** kwargs ):
36+ raise requests .exceptions .Timeout ()
37+
38+
39+ def mock_request_connection_error (* args , ** kwargs ):
40+ raise requests .exceptions .ConnectionError ()
41+
42+
43+ def mock_time (* args , ** kwargs ):
44+ if not hasattr (mock_time , "counter" ):
45+ mock_time .counter = 0 # it doesn't exist yet, so initialize it
46+ mock_time .counter += 1
47+ return mock_time .counter
48+
49+
2850class ConfigTest (TransactionTestCase ):
2951
3052 """Tests setting and updating config values"""
@@ -35,7 +57,7 @@ def setUp(self):
3557 self .user = User .objects .get (username = 'autotest' )
3658 self .token = Token .objects .get (user = self .user ).key
3759
38- @mock .patch ('requests.post' , mock_import_repository_task )
60+ @mock .patch ('requests.post' , mock_status_ok )
3961 def test_config (self ):
4062 """
4163 Test that config is auto-created for a new app and that
@@ -107,7 +129,7 @@ def test_config(self):
107129 self .assertEqual (response .status_code , 405 )
108130 return config5
109131
110- @mock .patch ('requests.post' , mock_import_repository_task )
132+ @mock .patch ('requests.post' , mock_status_ok )
111133 def test_response_data (self ):
112134 """Test that the serialized response contains only relevant data."""
113135 body = {'id' : 'test' }
@@ -132,7 +154,7 @@ def test_response_data(self):
132154 }
133155 self .assertDictContainsSubset (expected , response .data )
134156
135- @mock .patch ('requests.post' , mock_import_repository_task )
157+ @mock .patch ('requests.post' , mock_status_ok )
136158 def test_config_set_same_key (self ):
137159 """
138160 Test that config sets on the same key function properly
@@ -156,7 +178,7 @@ def test_config_set_same_key(self):
156178 self .assertIn ('PORT' , response .data ['values' ])
157179 self .assertEqual (response .data ['values' ]['PORT' ], '5001' )
158180
159- @mock .patch ('requests.post' , mock_import_repository_task )
181+ @mock .patch ('requests.post' , mock_status_ok )
160182 def test_config_set_unicode (self ):
161183 """
162184 Test that config sets with unicode values are accepted.
@@ -187,14 +209,14 @@ def test_config_set_unicode(self):
187209 self .assertIn ('INTEGER' , response .data ['values' ])
188210 self .assertEqual (response .data ['values' ]['INTEGER' ], 1 )
189211
190- @mock .patch ('requests.post' , mock_import_repository_task )
212+ @mock .patch ('requests.post' , mock_status_ok )
191213 def test_config_str (self ):
192214 """Test the text representation of a node."""
193215 config5 = self .test_config ()
194216 config = Config .objects .get (uuid = config5 ['uuid' ])
195217 self .assertEqual (str (config ), "{}-{}" .format (config5 ['app' ], config5 ['uuid' ][:7 ]))
196218
197- @mock .patch ('requests.post' , mock_import_repository_task )
219+ @mock .patch ('requests.post' , mock_status_ok )
198220 def test_admin_can_create_config_on_other_apps (self ):
199221 """If a non-admin creates an app, an administrator should be able to set config
200222 values for that app.
@@ -214,7 +236,7 @@ def test_admin_can_create_config_on_other_apps(self):
214236 self .assertIn ('PORT' , response .data ['values' ])
215237 return response
216238
217- @mock .patch ('requests.post' , mock_import_repository_task )
239+ @mock .patch ('requests.post' , mock_status_ok )
218240 def test_limit_memory (self ):
219241 """
220242 Test that limit is auto-created for a new app and that
@@ -302,7 +324,7 @@ def test_limit_memory(self):
302324 self .assertEqual (response .status_code , 405 )
303325 return limit4
304326
305- @mock .patch ('requests.post' , mock_import_repository_task )
327+ @mock .patch ('requests.post' , mock_status_ok )
306328 def test_limit_cpu (self ):
307329 """
308330 Test that CPU limits can be set
@@ -373,7 +395,7 @@ def test_limit_cpu(self):
373395 self .assertEqual (response .status_code , 405 )
374396 return limit4
375397
376- @mock .patch ('requests.post' , mock_import_repository_task )
398+ @mock .patch ('requests.post' , mock_status_ok )
377399 def test_tags (self ):
378400 """
379401 Test that tags can be set on an application
@@ -477,3 +499,73 @@ def test_unauthorized_user_cannot_modify_config(self):
477499 response = self .client .post (url , json .dumps (body ), content_type = 'application/json' ,
478500 HTTP_AUTHORIZATION = 'token {}' .format (unauthorized_token ))
479501 self .assertEqual (response .status_code , 403 )
502+
503+ def _test_app_healthcheck (self ):
504+ url = '/v1/apps'
505+ response = self .client .post (url , HTTP_AUTHORIZATION = 'token {}' .format (self .token ))
506+ self .assertEqual (response .status_code , 201 )
507+ app_id = response .data ['id' ]
508+ # post a new build, expecting it to pass as usual
509+ url = "/v1/apps/{app_id}/builds" .format (** locals ())
510+ body = {'image' : 'autotest/example' }
511+ response = self .client .post (url , json .dumps (body ), content_type = 'application/json' ,
512+ HTTP_AUTHORIZATION = 'token {}' .format (self .token ))
513+ self .assertEqual (response .status_code , 201 )
514+ # set an initial healthcheck url.
515+ url = "/v1/apps/{app_id}/config" .format (** locals ())
516+ body = {'values' : json .dumps ({'HEALTHCHECK_URL' : '/' })}
517+ return self .client .post (url , json .dumps (body ), content_type = 'application/json' ,
518+ HTTP_AUTHORIZATION = 'token {}' .format (self .token ))
519+
520+ @mock .patch ('requests.get' , mock_status_not_found )
521+ def test_app_healthcheck_good (self ):
522+ """
523+ If a user deploys an app with a config value set for HEALTHCHECK_URL, the controller
524+ should check that the application is up. If it's down, the app should be rolled back.
525+ """
526+ response = self ._test_app_healthcheck ()
527+ self .assertEqual (response .status_code , 503 )
528+ self .assertEqual (
529+ response .data ,
530+ {
531+ 'detail' :
532+ "aborting, app failed health check (got '404', expected: '200')"
533+ })
534+ # add in the expected app healthcheck status, which will result in a successful deployment
535+ app_id = App .objects .all ()[0 ]
536+ url = "/v1/apps/{app_id}/config" .format (** locals ())
537+ body = {'values' : json .dumps ({'HEALTHCHECK_STATUS_CODE' : '404' })}
538+ response = self .client .post (url , json .dumps (body ), content_type = 'application/json' ,
539+ HTTP_AUTHORIZATION = 'token {}' .format (self .token ))
540+ self .assertEqual (response .status_code , 201 )
541+
542+ @mock .patch ('requests.get' , mock_request_timed_out )
543+ @mock .patch ('time.time' , mock_time )
544+ def test_app_healthcheck_timeout (self ):
545+ """
546+ If a user deploys an app with a config value set for HEALTHCHECK_URL but the app
547+ times out, the controller should continue checking until either the app
548+ responds or the app fails to respond within the timeout.
549+ """
550+ response = self ._test_app_healthcheck ()
551+ self .assertEqual (response .status_code , 503 )
552+ self .assertEqual (
553+ response .data ,
554+ {'detail' : 'app failed to respond to health check within 60 seconds of launch' })
555+
556+ @mock .patch ('requests.get' , mock_request_connection_error )
557+ @mock .patch ('time.time' , mock_time )
558+ def test_app_healthcheck_connection_error (self ):
559+ """
560+ If a user deploys an app with a config value set for HEALTHCHECK_URL but the app
561+ returns a connection error, the controller should continue checking until either the app
562+ responds or the app fails to respond within the timeout.
563+
564+ NOTE (bacongobbler): the Docker userland proxy listens for connections and returns a
565+ ConnectionError, hence the unit test.
566+ """
567+ response = self ._test_app_healthcheck ()
568+ self .assertEqual (response .status_code , 503 )
569+ self .assertEqual (
570+ response .data ,
571+ {'detail' : 'app failed to respond to health check within 60 seconds of launch' })
0 commit comments