barbican/barbican/tests/queue/test_server.py

382 lines
14 KiB
Python

# Copyright (c) 2013-2014 Rackspace, 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 datetime
from unittest import mock
from barbican.model import models
from barbican.model import repositories
from barbican.queue import server
from barbican.tasks import common
from barbican.tests import database_utils
from barbican.tests import utils
class WhenUsingTransactionalDecorator(utils.BaseTestCase):
"""Test using the 'transactional' decorator in server.py.
Note that only the 'I am a server' logic is tested here, as the alternate
mode is only used for direct invocation of Task methods in the standalone
server mode, which is also thoroughly tested in WhenUsingBeginTypeOrderTask
below.
"""
def setUp(self):
super(WhenUsingTransactionalDecorator, self).setUp()
# Ensure we always thing we are in 'I am a server' mode.
is_server_side_config = {
'return_value': True
}
self.is_server_side_patcher = mock.patch(
'barbican.queue.is_server_side',
**is_server_side_config
)
self.is_server_side_patcher.start()
self.commit_patcher = mock.patch(
'barbican.model.repositories.commit'
)
self.commit_mock = self.commit_patcher.start()
self.rollback_patcher = mock.patch(
'barbican.model.repositories.rollback'
)
self.rollback_mock = self.rollback_patcher.start()
self.clear_patcher = mock.patch(
'barbican.model.repositories.clear'
)
self.clear_mock = self.clear_patcher.start()
self.args = ('foo', 'bar')
self.kwargs = {'k_foo': 1, 'k_bar': 2}
# Class/decorator under test.
class TestClass(object):
my_args = None
my_kwargs = None
is_exception_needed = False
@server.transactional
def test_method(self, *args, **kwargs):
if self.is_exception_needed:
raise ValueError()
self.my_args = args
self.my_kwargs = kwargs
self.test_object = TestClass()
def tearDown(self):
super(WhenUsingTransactionalDecorator, self).tearDown()
self.is_server_side_patcher.stop()
self.commit_patcher.stop()
self.rollback_patcher.stop()
self.clear_patcher.stop()
def test_should_commit(self):
self.test_object.test_method(*self.args, **self.kwargs)
self.assertEqual(self.args, self.test_object.my_args)
self.assertEqual(self.kwargs, self.test_object.my_kwargs)
self.assertEqual(1, self.commit_mock.call_count)
self.assertEqual(0, self.rollback_mock.call_count)
self.assertEqual(1, self.clear_mock.call_count)
def test_should_rollback(self):
self.test_object.is_exception_needed = True
self.test_object.test_method(*self.args, **self.kwargs)
self.assertEqual(0, self.commit_mock.call_count)
self.assertEqual(1, self.rollback_mock.call_count)
self.assertEqual(1, self.clear_mock.call_count)
class WhenUsingRetryableOrderDecorator(utils.BaseTestCase):
"""Test using the 'retryable_order' decorator in server.py."""
def setUp(self):
super(WhenUsingRetryableOrderDecorator, self).setUp()
self.schedule_retry_tasks_patcher = mock.patch(
'barbican.queue.server.schedule_order_retry_tasks'
)
self.schedule_retry_tasks_mock = (
self.schedule_retry_tasks_patcher.start()
)
self.order_id = 'order-id'
self.args = ('foo', 'bar')
self.kwargs = {'k_foo': 1, 'k_bar': 2}
# Class/decorator under test.
class TestClass(object):
self.order_id = None
my_args = None
my_kwargs = None
is_exception_needed = False
result = common.FollowOnProcessingStatusDTO()
@server.retryable_order
def test_method(self, order_id, *args, **kwargs):
if self.is_exception_needed:
raise ValueError()
self.order_id = order_id
self.my_args = args
self.my_kwargs = kwargs
return self.result
self.test_object = TestClass()
self.test_method = TestClass.test_method
def tearDown(self):
super(WhenUsingRetryableOrderDecorator, self).tearDown()
self.schedule_retry_tasks_patcher.stop()
def test_should_successfully_schedule_a_task_for_retry(self):
self.test_object.test_method(self.order_id, *self.args, **self.kwargs)
self.assertEqual(self.order_id, self.test_object.order_id)
self.assertEqual(self.args, self.test_object.my_args)
self.assertEqual(self.kwargs, self.test_object.my_kwargs)
self.assertEqual(1, self.schedule_retry_tasks_mock.call_count)
self.schedule_retry_tasks_mock.assert_called_with(
mock.ANY,
self.test_object.result,
self.order_id,
*self.args,
**self.kwargs)
def test_retry_should_not_be_scheduled_if_exception_is_raised(self):
self.test_object.is_exception_needed = True
self.assertRaises(
ValueError,
self.test_object.test_method,
self.order_id,
self.args,
self.kwargs,
)
self.assertEqual(0, self.schedule_retry_tasks_mock.call_count)
class WhenCallingScheduleOrderRetryTasks(database_utils.RepositoryTestCase):
"""Test calling schedule_order_retry_tasks() in server.py."""
def setUp(self):
super(WhenCallingScheduleOrderRetryTasks, self).setUp()
self.project = database_utils.create_project()
self.order = database_utils.create_order(self.project)
database_utils.get_session().commit()
self.repo = repositories.OrderRetryTaskRepo()
self.result = common.FollowOnProcessingStatusDTO()
self.args = ['args-foo', 'args-bar']
self.kwargs = {'order_id': self.order.id, 'foo': 1, 'bar': 2}
self.date_to_retry_at = (
datetime.datetime.utcnow() + datetime.timedelta(
milliseconds=self.result.retry_msec)
)
def test_should_not_schedule_task_due_to_no_result(self):
retry_rpc_method = server.schedule_order_retry_tasks(None, None, None)
self.assertIsNone(retry_rpc_method)
def test_should_not_schedule_task_due_to_no_action_required_result(self):
self.result.retry_task = common.RetryTasks.NO_ACTION_REQUIRED
retry_rpc_method = server.schedule_order_retry_tasks(
None, self.result, None)
self.assertIsNone(retry_rpc_method)
def test_should_schedule_invoking_task_for_retry(self):
self.result.retry_task = common.RetryTasks.INVOKE_SAME_TASK
# Schedule this test method as the passed-in 'retry' function.
retry_rpc_method = server.schedule_order_retry_tasks(
self.test_should_schedule_invoking_task_for_retry,
self.result,
None, # Not used.
*self.args,
**self.kwargs)
database_utils.get_session().commit() # Flush to the database.
self.assertEqual(
'test_should_schedule_invoking_task_for_retry', retry_rpc_method)
def _verify_retry_task_entity(self, retry_task):
# Retrieve the task retry entity created above and verify it.
entities, offset, limit, total = self.repo.get_by_create_date()
self.assertEqual(1, total)
retry_model = entities[0]
self.assertEqual(retry_task, retry_model.retry_task)
self.assertEqual(self.args, retry_model.retry_args)
self.assertEqual(self.kwargs, retry_model.retry_kwargs)
self.assertEqual(0, retry_model.retry_count)
# Compare retry_at times.
# Note that the expected retry_at time is computed at setUp() time, but
# the retry_at time on the task retry entity/model is computed and set
# a few milliseconds after this setUp() time, hence they will vary by a
# small amount of time.
delta = retry_model.retry_at - self.date_to_retry_at
delta_seconds = delta.seconds
self.assertLessEqual(delta_seconds, 2)
class WhenCallingTasksMethod(utils.BaseTestCase):
"""Test calling methods on the Tasks class."""
def setUp(self):
super(WhenCallingTasksMethod, self).setUp()
# Mock the 'am I a server process?' flag used by the decorator around
# all task methods. Since this test class focuses on testing task
# method behaviors, this flag is set to false to allow for direct
# testing of these tasks without database transactional interference.
is_server_side_config = {
'return_value': False
}
self.is_server_side_patcher = mock.patch(
'barbican.queue.is_server_side',
**is_server_side_config
)
self.is_server_side_patcher.start()
self.tasks = server.Tasks()
def tearDown(self):
super(WhenCallingTasksMethod, self).tearDown()
self.is_server_side_patcher.stop()
@mock.patch('barbican.queue.server.schedule_order_retry_tasks')
@mock.patch('barbican.tasks.resources.BeginTypeOrder')
def test_should_process_begin_order(self, mock_begin_order, mock_schedule):
method = mock_begin_order.return_value.process_and_suppress_exceptions
method.return_value = 'result'
self.tasks.process_type_order(
None, self.order_id, self.external_project_id, self.request_id)
mock_process = mock_begin_order.return_value
mock_process.process_and_suppress_exceptions.assert_called_with(
self.order_id, self.external_project_id)
mock_schedule.assert_called_with(
mock.ANY, 'result', None, 'order1234',
'keystone1234', 'request1234')
@mock.patch('barbican.tasks.resources.BeginTypeOrder')
def test_process_order_catch_exception(self, mock_begin_order):
"""Test that BeginTypeOrder's process() handles all exceptions."""
mock_begin_order.return_value._process.side_effect = Exception()
self.tasks.process_type_order(None, self.order_id,
self.external_project_id,
self.request_id)
class WhenUsingTaskServer(database_utils.RepositoryTestCase):
"""Test using the asynchronous task client.
This test suite performs a full-stack test of worker-side task
processing (except for queue interactions, which are mocked). This
includes testing database commit and session close behaviors.
"""
def setUp(self):
super(WhenUsingTaskServer, self).setUp()
# Queue target mocking setup.
self.target = 'a target value here'
queue_get_target_config = {
'return_value': self.target
}
self.queue_get_target_patcher = mock.patch(
'barbican.queue.get_target',
**queue_get_target_config
)
self.queue_get_target_mock = self.queue_get_target_patcher.start()
# Queue server mocking setup.
self.server_mock = mock.MagicMock()
self.server_mock.start.return_value = None
self.server_mock.stop.return_value = None
queue_get_server_config = {
'return_value': self.server_mock
}
self.queue_get_server_patcher = mock.patch(
'barbican.queue.get_server',
**queue_get_server_config
)
self.queue_get_server_mock = self.queue_get_server_patcher.start()
self.server = server.TaskServer()
# Add an order to the in-memory database.
self.external_id = 'keystone-id'
project = database_utils.create_project(
external_id=self.external_id)
self.order = database_utils.create_order(
project=project)
self.request_id = 'request1234'
def tearDown(self):
super(WhenUsingTaskServer, self).tearDown()
self.queue_get_target_patcher.stop()
self.queue_get_server_patcher.stop()
def test_should_start(self):
self.server.start()
self.queue_get_target_mock.assert_called_with()
self.queue_get_server_mock.assert_called_with(
target=self.target, endpoints=[self.server])
self.server_mock.start.assert_called_with()
def test_should_stop(self):
self.server.stop()
self.queue_get_target_mock.assert_called_with()
self.queue_get_server_mock.assert_called_with(
target=self.target, endpoints=[self.server])
self.server_mock.stop.assert_called_with()
def test_process_bogus_begin_type_order_should_not_rollback(self):
order_id = self.order.id
self.order.type = 'bogus-type' # Force error out of business logic.
# Invoke process, including the transactional decorator that terminates
# the session when it is done. Hence we must re-retrieve the order for
# verification afterwards.
self.server.process_type_order(
None, self.order.id, self.external_id, self.request_id)
order_repo = repositories.get_order_repository()
order_result = order_repo.get(order_id, self.external_id)
self.assertEqual(models.States.ERROR, order_result.status)
self.assertEqual(
('Process TypeOrder failure seen - '
'please contact site administrator.'),
order_result.error_reason)
self.assertEqual(
'500',
order_result.error_status_code)