From 2454e9e911171966f3e0e162cfc640af6208a2dd Mon Sep 17 00:00:00 2001 From: Nikolay Stanchev <ns17@it-innovation.soton.ac.uk> Date: Mon, 14 May 2018 13:51:32 +0100 Subject: [PATCH] Aggregator configuration API methods --- .gitignore | 2 ++ CLMCservice/__init__.py | 12 ++++++- CLMCservice/tests.py | 76 +++++++++++++++++++++++++++++++++++++++- CLMCservice/utilities.py | 17 +++++++++ CLMCservice/views.py | 49 +++++++++++++++++++++++--- production.ini | 1 + 6 files changed, 150 insertions(+), 7 deletions(-) create mode 100644 CLMCservice/utilities.py diff --git a/.gitignore b/.gitignore index 6f78280..8690a33 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,6 @@ ubuntu-xenial-16.04-cloudimg-console.log .idea/ *.egg *.pyc +.pytest_cache +.tox *$py.class diff --git a/CLMCservice/__init__.py b/CLMCservice/__init__.py index b87c12f..80dfcc7 100644 --- a/CLMCservice/__init__.py +++ b/CLMCservice/__init__.py @@ -1,10 +1,20 @@ from pyramid.config import Configurator +from pyramid.settings import asbool +from CLMCservice.views import AggregatorConfig def main(global_config, **settings): """ This function returns a Pyramid WSGI application.""" + # a conversion is necessary so that the configuration value of the aggregator is stored as bool and not as string + aggregator_running = asbool(settings.get('aggregator_running', 'false')) + settings['aggregator_running'] = aggregator_running + config = Configurator(settings=settings) - config.add_route('home', '/') + + config.add_route('aggregator', '/aggregator') + config.add_view(AggregatorConfig, attr='get', request_method='GET') + config.add_view(AggregatorConfig, attr='post', request_method='POST') + config.scan() return config.make_wsgi_app() diff --git a/CLMCservice/tests.py b/CLMCservice/tests.py index 54f2941..c475fa7 100644 --- a/CLMCservice/tests.py +++ b/CLMCservice/tests.py @@ -1 +1,75 @@ -# Specific tests related to the CLMC service \ No newline at end of file +import pytest +from pyramid import testing +from pyramid.httpexceptions import HTTPBadRequest + + +class TestAggregatorConfig(object): + """ + A pytest-implementation test for the aggregator configuration API calls + """ + + @pytest.fixture(autouse=True) + def app_config(self): + """ + A fixture to implement setUp/tearDown functionality for all tests by initializing configuration structure for the web service + """ + + self.config = testing.setUp() + self.config.add_settings({'aggregator_running': False}) + + yield + + testing.tearDown() + + def test_GET(self): + """ + Tests the GET method for the status of the aggregator. + """ + + from CLMCservice.views import AggregatorConfig # nested import so that importing the class view is part of the test itself + + assert not self.config.get_settings().get('aggregator_running'), "Initially aggregator is not running." + + request = testing.DummyRequest() + response = AggregatorConfig(request).get() + + assert type(response) == dict, "Response must be a dictionary representing a JSON object." + assert not response.get('aggregator_running'), "The response of the API call must return the aggregator status being set as False" + assert not self.config.get_settings().get('aggregator_running'), "A GET request must not modify the aggregator status." + + @pytest.mark.parametrize("input_val, output_val", [ + ("True", True), + ("true", True), + ("1", True), + ("False", False), + ("false", False), + ("0", False), + ("t", None), + ("f", None), + ]) + def test_POST(self, input_val, output_val): + """ + Tests the POST method for the status of the aggregator + :param input_val: the input form parameter + :param output_val: the expected output value, None for expecting an Exception + """ + + from CLMCservice.views import AggregatorConfig # nested import so that importing the class view is part of the test itself + + assert not self.config.get_settings().get('aggregator_running'), "Initially aggregator is not running." + + request = testing.DummyRequest() + + request.params['running'] = input_val + if output_val is not None: + response = AggregatorConfig(request).post() + assert response == {'aggregator_running': output_val}, "Response of POST request must include the new status of the aggregator" + assert self.config.get_settings().get('aggregator_running') == output_val, "Aggregator status must be updated to running." + else: + error_raised = False + try: + AggregatorConfig(request).post() + except HTTPBadRequest: + error_raised = True + + assert error_raised, "Error must be raised in case of an invalid argument." diff --git a/CLMCservice/utilities.py b/CLMCservice/utilities.py new file mode 100644 index 0000000..e17818a --- /dev/null +++ b/CLMCservice/utilities.py @@ -0,0 +1,17 @@ +def str_to_bool(value): + """ + A utility function to convert a string to boolean based on simple rules. + :param value: the value to convert + :return: True or False + :raises ValueError: if value cannot be converted to boolean + """ + + if type(value) is not str: + raise ValueError("This method only converts string to booolean.") + + if value in ('False', 'false', '0'): + return False + elif value in ('True', 'true', '1'): + return True + else: + raise ValueError("Invalid argument for conversion") diff --git a/CLMCservice/views.py b/CLMCservice/views.py index 10a8d11..4b92523 100644 --- a/CLMCservice/views.py +++ b/CLMCservice/views.py @@ -1,7 +1,46 @@ -from pyramid.view import view_config -from pyramid.response import Response +from pyramid.view import view_defaults +from pyramid.httpexceptions import HTTPBadRequest +from CLMCservice.utilities import str_to_bool -@view_config(route_name='home') -def my_view(request): - return Response("Hello world") + +@view_defaults(route_name='aggregator', renderer='json') +class AggregatorConfig(object): + """ + A class-based view for accessing and mutating the status of the aggregator. + """ + + def __init__(self, request): + """ + Initialises the instance of the view with the request argument. + :param request: client's call request + """ + + self.request = request + + def get(self): + """ + A GET API call for the status of the aggregator. + :return: A JSON response with the status of the aggregator. + """ + + aggregator_running = self.request.registry.settings.get('aggregator_running') + return {'aggregator_running': aggregator_running} + + def post(self): + """ + A POST API call for the status of the aggregator. + :return: A JSON response to the POST call (success or fail). + :raises HTTPBadRequest: if form argument cannot be converted to boolean + """ + + new_status = self.request.params.get('running') + + try: + new_status = str_to_bool(new_status) + except ValueError: + raise HTTPBadRequest("Bad request parameter - expected a boolean, received {0}".format(self.request.params.get('running'))) + + self.request.registry.settings['aggregator_running'] = new_status + # TODO start/stop aggregator based on value of new status + return {'aggregator_running': new_status} diff --git a/production.ini b/production.ini index 1331c1b..d127f3a 100644 --- a/production.ini +++ b/production.ini @@ -11,6 +11,7 @@ pyramid.debug_authorization = false pyramid.debug_notfound = false pyramid.debug_routematch = false pyramid.default_locale_name = en +aggregator_running = false ### # wsgi server configuration -- GitLab