mistral/mistral/tests/unit/api/v2/test_action_executions.py
Sean McGinnis 545a34c751
Use unittest.mock instead of third party mock
Now that we no longer support py27, we can use the standard library
unittest.mock module instead of the third party mock lib.

Change-Id: I6a4524c3e0458747646995615535e85cde2e155f
Signed-off-by: Sean McGinnis <sean.mcginnis@gmail.com>
2020-04-18 11:54:27 -05:00

700 lines
22 KiB
Python

# Copyright 2015 - Mirantis, Inc.
# Copyright 2016 - Brocade Communications Systems, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import datetime
import json
from unittest import mock
from oslo_config import cfg
import oslo_messaging
from oslo_messaging import exceptions as oslo_exc
import sqlalchemy as sa
from mistral.api.controllers.v2 import action_execution
from mistral.api.controllers.v2 import resources
from mistral.db.v2 import api as db_api
from mistral.db.v2.sqlalchemy import models
from mistral import exceptions as exc
from mistral.rpc import clients as rpc_clients
from mistral.rpc.oslo import oslo_client
from mistral.tests.unit.api import base
from mistral.utils import rest_utils
from mistral.workflow import states
from mistral_lib import actions as ml_actions
# This line is needed for correct initialization of messaging config.
oslo_messaging.get_rpc_transport(cfg.CONF)
ACTION_EX_DB = models.ActionExecution(
id='123',
workflow_name='flow',
task_execution=models.TaskExecution(name='task1'),
task_execution_id='333',
state=states.SUCCESS,
state_info=states.SUCCESS,
tags=['foo', 'fee'],
name='std.echo',
description='something',
accepted=True,
input={},
output={},
created_at=datetime.datetime(1970, 1, 1),
updated_at=datetime.datetime(1970, 1, 1)
)
AD_HOC_ACTION_EX_DB = models.ActionExecution(
id='123',
state=states.SUCCESS,
state_info=states.SUCCESS,
tags=['foo', 'fee'],
name='std.echo',
description='something',
accepted=True,
input={},
output={},
created_at=datetime.datetime(1970, 1, 1),
updated_at=datetime.datetime(1970, 1, 1)
)
AD_HOC_ACTION_EX_ERROR = models.ActionExecution(
id='123',
state=states.ERROR,
state_info=states.ERROR,
tags=['foo', 'fee'],
name='std.echo',
description='something',
accepted=True,
input={},
output={},
created_at=datetime.datetime(1970, 1, 1),
updated_at=datetime.datetime(1970, 1, 1)
)
AD_HOC_ACTION_EX_CANCELLED = models.ActionExecution(
id='123',
state=states.CANCELLED,
state_info=states.CANCELLED,
tags=['foo', 'fee'],
name='std.echo',
description='something',
accepted=True,
input={},
output={},
created_at=datetime.datetime(1970, 1, 1),
updated_at=datetime.datetime(1970, 1, 1)
)
ACTION_EX_DB_NOT_COMPLETE = models.ActionExecution(
id='123',
state=states.RUNNING,
state_info=states.RUNNING,
tags=['foo', 'fee'],
name='std.echo',
description='something',
accepted=False,
input={},
output={},
created_at=datetime.datetime(1970, 1, 1),
updated_at=datetime.datetime(1970, 1, 1)
)
ACTION_EX = {
'id': '123',
'workflow_name': 'flow',
'task_execution_id': '333',
'task_name': 'task1',
'state': 'SUCCESS',
'state_info': 'SUCCESS',
'tags': ['foo', 'fee'],
'name': 'std.echo',
'description': 'something',
'accepted': True,
'input': '{}',
'output': '{}',
'created_at': '1970-01-01 00:00:00',
'updated_at': '1970-01-01 00:00:00'
}
UPDATED_ACTION_EX_DB = copy.copy(ACTION_EX_DB).to_dict()
UPDATED_ACTION_EX_DB['state'] = 'SUCCESS'
UPDATED_ACTION_EX_DB['task_name'] = 'task1'
UPDATED_ACTION = copy.deepcopy(ACTION_EX)
UPDATED_ACTION['state'] = 'SUCCESS'
UPDATED_ACTION_OUTPUT = UPDATED_ACTION['output']
CANCELLED_ACTION_EX_DB = copy.copy(ACTION_EX_DB).to_dict()
CANCELLED_ACTION_EX_DB['state'] = 'CANCELLED'
CANCELLED_ACTION_EX_DB['task_name'] = 'task1'
CANCELLED_ACTION = copy.deepcopy(ACTION_EX)
CANCELLED_ACTION['state'] = 'CANCELLED'
PAUSED_ACTION_EX_DB = copy.copy(ACTION_EX_DB).to_dict()
PAUSED_ACTION_EX_DB['state'] = 'PAUSED'
PAUSED_ACTION_EX_DB['task_name'] = 'task1'
PAUSED_ACTION = copy.deepcopy(ACTION_EX)
PAUSED_ACTION['state'] = 'PAUSED'
RUNNING_ACTION_EX_DB = copy.copy(ACTION_EX_DB).to_dict()
RUNNING_ACTION_EX_DB['state'] = 'RUNNING'
RUNNING_ACTION_EX_DB['task_name'] = 'task1'
RUNNING_ACTION = copy.deepcopy(ACTION_EX)
RUNNING_ACTION['state'] = 'RUNNING'
ERROR_ACTION_EX = copy.copy(ACTION_EX_DB).to_dict()
ERROR_ACTION_EX['state'] = 'ERROR'
ERROR_ACTION_EX['task_name'] = 'task1'
ERROR_ACTION = copy.deepcopy(ACTION_EX)
ERROR_ACTION['state'] = 'ERROR'
ERROR_ACTION_RES = ERROR_ACTION['output']
ERROR_OUTPUT = "Fake error, it is a test"
ERROR_ACTION_EX_WITH_OUTPUT = copy.copy(ACTION_EX_DB).to_dict()
ERROR_ACTION_EX_WITH_OUTPUT['state'] = 'ERROR'
ERROR_ACTION_EX_WITH_OUTPUT['task_name'] = 'task1'
ERROR_ACTION_EX_WITH_OUTPUT['output'] = {"output": ERROR_OUTPUT}
ERROR_ACTION_WITH_OUTPUT = copy.deepcopy(ACTION_EX)
ERROR_ACTION_WITH_OUTPUT['state'] = 'ERROR'
ERROR_ACTION_WITH_OUTPUT['output'] = (
'{"output": "%s"}' % ERROR_OUTPUT
)
ERROR_ACTION_RES_WITH_OUTPUT = {"output": ERROR_OUTPUT}
DEFAULT_ERROR_OUTPUT = "Unknown error"
ERROR_ACTION_EX_FOR_EMPTY_OUTPUT = copy.copy(ACTION_EX_DB).to_dict()
ERROR_ACTION_EX_FOR_EMPTY_OUTPUT['state'] = 'ERROR'
ERROR_ACTION_EX_FOR_EMPTY_OUTPUT['task_name'] = 'task1'
ERROR_ACTION_EX_FOR_EMPTY_OUTPUT['output'] = {"output": DEFAULT_ERROR_OUTPUT}
ERROR_ACTION_FOR_EMPTY_OUTPUT = copy.deepcopy(ERROR_ACTION)
ERROR_ACTION_FOR_EMPTY_OUTPUT['output'] = (
'{"output": "%s"}' % DEFAULT_ERROR_OUTPUT
)
ERROR_ACTION_WITH_NONE_OUTPUT = copy.deepcopy(ERROR_ACTION)
ERROR_ACTION_WITH_NONE_OUTPUT['output'] = None
BROKEN_ACTION = copy.deepcopy(ACTION_EX)
BROKEN_ACTION['output'] = 'string not escaped'
MOCK_ACTION = mock.MagicMock(return_value=ACTION_EX_DB)
MOCK_ACTION_NOT_COMPLETE = mock.MagicMock(
return_value=ACTION_EX_DB_NOT_COMPLETE
)
MOCK_ACTION_COMPLETE_ERROR = mock.MagicMock(
return_value=AD_HOC_ACTION_EX_ERROR
)
MOCK_ACTION_COMPLETE_CANCELLED = mock.MagicMock(
return_value=AD_HOC_ACTION_EX_CANCELLED
)
MOCK_AD_HOC_ACTION = mock.MagicMock(return_value=AD_HOC_ACTION_EX_DB)
MOCK_ACTIONS = mock.MagicMock(return_value=[ACTION_EX_DB])
MOCK_EMPTY = mock.MagicMock(return_value=[])
MOCK_NOT_FOUND = mock.MagicMock(side_effect=exc.DBEntityNotFoundError())
MOCK_DELETE = mock.MagicMock(return_value=None)
ACTION_EX_DB_WITH_PROJECT_ID = AD_HOC_ACTION_EX_DB.get_clone()
ACTION_EX_DB_WITH_PROJECT_ID.project_id = '<default-project>'
class TestActionExecutionsController(base.APITest):
def setUp(self):
super(TestActionExecutionsController, self).setUp()
self.addCleanup(
cfg.CONF.set_default,
'allow_action_execution_deletion',
False,
group='api'
)
@mock.patch.object(db_api, 'get_action_execution', MOCK_ACTION)
def test_get(self):
resp = self.app.get('/v2/action_executions/123')
self.assertEqual(200, resp.status_int)
self.assertDictEqual(ACTION_EX, resp.json)
@mock.patch.object(db_api, 'get_action_execution')
def test_get_operational_error(self, mocked_get):
mocked_get.side_effect = [
# Emulating DB OperationalError
sa.exc.OperationalError('Mock', 'mock', 'mock'),
ACTION_EX_DB # Successful run
]
resp = self.app.get('/v2/action_executions/123')
self.assertEqual(200, resp.status_int)
self.assertDictEqual(ACTION_EX, resp.json)
def test_basic_get(self):
resp = self.app.get('/v2/action_executions/')
self.assertEqual(200, resp.status_int)
@mock.patch.object(db_api, 'get_action_execution', MOCK_NOT_FOUND)
def test_get_not_found(self):
resp = self.app.get('/v2/action_executions/123', expect_errors=True)
self.assertEqual(404, resp.status_int)
@mock.patch.object(db_api, 'get_action_execution',
return_value=ACTION_EX_DB_WITH_PROJECT_ID)
def test_get_within_project_id(self, mock_get):
resp = self.app.get('/v2/action_executions/123')
self.assertEqual(200, resp.status_int)
self.assertTrue('project_id' in resp.json)
@mock.patch.object(oslo_client.OsloRPCClient, 'sync_call',
mock.MagicMock(side_effect=oslo_exc.MessagingTimeout))
def test_post_timeout(self):
self.override_config('rpc_response_timeout', 100)
resp = self.app.post_json(
'/v2/action_executions',
{
'name': 'std.sleep',
'input': {'seconds': 120}
},
expect_errors=True
)
error_msg = resp.json['faultstring']
self.assertEqual(error_msg,
'This rpc call "start_action" took longer than '
'configured 100 seconds.')
@mock.patch.object(rpc_clients.EngineClient, 'start_action')
def test_post(self, f):
f.return_value = ACTION_EX_DB.to_dict()
resp = self.app.post_json(
'/v2/action_executions',
{
'name': 'std.echo',
'input': "{}",
'params': '{"save_result": true, "run_sync": true}'
}
)
self.assertEqual(201, resp.status_int)
action_exec = copy.deepcopy(ACTION_EX)
del action_exec['task_name']
self.assertDictEqual(action_exec, resp.json)
f.assert_called_once_with(
action_exec['name'],
json.loads(action_exec['input']),
description=None,
save_result=True,
run_sync=True,
namespace=''
)
@mock.patch.object(rpc_clients.EngineClient, 'start_action')
def test_post_with_timeout(self, f):
f.return_value = ACTION_EX_DB.to_dict()
resp = self.app.post_json(
'/v2/action_executions',
{
'name': 'std.echo',
'input': "{}",
'params': '{"timeout": 2}'
}
)
self.assertEqual(201, resp.status_int)
action_exec = copy.deepcopy(ACTION_EX)
del action_exec['task_name']
self.assertDictEqual(action_exec, resp.json)
f.assert_called_once_with(
action_exec['name'],
json.loads(action_exec['input']),
description=None,
timeout=2,
namespace=''
)
@mock.patch.object(rpc_clients.EngineClient, 'start_action')
def test_post_json(self, f):
f.return_value = ACTION_EX_DB.to_dict()
resp = self.app.post_json(
'/v2/action_executions',
{
'name': 'std.echo',
'input': {},
'params': '{"save_result": true}'
}
)
self.assertEqual(201, resp.status_int)
action_exec = copy.deepcopy(ACTION_EX)
del action_exec['task_name']
self.assertDictEqual(action_exec, resp.json)
f.assert_called_once_with(
action_exec['name'],
json.loads(action_exec['input']),
description=None,
save_result=True,
namespace=''
)
@mock.patch.object(rpc_clients.EngineClient, 'start_action')
def test_post_without_input(self, f):
f.return_value = ACTION_EX_DB.to_dict()
f.return_value['output'] = {'result': '123'}
resp = self.app.post_json(
'/v2/action_executions',
{'name': 'nova.servers_list'}
)
self.assertEqual(201, resp.status_int)
self.assertEqual('{"result": "123"}', resp.json['output'])
f.assert_called_once_with('nova.servers_list', {},
description=None,
namespace='')
def test_post_bad_result(self):
resp = self.app.post_json(
'/v2/action_executions',
{'input': 'null'},
expect_errors=True
)
self.assertEqual(400, resp.status_int)
def test_post_bad_input(self):
resp = self.app.post_json(
'/v2/action_executions',
{'input': None},
expect_errors=True
)
self.assertEqual(400, resp.status_int)
def test_post_bad_json_input(self):
resp = self.app.post_json(
'/v2/action_executions',
{'input': 2},
expect_errors=True
)
self.assertEqual(400, resp.status_int)
@mock.patch.object(rpc_clients.EngineClient, 'on_action_complete')
def test_put(self, f):
f.return_value = UPDATED_ACTION_EX_DB
resp = self.app.put_json('/v2/action_executions/123', UPDATED_ACTION)
self.assertEqual(200, resp.status_int)
self.assertDictEqual(UPDATED_ACTION, resp.json)
f.assert_called_once_with(
UPDATED_ACTION['id'],
ml_actions.Result(data=ACTION_EX_DB.output)
)
@mock.patch.object(rpc_clients.EngineClient, 'on_action_complete')
def test_put_error_with_output(self, f):
f.return_value = ERROR_ACTION_EX_WITH_OUTPUT
resp = self.app.put_json(
'/v2/action_executions/123',
ERROR_ACTION_WITH_OUTPUT
)
self.assertEqual(200, resp.status_int)
self.assertDictEqual(ERROR_ACTION_WITH_OUTPUT, resp.json)
f.assert_called_once_with(
ERROR_ACTION_WITH_OUTPUT['id'],
ml_actions.Result(error=ERROR_ACTION_RES_WITH_OUTPUT)
)
@mock.patch.object(rpc_clients.EngineClient, 'on_action_complete')
def test_put_error_with_unknown_reason(self, f):
f.return_value = ERROR_ACTION_EX_FOR_EMPTY_OUTPUT
resp = self.app.put_json('/v2/action_executions/123', ERROR_ACTION)
self.assertEqual(200, resp.status_int)
self.assertDictEqual(ERROR_ACTION_FOR_EMPTY_OUTPUT, resp.json)
f.assert_called_once_with(
ERROR_ACTION_FOR_EMPTY_OUTPUT['id'],
ml_actions.Result(error=DEFAULT_ERROR_OUTPUT)
)
@mock.patch.object(rpc_clients.EngineClient, 'on_action_complete')
def test_put_error_with_unknown_reason_output_none(self, f):
f.return_value = ERROR_ACTION_EX_FOR_EMPTY_OUTPUT
resp = self.app.put_json(
'/v2/action_executions/123',
ERROR_ACTION_WITH_NONE_OUTPUT
)
self.assertEqual(200, resp.status_int)
self.assertDictEqual(ERROR_ACTION_FOR_EMPTY_OUTPUT, resp.json)
f.assert_called_once_with(
ERROR_ACTION_FOR_EMPTY_OUTPUT['id'],
ml_actions.Result(error=DEFAULT_ERROR_OUTPUT)
)
@mock.patch.object(rpc_clients.EngineClient, 'on_action_complete')
def test_put_cancelled(self, on_action_complete_mock_func):
on_action_complete_mock_func.return_value = CANCELLED_ACTION_EX_DB
resp = self.app.put_json('/v2/action_executions/123', CANCELLED_ACTION)
self.assertEqual(200, resp.status_int)
self.assertDictEqual(CANCELLED_ACTION, resp.json)
on_action_complete_mock_func.assert_called_once_with(
CANCELLED_ACTION['id'],
ml_actions.Result(cancel=True)
)
@mock.patch.object(rpc_clients.EngineClient, 'on_action_update')
def test_put_paused(self, on_action_update_mock_func):
on_action_update_mock_func.return_value = PAUSED_ACTION_EX_DB
resp = self.app.put_json('/v2/action_executions/123', PAUSED_ACTION)
self.assertEqual(200, resp.status_int)
self.assertDictEqual(PAUSED_ACTION, resp.json)
on_action_update_mock_func.assert_called_once_with(
PAUSED_ACTION['id'],
PAUSED_ACTION['state']
)
@mock.patch.object(rpc_clients.EngineClient, 'on_action_update')
def test_put_resume(self, on_action_update_mock_func):
on_action_update_mock_func.return_value = RUNNING_ACTION_EX_DB
resp = self.app.put_json('/v2/action_executions/123', RUNNING_ACTION)
self.assertEqual(200, resp.status_int)
self.assertDictEqual(RUNNING_ACTION, resp.json)
on_action_update_mock_func.assert_called_once_with(
RUNNING_ACTION['id'],
RUNNING_ACTION['state']
)
@mock.patch.object(
rpc_clients.EngineClient,
'on_action_complete',
MOCK_NOT_FOUND
)
def test_put_no_action_ex(self):
resp = self.app.put_json(
'/v2/action_executions/123',
UPDATED_ACTION,
expect_errors=True
)
self.assertEqual(404, resp.status_int)
def test_put_bad_state(self):
action = copy.deepcopy(ACTION_EX)
action['state'] = 'DELAYED'
resp = self.app.put_json(
'/v2/action_executions/123',
action,
expect_errors=True
)
self.assertEqual(400, resp.status_int)
self.assertIn('Expected one of', resp.json['faultstring'])
def test_put_bad_result(self):
resp = self.app.put_json(
'/v2/action_executions/123',
BROKEN_ACTION,
expect_errors=True
)
self.assertEqual(400, resp.status_int)
@mock.patch.object(rpc_clients.EngineClient, 'on_action_complete')
def test_put_without_result(self, f):
action_ex = copy.deepcopy(UPDATED_ACTION)
del action_ex['output']
f.return_value = UPDATED_ACTION_EX_DB
resp = self.app.put_json('/v2/action_executions/123', action_ex)
self.assertEqual(200, resp.status_int)
@mock.patch.object(db_api, 'get_action_executions', MOCK_ACTIONS)
def test_get_all(self):
resp = self.app.get('/v2/action_executions')
self.assertEqual(200, resp.status_int)
self.assertEqual(1, len(resp.json['action_executions']))
self.assertDictEqual(ACTION_EX, resp.json['action_executions'][0])
@mock.patch.object(db_api, 'get_action_executions')
def test_get_all_operational_error(self, mocked_get_all):
mocked_get_all.side_effect = [
# Emulating DB OperationalError
sa.exc.OperationalError('Mock', 'mock', 'mock'),
[ACTION_EX_DB] # Successful run
]
resp = self.app.get('/v2/action_executions')
self.assertEqual(200, resp.status_int)
self.assertEqual(1, len(resp.json['action_executions']))
self.assertDictEqual(ACTION_EX, resp.json['action_executions'][0])
@mock.patch.object(rest_utils, 'get_all',
return_value=resources.ActionExecutions())
def test_get_all_without_output(self, mock_get_all):
resp = self.app.get('/v2/action_executions')
args, kwargs = mock_get_all.call_args
resource_function = kwargs['resource_function']
self.assertEqual(200, resp.status_int)
self.assertEqual(
action_execution._get_action_execution_resource_for_list,
resource_function
)
@mock.patch.object(rest_utils, 'get_all',
return_value=resources.ActionExecutions())
def test_get_all_with_output(self, mock_get_all):
resp = self.app.get('/v2/action_executions?include_output=true')
args, kwargs = mock_get_all.call_args
resource_function = kwargs['resource_function']
self.assertEqual(200, resp.status_int)
self.assertEqual(
action_execution._get_action_execution_resource,
resource_function
)
@mock.patch.object(db_api, 'get_action_executions', MOCK_EMPTY)
def test_get_all_empty(self):
resp = self.app.get('/v2/action_executions')
self.assertEqual(200, resp.status_int)
self.assertEqual(0, len(resp.json['action_executions']))
@mock.patch.object(db_api, 'get_action_execution', MOCK_AD_HOC_ACTION)
@mock.patch.object(db_api, 'delete_action_execution', MOCK_DELETE)
def test_delete(self):
cfg.CONF.set_default('allow_action_execution_deletion', True, 'api')
resp = self.app.delete('/v2/action_executions/123')
self.assertEqual(204, resp.status_int)
@mock.patch.object(db_api, 'get_action_execution', MOCK_NOT_FOUND)
def test_delete_not_found(self):
cfg.CONF.set_default('allow_action_execution_deletion', True, 'api')
resp = self.app.delete('/v2/action_executions/123', expect_errors=True)
self.assertEqual(404, resp.status_int)
def test_delete_not_allowed(self):
resp = self.app.delete('/v2/action_executions/123', expect_errors=True)
self.assertEqual(403, resp.status_int)
self.assertIn(
"Action execution deletion is not allowed",
resp.body.decode()
)
@mock.patch.object(db_api, 'get_action_execution', MOCK_ACTION)
def test_delete_action_execution_with_task(self):
cfg.CONF.set_default('allow_action_execution_deletion', True, 'api')
resp = self.app.delete('/v2/action_executions/123', expect_errors=True)
self.assertEqual(403, resp.status_int)
self.assertIn(
"Only ad-hoc action execution can be deleted",
resp.body.decode()
)
@mock.patch.object(
db_api,
'get_action_execution',
MOCK_ACTION_NOT_COMPLETE
)
def test_delete_action_execution_not_complete(self):
cfg.CONF.set_default('allow_action_execution_deletion', True, 'api')
resp = self.app.delete('/v2/action_executions/123', expect_errors=True)
self.assertEqual(403, resp.status_int)
self.assertIn(
"Only completed action execution can be deleted",
resp.body.decode()
)
@mock.patch.object(
db_api,
'get_action_execution',
MOCK_ACTION_COMPLETE_ERROR
)
@mock.patch.object(db_api, 'delete_action_execution', MOCK_DELETE)
def test_delete_action_execution_complete_error(self):
cfg.CONF.set_default('allow_action_execution_deletion', True, 'api')
resp = self.app.delete('/v2/action_executions/123', expect_errors=True)
self.assertEqual(204, resp.status_int)
@mock.patch.object(
db_api,
'get_action_execution',
MOCK_ACTION_COMPLETE_CANCELLED
)
@mock.patch.object(db_api, 'delete_action_execution', MOCK_DELETE)
def test_delete_action_execution_complete_cancelled(self):
cfg.CONF.set_default('allow_action_execution_deletion', True, 'api')
resp = self.app.delete('/v2/action_executions/123', expect_errors=True)
self.assertEqual(204, resp.status_int)