842 lines
29 KiB
Python
842 lines
29 KiB
Python
import re
|
|
|
|
import jsonschema
|
|
import mock
|
|
import pytest
|
|
import requests
|
|
from requests.structures import CaseInsensitiveDict
|
|
|
|
from dcos import http, marathon, rpcclient
|
|
from dcos.errors import DCOSException, DCOSHTTPException
|
|
|
|
|
|
def test_add_pod_puts_json_in_request_body():
|
|
_assert_add_pod_puts_json_in_request_body(pod_json={"some": "json"})
|
|
_assert_add_pod_puts_json_in_request_body(
|
|
pod_json=["another", {"json": "value"}])
|
|
|
|
|
|
def test_add_pod_returns_parsed_response_body():
|
|
_assert_add_pod_returns_parsed_response_body({"id": "i-am-a-pod"})
|
|
_assert_add_pod_returns_parsed_response_body(["another", "pod", "json"])
|
|
|
|
|
|
def test_remove_pod_has_default_force_value():
|
|
marathon_client, rpc_client = _create_fixtures()
|
|
marathon_client.remove_pod('foo')
|
|
rpc_client.http_req.assert_called_with(
|
|
http.delete, 'v2/pods/foo', params=None)
|
|
|
|
|
|
def test_remove_pod_builds_rpc_correctly():
|
|
_assert_remove_pod_builds_rpc_correctly(
|
|
pod_id='foo', force=False, path='v2/pods/foo', params=None)
|
|
_assert_remove_pod_builds_rpc_correctly(
|
|
pod_id='foo', force=True, path='v2/pods/foo', params={'force': 'true'})
|
|
_assert_remove_pod_builds_rpc_correctly(
|
|
pod_id='/foo bar/', force=False, path='v2/pods/foo%20bar', params=None)
|
|
|
|
|
|
def test_remove_pod_propagates_dcos_exception():
|
|
_assert_method_propagates_rpc_dcos_exception(
|
|
lambda marathon_client: marathon_client.remove_pod('bad'))
|
|
|
|
|
|
def test_show_pod_builds_rpc_correctly():
|
|
_assert_show_pod_builds_rpc_correctly(
|
|
pod_id='foo', path='v2/pods/foo::status')
|
|
_assert_show_pod_builds_rpc_correctly(
|
|
pod_id='/bar', path='v2/pods/bar::status')
|
|
_assert_show_pod_builds_rpc_correctly(
|
|
pod_id='baz/', path='v2/pods/baz::status')
|
|
_assert_show_pod_builds_rpc_correctly(
|
|
pod_id='foo bar', path='v2/pods/foo%20bar::status')
|
|
|
|
|
|
def test_show_pod_returns_response_json():
|
|
_assert_show_pod_returns_response_json({'some': 'json'})
|
|
_assert_show_pod_returns_response_json(['another', 'json', 'value'])
|
|
|
|
|
|
def test_show_pod_propagates_dcos_exception():
|
|
_assert_method_propagates_rpc_dcos_exception(
|
|
lambda marathon_client: marathon_client.show_pod('bad-req'))
|
|
|
|
|
|
def test_show_pod_raises_dcos_exception_for_json_parse_errors():
|
|
_assert_method_raises_dcos_exception_for_json_parse_errors(
|
|
lambda marathon_client: marathon_client.show_pod('bad-json'))
|
|
|
|
|
|
def test_list_pod_builds_rpc_correctly():
|
|
marathon_client, rpc_client = _create_fixtures()
|
|
marathon_client.list_pod()
|
|
rpc_client.http_req.assert_called_with(http.get, 'v2/pods/::status')
|
|
|
|
|
|
def test_list_pod_returns_success_response_json():
|
|
_assert_list_pod_returns_success_response_json(body_json={'some': 'json'})
|
|
_assert_list_pod_returns_success_response_json(body_json=['a', 'b', 'c'])
|
|
|
|
|
|
def test_list_pod_propagates_rpc_dcos_exception():
|
|
_assert_method_propagates_rpc_dcos_exception(
|
|
lambda marathon_client: marathon_client.list_pod())
|
|
|
|
|
|
def test_list_pod_raises_dcos_exception_for_json_parse_errors():
|
|
_assert_method_raises_dcos_exception_for_json_parse_errors(
|
|
lambda marathon_client: marathon_client.list_pod())
|
|
|
|
|
|
def test_update_pod_executes_successfully():
|
|
_assert_update_pod_executes_successfully(
|
|
pod_id='foo',
|
|
pod_json={'some', 'json'},
|
|
force=False,
|
|
path='v2/pods/foo',
|
|
params=None,
|
|
deployment_id='pod-deployment-id')
|
|
_assert_update_pod_executes_successfully(
|
|
pod_id='/foo bar/',
|
|
pod_json={'some', 'json'},
|
|
force=False,
|
|
path='v2/pods/foo%20bar',
|
|
params=None,
|
|
deployment_id='pod-deployment-id')
|
|
_assert_update_pod_executes_successfully(
|
|
pod_id='foo',
|
|
pod_json={'some', 'json'},
|
|
force=True,
|
|
path='v2/pods/foo',
|
|
params={'force': 'true'},
|
|
deployment_id='pod-deployment-id')
|
|
_assert_update_pod_executes_successfully(
|
|
pod_id='foo',
|
|
pod_json={'something', 'different'},
|
|
force=False,
|
|
path='v2/pods/foo',
|
|
params=None,
|
|
deployment_id='pod-deployment-id')
|
|
_assert_update_pod_executes_successfully(
|
|
pod_id='foo',
|
|
pod_json={'some', 'json'},
|
|
force=False,
|
|
path='v2/pods/foo',
|
|
params=None,
|
|
deployment_id='an-arbitrary-value')
|
|
|
|
|
|
def test_update_pod_has_default_force_value():
|
|
marathon_client, rpc_client = _create_fixtures()
|
|
marathon_client.update_pod('foo', {'some': 'json'})
|
|
rpc_client.http_req.assert_called_with(
|
|
http.put, 'v2/pods/foo', params=None, json={'some': 'json'})
|
|
|
|
|
|
def test_update_pod_propagates_rpc_dcos_exception():
|
|
_assert_method_propagates_rpc_dcos_exception(
|
|
lambda marathon_client:
|
|
marathon_client.update_pod('foo', {'some': 'json'}))
|
|
|
|
|
|
def test_add_pod_raises_dcos_exception_if_deployment_id_missing():
|
|
_assert_add_pod_raises_dcos_exception_if_deployment_id_missing(
|
|
headers={'foo': 'bar'})
|
|
_assert_add_pod_raises_dcos_exception_if_deployment_id_missing(
|
|
headers={'marathon-deployment_ID': 'misspelled-field', 'zzz': 'aaa'})
|
|
|
|
|
|
def test_update_pod_raises_dcos_exception_if_deployment_id_missing():
|
|
_assert_update_pod_raises_dcos_exception_if_deployment_id_missing(
|
|
headers={'foo': 'bar'})
|
|
_assert_update_pod_raises_dcos_exception_if_deployment_id_missing(
|
|
headers={'marathon-deployment_ID': 'misspelled-field', 'zzz': 'aaa'})
|
|
|
|
|
|
def test_kill_pod_instances_executes_successfully():
|
|
pod_id = 'foo'
|
|
instance_ids = ['instance1', 'instance2']
|
|
path = 'v2/pods/foo::instances'
|
|
response_json = {'some': ['instance', 'status']}
|
|
|
|
mock_response = mock.create_autospec(requests.Response)
|
|
mock_response.json.return_value = response_json
|
|
|
|
marathon_client, rpc_client = _create_fixtures()
|
|
rpc_client.http_req.return_value = mock_response
|
|
|
|
path_format_method_name = 'dcos.marathon.Client._marathon_id_path_format'
|
|
path_format = mock.MagicMock()
|
|
path_format.return_value = path
|
|
|
|
with mock.patch(path_format_method_name, new=path_format):
|
|
actual_json = marathon_client.kill_pod_instances(pod_id, instance_ids)
|
|
|
|
path_format.assert_called_with('v2/pods/{}::instances', pod_id)
|
|
rpc_client.http_req.assert_called_with(
|
|
http.delete, path, json=instance_ids)
|
|
assert actual_json == response_json
|
|
|
|
|
|
def test_kill_pod_instances_propagates_rpc_dcos_exception():
|
|
_assert_method_propagates_rpc_dcos_exception(
|
|
lambda marathon_client:
|
|
marathon_client.kill_pod_instances('foo', ['inst1', 'inst2']))
|
|
|
|
|
|
def test_kill_pod_instances_raises_dcos_exception_for_json_parse_errors():
|
|
_assert_method_raises_dcos_exception_for_json_parse_errors(
|
|
lambda marathon_client: marathon_client.kill_pod_instances(
|
|
'foo', ['bar', 'baz']))
|
|
|
|
|
|
@mock.patch('dcos.http.head')
|
|
def test_pod_feature_supported_gets_success_response(head_fn):
|
|
def invoke_test_case(status_code):
|
|
mock_response = mock.create_autospec(requests.Response)
|
|
mock_response.status_code = status_code
|
|
head_fn.return_value = mock_response
|
|
|
|
rpc_client = rpcclient.RpcClient('http://base/url', timeout=42)
|
|
marathon_client = marathon.Client(rpc_client)
|
|
is_supported = marathon_client.pod_feature_supported()
|
|
|
|
head_fn.assert_called_with('http://base/url/v2/pods', timeout=42)
|
|
|
|
return is_supported
|
|
|
|
assert invoke_test_case(status_code=200)
|
|
assert invoke_test_case(status_code=204)
|
|
|
|
assert not invoke_test_case(status_code=100)
|
|
assert not invoke_test_case(status_code=302)
|
|
|
|
|
|
@mock.patch('dcos.http.head')
|
|
def test_pod_feature_supported_gets_404_response(head_fn):
|
|
mock_response = mock.create_autospec(requests.Response)
|
|
mock_response.status_code = 404
|
|
head_fn.side_effect = DCOSHTTPException(mock_response)
|
|
|
|
rpc_client = rpcclient.RpcClient('http://base/url', timeout=24)
|
|
marathon_client = marathon.Client(rpc_client)
|
|
|
|
assert not marathon_client.pod_feature_supported()
|
|
head_fn.assert_called_with('http://base/url/v2/pods', timeout=24)
|
|
|
|
|
|
def test_pod_feature_supported_converts_http_exceptions_to_dcos_exceptions():
|
|
@mock.patch('dcos.http.head')
|
|
def test_case(head_fn, status_code):
|
|
request = requests.Request(method='ANY', url='http://arbitrary/url')
|
|
mock_response = mock.create_autospec(requests.Response)
|
|
mock_response.status_code = status_code
|
|
mock_response.reason = 'Arbitrary Reason'
|
|
mock_response.request = request
|
|
mock_response.json.side_effect = ValueError('empty body')
|
|
head_fn.side_effect = DCOSHTTPException(mock_response)
|
|
|
|
rpc_client = rpcclient.RpcClient('http://does/not/matter')
|
|
marathon_client = marathon.Client(rpc_client)
|
|
|
|
with pytest.raises(DCOSException) as exception_info:
|
|
marathon_client.pod_feature_supported()
|
|
|
|
message = rpcclient.RpcClient.response_error_message(
|
|
status_code,
|
|
reason='Arbitrary Reason',
|
|
request_method='ANY',
|
|
request_url='http://arbitrary/url',
|
|
json_body=None)
|
|
assert str(exception_info.value).endswith(message)
|
|
|
|
test_case(status_code=400)
|
|
test_case(status_code=401)
|
|
test_case(status_code=403)
|
|
test_case(status_code=409)
|
|
test_case(status_code=422)
|
|
test_case(status_code=500)
|
|
|
|
|
|
def test_pod_feature_supported_propagates_other_exceptions():
|
|
_assert_pod_feature_supported_raises_exception(
|
|
exception=DCOSException("BOOM!"))
|
|
_assert_pod_feature_supported_raises_exception(
|
|
exception=Exception("Uh oh"))
|
|
|
|
|
|
def test_rpc_client_http_req_calls_method_fn():
|
|
def test_case(base_url, path, full_url):
|
|
method_fn = mock.Mock()
|
|
|
|
rpc_client = rpcclient.RpcClient(base_url)
|
|
rpc_client.http_req(method_fn, path)
|
|
|
|
method_fn.assert_called_with(full_url,
|
|
timeout=http.DEFAULT_TIMEOUT)
|
|
|
|
test_case(
|
|
base_url='http://base/url',
|
|
path='some/path',
|
|
full_url='http://base/url/some/path')
|
|
test_case(
|
|
base_url='http://base/url',
|
|
path='different/thing',
|
|
full_url='http://base/url/different/thing')
|
|
test_case(
|
|
base_url='gopher://different/thing',
|
|
path='some/path',
|
|
full_url='gopher://different/thing/some/path')
|
|
test_case(
|
|
base_url='http://base/without/slash',
|
|
path='/path/with/slash',
|
|
full_url='http://base/without/slash/path/with/slash')
|
|
test_case(
|
|
base_url='http://base/with/slash/',
|
|
path='path/without/slash',
|
|
full_url='http://base/with/slash/path/without/slash')
|
|
test_case(
|
|
base_url='http://base/with/slash/',
|
|
path='/path/with/slash',
|
|
full_url='http://base/with/slash/path/with/slash')
|
|
|
|
|
|
def test_rpc_client_http_req_passes_args_to_method_fn():
|
|
method_fn = mock.Mock()
|
|
|
|
rpc_client = rpcclient.RpcClient('http://base/url')
|
|
rpc_client.http_req(method_fn, 'some/path', 'foo', 42)
|
|
|
|
method_fn.assert_called_with('http://base/url/some/path',
|
|
'foo',
|
|
42,
|
|
timeout=http.DEFAULT_TIMEOUT)
|
|
|
|
|
|
def test_rpc_client_http_req_passes_kwargs_to_method_fn():
|
|
method_fn = mock.Mock()
|
|
|
|
rpc_client = rpcclient.RpcClient('http://base/url')
|
|
rpc_client.http_req(method_fn, 'some/path', foo='bar', baz=42)
|
|
|
|
method_fn.assert_called_with('http://base/url/some/path',
|
|
foo='bar',
|
|
baz=42,
|
|
timeout=http.DEFAULT_TIMEOUT)
|
|
|
|
|
|
def test_rpc_client_http_req_kwarg_timeout_overrides_default():
|
|
method_fn = mock.Mock()
|
|
|
|
rpc_client = rpcclient.RpcClient('http://base/url')
|
|
rpc_client.http_req(method_fn, 'some/path', timeout=42)
|
|
|
|
method_fn.assert_called_with('http://base/url/some/path', timeout=42)
|
|
|
|
|
|
def test_rpc_client_http_req_set_timeout_in_constructor():
|
|
method_fn = mock.Mock()
|
|
|
|
rpc_client = rpcclient.RpcClient('http://base/url', 24)
|
|
rpc_client.http_req(method_fn, 'some/path')
|
|
|
|
method_fn.assert_called_with('http://base/url/some/path', timeout=24)
|
|
|
|
|
|
def test_rpc_client_http_req_returns_method_fn_result():
|
|
def test_case(expected):
|
|
|
|
def method_fn(*args, **kwargs):
|
|
return expected
|
|
|
|
rpc_client = rpcclient.RpcClient('http://base/url')
|
|
actual = rpc_client.http_req(method_fn, 'some/path')
|
|
|
|
assert actual == expected
|
|
|
|
test_case(['the', 'result'])
|
|
test_case({'another': 'result'})
|
|
|
|
|
|
def test_rpc_client_http_req_propagates_method_fn_exception_1():
|
|
request = requests.Request(method='ANY', url='http://arbitrary/url')
|
|
# Need the mock so that the `.json()` method can be overridden
|
|
response = mock.create_autospec(requests.Response)
|
|
response.status_code = 403
|
|
response.reason = 'Forbidden'
|
|
response.request = request
|
|
response.json.side_effect = Exception('not JSON')
|
|
|
|
def method_fn(*args, **kwargs):
|
|
raise DCOSHTTPException(response)
|
|
|
|
rpc_client = rpcclient.RpcClient('http://base/url')
|
|
with pytest.raises(DCOSException) as e:
|
|
rpc_client.http_req(method_fn, 'some/path')
|
|
|
|
expected_message = rpcclient.RpcClient.response_error_message(
|
|
status_code=403,
|
|
reason='Forbidden',
|
|
request_method='ANY',
|
|
request_url='http://arbitrary/url',
|
|
json_body=None)
|
|
assert str(e).endswith(expected_message)
|
|
|
|
|
|
def test_rpc_client_http_req_propagates_method_fn_exception_2():
|
|
request = requests.Request(method='NONE', url='http://host/path')
|
|
# Need the mock so that the `.json()` method can be overridden
|
|
response = mock.create_autospec(requests.Response)
|
|
response.status_code = 422
|
|
response.reason = 'Something Bad'
|
|
response.request = request
|
|
response.json.return_value = {'message': 'BOOM!'}
|
|
|
|
def method_fn(*args, **kwargs):
|
|
raise DCOSHTTPException(response)
|
|
|
|
rpc_client = rpcclient.RpcClient('http://base/url')
|
|
with pytest.raises(DCOSException) as e:
|
|
rpc_client.http_req(method_fn, 'some/path')
|
|
|
|
expected_message = rpcclient.RpcClient.response_error_message(
|
|
status_code=422,
|
|
reason='Something Bad',
|
|
request_method='None',
|
|
request_url='http://host/path',
|
|
json_body={'message': 'BOOM!'})
|
|
assert str(e).endswith(expected_message)
|
|
|
|
|
|
def test_error_json_schema_is_valid():
|
|
error_json_schema = rpcclient.load_error_json_schema()
|
|
jsonschema.Draft4Validator.check_schema(error_json_schema)
|
|
|
|
|
|
def test_response_error_message_with_400_status_no_json():
|
|
_assert_response_error_message_with_400_status_no_json(
|
|
reason=_REASON_1,
|
|
request_method=_METHOD_1,
|
|
request_url=_URL_1)
|
|
|
|
_assert_response_error_message_with_400_status_no_json(
|
|
reason=_REASON_2,
|
|
request_method=_METHOD_2,
|
|
request_url=_URL_2)
|
|
|
|
|
|
def test_response_error_message_with_400_status_json():
|
|
_assert_response_error_message_with_400_status_json(
|
|
_RESPONSE_JSON_1, _PRINTED_JSON_1)
|
|
|
|
_assert_response_error_message_with_400_status_json(
|
|
_RESPONSE_JSON_2, _PRINTED_JSON_2)
|
|
|
|
|
|
def test_res_err_msg_with_409_status():
|
|
def test_case(request_url, expected_resource):
|
|
actual = rpcclient.RpcClient.response_error_message(
|
|
status_code=409,
|
|
reason=_REASON_X,
|
|
request_method=_METHOD_X,
|
|
request_url=request_url,
|
|
json_body=None)
|
|
|
|
pattern = r'Changes blocked: deployment already in progress for (.*)\.'
|
|
_assert_matches_with_groups(pattern, actual, (expected_resource,))
|
|
|
|
test_case('http://marathon/v2/apps', 'app')
|
|
test_case('http://marathon/v2/groups/group-id', 'group')
|
|
test_case('http://marathon/v2/pods/', 'pod')
|
|
test_case('http://marathon/v2/thingies/foo', 'resource')
|
|
test_case('http://dcos/service/marathon/v2/apps/bar', 'app')
|
|
test_case('http://pods-app.com/service/marathon/v2/groups/baz', 'group')
|
|
|
|
|
|
def test_response_error_message_with_other_status_no_json():
|
|
_assert_response_error_message_with_other_status_no_json(
|
|
status_code=401,
|
|
reason=_REASON_1,
|
|
request_url=_URL_1)
|
|
|
|
_assert_response_error_message_with_other_status_no_json(
|
|
status_code=401,
|
|
reason=_REASON_2,
|
|
request_url=_URL_2)
|
|
|
|
_assert_response_error_message_with_other_status_no_json(
|
|
status_code=403,
|
|
reason=_REASON_X,
|
|
request_url=_URL_X)
|
|
|
|
_assert_response_error_message_with_other_status_no_json(
|
|
status_code=404,
|
|
reason=_REASON_X,
|
|
request_url=_URL_X)
|
|
|
|
_assert_response_error_message_with_other_status_no_json(
|
|
status_code=422,
|
|
reason=_REASON_X,
|
|
request_url=_URL_X)
|
|
|
|
|
|
def test_response_error_message_with_other_status_json_has_message():
|
|
_assert_response_error_message_with_other_status_json_has_message(
|
|
status_code=401,
|
|
json_message=_MESSAGE_1)
|
|
|
|
_assert_response_error_message_with_other_status_json_has_message(
|
|
status_code=401,
|
|
json_message=_MESSAGE_2)
|
|
|
|
_assert_response_error_message_with_other_status_json_has_message(
|
|
status_code=403,
|
|
json_message=_MESSAGE_X)
|
|
|
|
_assert_response_error_message_with_other_status_json_has_message(
|
|
status_code=404,
|
|
json_message=_MESSAGE_X)
|
|
|
|
_assert_response_error_message_with_other_status_json_has_message(
|
|
status_code=422,
|
|
json_message=_MESSAGE_X)
|
|
|
|
|
|
def test_res_err_msg_with_other_status_json_no_message_has_valid_errors():
|
|
_assert_res_err_msg_with_other_status_json_no_message_has_valid_errors(
|
|
status_code=401,
|
|
errors_json=[{'error': 'err1'}, {'error': 'err2'}],
|
|
errors_str='err1\nerr2')
|
|
|
|
_assert_res_err_msg_with_other_status_json_no_message_has_valid_errors(
|
|
status_code=401,
|
|
errors_json=[{'error': 'foo'}, {'error': 'bar'}, {'error': 'baz'}],
|
|
errors_str='foo\nbar\nbaz')
|
|
|
|
_assert_res_err_msg_with_other_status_json_no_message_has_valid_errors(
|
|
status_code=403,
|
|
errors_json=[{'error': 'foo'}, {'error': 'bar'}, {'error': 'baz'}],
|
|
errors_str='foo\nbar\nbaz')
|
|
|
|
_assert_res_err_msg_with_other_status_json_no_message_has_valid_errors(
|
|
status_code=404,
|
|
errors_json=[{'error': 'foo'}, {'error': 'bar'}, {'error': 'baz'}],
|
|
errors_str='foo\nbar\nbaz')
|
|
|
|
_assert_res_err_msg_with_other_status_json_no_message_has_valid_errors(
|
|
status_code=422,
|
|
errors_json=[{'error': 'foo'}, {'error': 'bar'}, {'error': 'baz'}],
|
|
errors_str='foo\nbar\nbaz')
|
|
|
|
|
|
def test_res_err_msg_with_other_status_invalid_error_json():
|
|
# Is not an object
|
|
_assert_res_err_msg_with_other_status_invalid_json(401, 'Error!')
|
|
_assert_res_err_msg_with_other_status_invalid_json(401, ['Error!'])
|
|
# Missing both message and errors fields
|
|
_assert_res_err_msg_with_other_status_invalid_json(401, {})
|
|
_assert_res_err_msg_with_other_status_invalid_json(
|
|
401, {'foo': 5, 'bar': 'x'})
|
|
# Has non-str message
|
|
_assert_res_err_msg_with_other_status_invalid_json(401, {'message': 42})
|
|
_assert_res_err_msg_with_other_status_invalid_json(
|
|
401, {'message': ['Oops!']})
|
|
# Has non-array errors
|
|
_assert_res_err_msg_with_other_status_invalid_json(401, {'errors': 42})
|
|
_assert_res_err_msg_with_other_status_invalid_json(
|
|
401, {'errors': {'error': 5}})
|
|
# Errors array has non-object elements
|
|
_assert_res_err_msg_with_other_status_invalid_json(
|
|
401, {'errors': [42, True]})
|
|
_assert_res_err_msg_with_other_status_invalid_json(
|
|
401, {'errors': [{'error': 'BOOM!'}, 'not_an_error']})
|
|
# Errors array has objects without `error` field
|
|
_assert_res_err_msg_with_other_status_invalid_json(
|
|
401, {'errors': [{'error': 'BOOM!'}, {'foo': 'bar'}]})
|
|
# Errors array has objects with non-str `error` field
|
|
_assert_res_err_msg_with_other_status_invalid_json(
|
|
401, {'errors': [{'error': 'BOOM!'}, {'error': 42}]})
|
|
# Other status codes
|
|
_assert_res_err_msg_with_other_status_invalid_json(
|
|
403, {'errors': [{'error': 'BOOM!'}, {'error': 42}]})
|
|
_assert_res_err_msg_with_other_status_invalid_json(
|
|
404, {'errors': [{'error': 'BOOM!'}, {'error': 42}]})
|
|
_assert_res_err_msg_with_other_status_invalid_json(
|
|
422, {'errors': [{'error': 'BOOM!'}, {'error': 42}]})
|
|
|
|
|
|
def _assert_add_pod_puts_json_in_request_body(pod_json):
|
|
rpc_client = mock.create_autospec(rpcclient.RpcClient)
|
|
|
|
client = marathon.Client(rpc_client)
|
|
client.add_pod(pod_json)
|
|
|
|
rpc_client.http_req.assert_called_with(http.post, 'v2/pods', json=pod_json)
|
|
|
|
|
|
def _assert_add_pod_returns_parsed_response_body(response_json):
|
|
|
|
headers = {'Marathon-Deployment-Id': "XYZ"}
|
|
mock_response = _pod_response_fixture(headers)
|
|
mock_response.json.return_value = response_json
|
|
|
|
marathon_client, rpc_client = _create_fixtures()
|
|
rpc_client.http_req.return_value = mock_response
|
|
|
|
assert marathon_client.add_pod({'some': 'json'}) == "XYZ"
|
|
|
|
|
|
def _assert_remove_pod_builds_rpc_correctly(pod_id, force, path, params):
|
|
marathon_client, rpc_client = _create_fixtures()
|
|
marathon_client.remove_pod(pod_id, force)
|
|
rpc_client.http_req.assert_called_with(http.delete, path, params=params)
|
|
|
|
|
|
def _assert_show_pod_builds_rpc_correctly(pod_id, path):
|
|
marathon_client, rpc_client = _create_fixtures()
|
|
marathon_client.show_pod(pod_id)
|
|
rpc_client.http_req.assert_called_with(http.get, path)
|
|
|
|
|
|
def _assert_show_pod_returns_response_json(expected):
|
|
marathon_client, rpc_client = _create_fixtures()
|
|
mock_response = mock.create_autospec(requests.Response)
|
|
mock_response.json.return_value = expected
|
|
rpc_client.http_req.return_value = mock_response
|
|
|
|
response_json = marathon_client.show_pod('arbitrary-id')
|
|
|
|
assert response_json == expected
|
|
|
|
|
|
def _assert_list_pod_returns_success_response_json(body_json):
|
|
marathon_client, rpc_client = _create_fixtures()
|
|
mock_response = mock.create_autospec(requests.Response)
|
|
mock_response.json.return_value = body_json
|
|
rpc_client.http_req.return_value = mock_response
|
|
|
|
assert marathon_client.list_pod() == body_json
|
|
|
|
|
|
def _assert_update_pod_executes_successfully(
|
|
pod_id, pod_json, force, path, params, deployment_id):
|
|
headers = {'Marathon-Deployment-Id': deployment_id}
|
|
mock_response = _pod_response_fixture(headers)
|
|
|
|
marathon_client, rpc_client = _create_fixtures()
|
|
rpc_client.http_req.return_value = mock_response
|
|
|
|
actual_return = marathon_client.update_pod(pod_id, pod_json, force)
|
|
|
|
rpc_client.http_req.assert_called_with(
|
|
http.put, path, params=params, json=pod_json)
|
|
assert actual_return == deployment_id
|
|
|
|
|
|
def _assert_method_raises_dcos_exception_for_json_parse_errors(invoke_method):
|
|
def assert_test_case(non_json):
|
|
mock_response = mock.create_autospec(requests.Response)
|
|
mock_response.json.side_effect = Exception()
|
|
mock_response.text = non_json
|
|
|
|
marathon_client, rpc_client = _create_fixtures()
|
|
rpc_client.http_req.return_value = mock_response
|
|
|
|
with pytest.raises(DCOSException) as exception_info:
|
|
invoke_method(marathon_client)
|
|
|
|
pattern = ('Error: Response from Marathon was not in expected JSON '
|
|
'format:\n(.*)')
|
|
actual_error = str(exception_info.value)
|
|
_assert_matches_with_groups(pattern, actual_error, (non_json,))
|
|
|
|
assert_test_case('not-json')
|
|
assert_test_case('{"oops"}')
|
|
|
|
|
|
def _assert_add_pod_raises_dcos_exception_if_deployment_id_missing(headers):
|
|
marathon_client, rpc_client = _create_fixtures()
|
|
rpc_client.http_req.return_value = _pod_response_fixture(headers)
|
|
result = marathon_client.add_pod({'some': 'json'})
|
|
|
|
assert result is None
|
|
|
|
|
|
def _assert_update_pod_raises_dcos_exception_if_deployment_id_missing(headers):
|
|
marathon_client, rpc_client = _create_fixtures()
|
|
rpc_client.http_req.return_value = _pod_response_fixture(headers)
|
|
|
|
with pytest.raises(DCOSException) as exception_info:
|
|
marathon_client.update_pod('foo', {'some': 'json'})
|
|
|
|
expected_error = ('Error: missing "Marathon-Deployment-Id" header from '
|
|
'Marathon response')
|
|
assert str(exception_info.value) == expected_error
|
|
|
|
|
|
def _assert_method_propagates_rpc_dcos_exception(invoke_method):
|
|
marathon_client, rpc_client = _create_fixtures()
|
|
rpc_client.http_req.side_effect = DCOSException('BOOM!')
|
|
|
|
with pytest.raises(DCOSException) as exception_info:
|
|
invoke_method(marathon_client)
|
|
|
|
assert str(exception_info.value) == 'BOOM!'
|
|
|
|
|
|
@mock.patch('dcos.http.head')
|
|
def _assert_pod_feature_supported_raises_exception(head_fn, exception):
|
|
rpc_client = rpcclient.RpcClient('http://base/url', timeout=22)
|
|
marathon_client = marathon.Client(rpc_client)
|
|
head_fn.side_effect = exception
|
|
|
|
with pytest.raises(exception.__class__) as exception_info:
|
|
marathon_client.pod_feature_supported()
|
|
|
|
assert exception_info.value == exception
|
|
|
|
|
|
def _assert_response_error_message_with_400_status_no_json(
|
|
reason, request_method, request_url):
|
|
message = rpcclient.RpcClient.response_error_message(
|
|
status_code=400,
|
|
reason=reason,
|
|
request_method=request_method,
|
|
request_url=request_url,
|
|
json_body=None)
|
|
|
|
pattern = r'Error on request \[(.*) (.*)\]: HTTP 400: (.*)'
|
|
groups = (request_method, request_url, reason)
|
|
_assert_matches_with_groups(pattern, message, groups)
|
|
|
|
|
|
def _assert_response_error_message_with_400_status_json(
|
|
response_json, printed_json):
|
|
message = rpcclient.RpcClient.response_error_message(
|
|
status_code=400,
|
|
reason=_REASON_X,
|
|
request_method=_METHOD_X,
|
|
request_url=_URL_X,
|
|
json_body=response_json)
|
|
|
|
error_line, json_lines = message.split('\n', 1)
|
|
pattern = r'Error on request \[(.*) (.*)\]: HTTP 400: (.*):'
|
|
groups = (_METHOD_X, _URL_X, _REASON_X)
|
|
|
|
_assert_matches_with_groups(pattern, error_line, groups)
|
|
assert json_lines == printed_json
|
|
|
|
|
|
def _assert_response_error_message_with_other_status_no_json(
|
|
status_code, reason, request_url):
|
|
message = rpcclient.RpcClient.response_error_message(
|
|
status_code=status_code,
|
|
reason=reason,
|
|
request_method=_METHOD_X,
|
|
request_url=request_url,
|
|
json_body=None)
|
|
|
|
pattern = r'Error decoding response from \[(.*)\]: HTTP (.*): (.*)'
|
|
groups = (request_url, str(status_code), reason)
|
|
_assert_matches_with_groups(pattern, message, groups)
|
|
|
|
|
|
def _assert_response_error_message_with_other_status_json_has_message(
|
|
status_code, json_message):
|
|
error_message = rpcclient.RpcClient.response_error_message(
|
|
status_code=status_code,
|
|
reason=_REASON_X,
|
|
request_method=_METHOD_X,
|
|
request_url=_URL_X,
|
|
json_body={'message': json_message})
|
|
|
|
pattern = r'Error: (.*)'
|
|
_assert_matches_with_groups(pattern, error_message, (json_message,))
|
|
|
|
|
|
def _assert_res_err_msg_with_other_status_json_no_message_has_valid_errors(
|
|
status_code, errors_json, errors_str):
|
|
message = rpcclient.RpcClient.response_error_message(
|
|
status_code=status_code,
|
|
reason=_REASON_X,
|
|
request_method=_METHOD_X,
|
|
request_url=_URL_X,
|
|
json_body={'errors': errors_json})
|
|
|
|
pattern = (r'Service likely misconfigured\. Please check your proxy or '
|
|
r'Service URL settings\. See dcos config --help\. (.*)')
|
|
_assert_matches_with_groups(pattern, message, (errors_str,))
|
|
|
|
|
|
def _assert_res_err_msg_with_other_status_invalid_json(status_code, json_body):
|
|
actual = rpcclient.RpcClient.response_error_message(
|
|
status_code=status_code,
|
|
reason=_REASON_X,
|
|
request_method=_METHOD_X,
|
|
request_url=_URL_X,
|
|
json_body=json_body)
|
|
|
|
expected = ('Service likely misconfigured. Please check your proxy or '
|
|
'Service URL settings. See dcos config --help. ')
|
|
assert actual == expected
|
|
|
|
|
|
def _assert_matches_with_groups(pattern, text, groups):
|
|
match = re.fullmatch(pattern, text, flags=re.DOTALL)
|
|
assert match
|
|
assert match.groups() == groups
|
|
|
|
|
|
def _create_fixtures():
|
|
rpc_client = mock.create_autospec(rpcclient.RpcClient)
|
|
marathon_client = marathon.Client(rpc_client)
|
|
|
|
return marathon_client, rpc_client
|
|
|
|
|
|
def _pod_response_fixture(headers=None):
|
|
mock_response = mock.create_autospec(requests.Response)
|
|
|
|
headers = CaseInsensitiveDict({} if headers is None else headers)
|
|
mock_response.headers = headers
|
|
|
|
return mock_response
|
|
|
|
|
|
_MESSAGE_1 = 'Oops!'
|
|
_MESSAGE_2 = 'Uh oh!'
|
|
_MESSAGE_X = "D'oh!"
|
|
|
|
_METHOD_1 = 'FOO'
|
|
_METHOD_2 = 'BAR'
|
|
_METHOD_X = 'ANY'
|
|
|
|
_PRINTED_JSON_1 = (
|
|
'{\n'
|
|
' "x": "err",\n'
|
|
' "y": 3.14,\n'
|
|
' "z": [\n'
|
|
' 1,\n'
|
|
' 2,\n'
|
|
' 3\n'
|
|
' ]\n'
|
|
'}')
|
|
_PRINTED_JSON_2 = (
|
|
'[\n'
|
|
' "something",\n'
|
|
' "completely",\n'
|
|
' "different"\n'
|
|
']')
|
|
|
|
_REASON_1 = 'Something Bad'
|
|
_REASON_2 = 'Another Reason'
|
|
_REASON_X = 'Arbitrary Reason'
|
|
|
|
_RESPONSE_JSON_1 = {"z": [1, 2, 3], "y": 3.14, "x": "err"}
|
|
_RESPONSE_JSON_2 = ["something", "completely", "different"]
|
|
|
|
_URL_1 = 'http://request/url'
|
|
_URL_2 = 'https://another/url'
|
|
_URL_X = 'http://does/not/matter'
|