diff options
author | Milo Casagrande <milo.casagrande@linaro.org> | 2014-11-12 17:35:34 +0100 |
---|---|---|
committer | Milo Casagrande <milo.casagrande@linaro.org> | 2014-11-12 17:35:34 +0100 |
commit | fa9c001a2b88a9c7ee43015528c4e938cb46c0db (patch) | |
tree | e1a10cd8173a06d124bd57e66d3412763e0010b2 | |
parent | a6d2a1d4df3c2544fa0425dc492b11fcca1c70ef (diff) |
Add /lab URL handler.
Change-Id: I89a770993556b1c1795108e200a0e2bfb956c761
-rw-r--r-- | app/handlers/base.py | 9 | ||||
-rw-r--r-- | app/handlers/common.py | 26 | ||||
-rw-r--r-- | app/handlers/lab.py | 284 | ||||
-rw-r--r-- | app/handlers/response.py | 2 | ||||
-rw-r--r-- | app/handlers/tests/test_lab_handler.py | 396 | ||||
-rw-r--r-- | app/models/__init__.py | 1 | ||||
-rw-r--r-- | app/models/lab.py | 14 | ||||
-rw-r--r-- | app/urls.py | 43 | ||||
-rw-r--r-- | app/utils/db.py | 10 | ||||
-rw-r--r-- | app/utils/tests/test_validator.py | 83 | ||||
-rw-r--r-- | app/utils/validator.py | 38 |
11 files changed, 873 insertions, 33 deletions
diff --git a/app/handlers/base.py b/app/handlers/base.py index 4fdde92..d8271a7 100644 --- a/app/handlers/base.py +++ b/app/handlers/base.py @@ -1,5 +1,3 @@ -# Copyright (C) 2014 Linaro Ltd. -# # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the @@ -29,7 +27,7 @@ import models import utils import utils.db import utils.log -import utils.validator as utilsv +import utils.validator as validator STATUS_MESSAGES = { @@ -190,7 +188,7 @@ class BaseHandler(tornado.web.RequestHandler): try: json_obj = json.loads(self.request.body.decode('utf8')) - valid_json, j_reason = utilsv.is_valid_json( + valid_json, j_reason = validator.is_valid_json( json_obj, self._valid_keys("POST") ) if valid_json: @@ -207,7 +205,8 @@ class BaseHandler(tornado.web.RequestHandler): else: response.reason = "Provided JSON is not valid" response.result = None - except ValueError: + except ValueError, ex: + self.log.exception(ex) error = "No JSON data found in the POST request" self.log.error(error) response = handr.HandlerResponse(422) diff --git a/app/handlers/common.py b/app/handlers/common.py index c7222aa..f9002e3 100644 --- a/app/handlers/common.py +++ b/app/handlers/common.py @@ -188,6 +188,32 @@ BATCH_VALID_KEYS = { ] } +LAB_VALID_KEYS = { + "POST": { + models.MANDATORY_KEYS: [ + models.CONTACT_KEY, + models.NAME_KEY, + ], + models.ACCEPTED_KEYS: [ + models.ADDRESS_KEY, + models.CONTACT_KEY, + models.NAME_KEY, + models.PRIVATE_KEY, + models.TOKEN_KEY, + ] + }, + "GET": [ + models.ADDRESS_KEY, + models.CONTACT_KEY, + models.CREATED_KEY, + models.ID_KEY, + models.NAME_KEY, + models.PRIVATE_KEY, + models.TOKEN_KEY, + models.UPDATED_KEY, + ] +} + MASTER_KEY = 'master_key' API_TOKEN_HEADER = 'Authorization' ACCEPTED_CONTENT_TYPE = 'application/json' diff --git a/app/handlers/lab.py b/app/handlers/lab.py new file mode 100644 index 0000000..12a7da0 --- /dev/null +++ b/app/handlers/lab.py @@ -0,0 +1,284 @@ +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +"""Handler for the /lab URLs.""" + +from urlparse import urlunparse + +import datetime +import bson +import handlers.base +import handlers.common +import handlers.response as hresponse +import models +import models.lab as mlab +import models.token as mtoken +import utils.validator as validator +import utils.db + + +# pylint: disable=too-many-public-methods +class LabHandler(handlers.base.BaseHandler): + """Handle all traffic through the /lab URLs.""" + + def __init__(self, application, request, **kwargs): + super(LabHandler, self).__init__(application, request, **kwargs) + + @property + def collection(self): + return self.db[models.LAB_COLLECTION] + + @staticmethod + def _valid_keys(method): + return handlers.common.LAB_VALID_KEYS.get(method, None) + + @staticmethod + def _token_validation_func(): + return handlers.common.valid_token_th + + def _post(self, *args, **kwargs): + response = hresponse.HandlerResponse(201) + + json_obj = kwargs["json_obj"] + + valid_contact, reason = validator.is_valid_lab_contact_data(json_obj) + if valid_contact: + lab_name = kwargs.get("id", None) + status_code, reason, result, headers = self._create_or_update( + json_obj, lab_name) + + response.status_code = status_code + response.result = result + if reason: + if kwargs["reason"]: + reason += "\n" + kwargs["reason"] + response.reason = reason + if headers: + response.headers = headers + else: + response.status_code = 400 + if reason: + if kwargs["reason"]: + reason += "\n" + kwargs["reason"] + response.reason = reason + + return response + + def _create_or_update(self, json_obj, lab_name): + """Create or update a new lab object. + + If the request comes in with a specified lab name, it will be treated + as an update request. + + :param json_obj: The JSON data as sent in the request. + :type json_obj: dict + :param lab_name: The ID part of the request. + :type lab_name: str + :return A tuple with: status code, reason, result and headers. + """ + status_code = None + reason = None + result = None + headers = None + + if lab_name: + old_lab = utils.db.find_one( + self.collection, [lab_name], field='name' + ) + else: + name = json_obj.get(models.NAME_KEY) + old_lab = utils.db.find_one(self.collection, [name], field='name') + + if all([lab_name, old_lab]): + self.log.info( + "Updating lab with ID '%s' from IP address %s", + old_lab.get(models.ID_KEY), self.request.remote_ip + ) + status_code, reason, result, headers = self._update_lab( + json_obj, old_lab + ) + elif all([lab_name, not old_lab]): + status_code = 404 + reason = "Lab with name '%s' not found" % lab_name + elif all([not old_lab, not lab_name]): + self.log.info("Creating new lab object") + status_code, reason, result, headers = self._create_new_lab( + json_obj) + else: + status_code = 400 + reason = "Lab with name '%s' already exists" % lab_name + + return status_code, reason, result, headers + + def _update_lab(self, json_obj, old_lab): + """Update an existing lab object. + + :param json_obj: The JSON object with the lab data. + :type json_obj: dict + :param old_lab: The JSON object of the lab from the db. + :type old_lab: dict + :return A tuple with: status code, reason, result and headers. + """ + status_code = None + reason = None + result = None + headers = None + + # Locally used to store the contact information from the new lab object. + new_contact = None + + old_lab = mlab.LabDocument.from_json(old_lab) + new_lab = mlab.LabDocument.from_json(json_obj) + + if new_lab.contact: + if old_lab.contact != new_lab.contact: + old_lab.contact = new_lab.contact + new_contact = new_lab.contact + + if new_lab.token: + self._update_lab_token(old_lab, new_lab, new_contact) + + if new_lab.address: + if old_lab.address != new_lab.address: + old_lab.address = new_lab.address + + if old_lab.private != new_lab.private: + old_lab.private = new_lab.private + + old_lab.updated_on = datetime.datetime.now(tz=bson.tz_util.utc) + + status_code, _ = utils.db.save(self.db, old_lab) + if status_code != 201: + reason = "Error updating lab '%s'" % old_lab.name + else: + reason = "Lab '%s' updated" % old_lab.name + status_code = 200 + + return status_code, reason, result, headers + + def _update_lab_token(self, old_lab, new_lab, new_contact): + """Update references of lab token. + + :param old_lab: The lab object as found in the database. + :type old_lab: LabDocument + :param new_lab: The new lab object as passed by the user. + :type new_lab: LabDocument + :param new_contact: The contact information as found in the new lab + document. + :type new_contact: dict + """ + # If the user specifies a new token, it will be doing so using the + # actual token value, not its ID. We need to make sure we still + # have the old token as defined in the old lab document, find the + # new token and update accordingly using the token ID. + old_token = utils.db.find_one( + self.db[models.TOKEN_COLLECTION], + [old_lab.token], field=models.TOKEN_KEY + ) + new_token = utils.db.find_one( + self.db[models.TOKEN_COLLECTION], + [new_lab.token], field=models.TOKEN_KEY + ) + + if old_token: + old_token = mtoken.Token.from_json(old_token) + if new_token: + new_token = mtoken.Token.from_json(new_token) + + if all([old_token, new_token]): + # Both old and new tokens? + # Expire the old one and save it. + if old_token.token != new_token.token: + old_lab.token = new_token.id + + old_token.expired = True + ret_code, _ = utils.db.save(self.db, old_token) + if ret_code != 201: + self.log.warn("Error expiring old token '%s'", old_token.id) + + if all([old_token, not new_token, new_contact]): + # Just the old token? + # Make sure its contact information are correct and save it. + old_token.username = ( + new_contact[models.NAME_KEY] + + " " + + new_contact[models.SURNAME_KEY] + ) + old_token.email = new_contact[models.EMAIL_KEY] + ret_code, _ = utils.db.save(self.db, old_token) + if ret_code != 201: + self.log.warn("Error updating old token '%s'", old_token.id) + + def _create_new_lab(self, json_obj): + """Create a new lab in the database. + + :param json_obj: The JSON object with the lab data. + :type json_obj: dict + :return A tuple with: status code, reason, result and headers. + """ + token_id = None + ret_val = None + reason = "New lab created" + result = None + headers = None + + lab_doc = mlab.LabDocument.from_json(json_obj) + lab_doc.created_on = datetime.datetime.now(tz=bson.tz_util.utc) + + if lab_doc.token: + token_json = utils.db.find_one( + self.db[models.TOKEN_COLLECTION], lab_doc.token, field="token" + ) + if token_json: + token = mtoken.Token.from_json(token_json) + token_id = token.id + ret_val = 200 + else: + ret_val = 500 + else: + token = mtoken.Token() + token.email = lab_doc.contact[models.EMAIL_KEY] + token.username = ( + lab_doc.contact[models.NAME_KEY] + + " " + + lab_doc.contact[models.SURNAME_KEY] + ) + token.is_post_token = True + ret_val, token_id = utils.db.save(self.db, token, manipulate=True) + + if ret_val == 201 or ret_val == 200: + lab_doc.token = token_id + ret_val, lab_id = utils.db.save(self.db, lab_doc, manipulate=True) + if ret_val == 201: + result = { + models.ID_KEY: lab_id, + models.NAME_KEY: lab_doc.name, + models.TOKEN_KEY: token.token + } + location = urlunparse( + ( + 'http', + self.request.headers.get('Host'), + self.request.uri + '/' + lab_doc.name, + '', '', '' + ) + ) + headers = {'Location': location} + else: + reason = "Error saving new lab '%s'" % lab_doc.name + else: + reason = ( + "Error saving or finding the token for lab '%s'" % lab_doc.name + ) + + return (ret_val, reason, result, headers) diff --git a/app/handlers/response.py b/app/handlers/response.py index ae21426..653f815 100644 --- a/app/handlers/response.py +++ b/app/handlers/response.py @@ -149,7 +149,7 @@ class HandlerResponse(object): # The pymongo cursor is an iterable. if not isinstance(value, (ListType, Cursor)): value = [value] - if isinstance(value, Cursor): + elif isinstance(value, Cursor): value = [r for r in value] self._result = value diff --git a/app/handlers/tests/test_lab_handler.py b/app/handlers/tests/test_lab_handler.py new file mode 100644 index 0000000..edaa05e --- /dev/null +++ b/app/handlers/tests/test_lab_handler.py @@ -0,0 +1,396 @@ +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +"""Test module for the LabHandler handler.""" + +import concurrent.futures +import json +import mock +import mongomock +import tornado +import tornado.testing + +import handlers.app +import handlers.lab as hlab +import urls + +# Default Content-Type header returned by Tornado. +DEFAULT_CONTENT_TYPE = 'application/json; charset=UTF-8' + + +class TestLabHandler( + tornado.testing.AsyncHTTPTestCase, tornado.testing.LogTrapTestCase): + + def setUp(self): + self.mongodb_client = mongomock.Connection() + + super(TestLabHandler, self).setUp() + + patched_find_token = mock.patch("handlers.base.BaseHandler._find_token") + self.find_token = patched_find_token.start() + self.find_token.return_value = "token" + + patched_validate_token = mock.patch("handlers.common.validate_token") + self.validate_token = patched_validate_token.start() + self.validate_token.return_value = True + + self.addCleanup(patched_find_token.stop) + self.addCleanup(patched_validate_token.stop) + + def get_app(self): + dboptions = { + 'dbpassword': "", + 'dbuser': "" + } + + settings = { + 'dboptions': dboptions, + 'client': self.mongodb_client, + 'executor': concurrent.futures.ThreadPoolExecutor(max_workers=2), + 'default_handler_class': handlers.app.AppHandler, + 'debug': False + } + + return tornado.web.Application([urls._LAB_URL], **settings) + + def get_new_ioloop(self): + return tornado.ioloop.IOLoop.instance() + + def test_post_no_json(self): + body = json.dumps(dict(name='name', contact={})) + + response = self.fetch('/lab', method='POST', body=body) + + self.assertEqual(response.code, 403) + self.assertEqual( + response.headers['Content-Type'], DEFAULT_CONTENT_TYPE) + + def test_post_not_json_content(self): + headers = {'Authorization': 'foo', 'Content-Type': 'application/json'} + + response = self.fetch( + '/lab', method='POST', body='', headers=headers + ) + + self.assertEqual(response.code, 422) + self.assertEqual( + response.headers['Content-Type'], DEFAULT_CONTENT_TYPE) + + def test_post_wrong_content_type(self): + headers = {'Authorization': 'foo'} + + response = self.fetch( + '/lab', method='POST', body='', headers=headers + ) + + self.assertEqual(response.code, 415) + self.assertEqual( + response.headers['Content-Type'], DEFAULT_CONTENT_TYPE) + + def test_post_wrong_json(self): + headers = {'Authorization': 'foo', 'Content-Type': 'application/json'} + + body = json.dumps(dict(foo='foo', bar='bar')) + + response = self.fetch( + '/lab', method='POST', body=body, headers=headers + ) + + self.assertEqual(response.code, 400) + self.assertEqual( + response.headers['Content-Type'], DEFAULT_CONTENT_TYPE) + + def test_post_wrong_json_no_fields(self): + headers = {'Authorization': 'foo', 'Content-Type': 'application/json'} + + body = json.dumps(dict(name='foo', contact={})) + + response = self.fetch( + '/lab', method='POST', body=body, headers=headers + ) + + self.assertEqual(response.code, 400) + self.assertEqual( + response.headers['Content-Type'], DEFAULT_CONTENT_TYPE) + + def test_post_wrong_json_no_all_fields(self): + headers = {'Authorization': 'foo', 'Content-Type': 'application/json'} + + body = json.dumps( + dict(name='foo', contact={"name": "bar", "surname": "foo"}) + ) + + response = self.fetch( + '/lab', method='POST', body=body, headers=headers + ) + + self.assertEqual(response.code, 400) + self.assertEqual( + response.headers['Content-Type'], DEFAULT_CONTENT_TYPE) + + @mock.patch("utils.db.find_one") + def test_post_correct(self, find_one): + find_one.side_effect = [None] + + headers = {'Authorization': 'foo', 'Content-Type': 'application/json'} + + body = json.dumps( + dict( + name='foo', + contact={"name": "bar", "surname": "foo", "email": "foo"}, + ) + ) + + response = self.fetch( + '/lab', method='POST', body=body, headers=headers + ) + + self.assertEqual(response.code, 201) + self.assertEqual( + response.headers['Content-Type'], DEFAULT_CONTENT_TYPE) + self.assertIsNotNone(response.headers["Location"]) + + @mock.patch("utils.db.find_one") + def test_post_correct_lab_id_found(self, find_one): + find_one.side_effect = [True] + + headers = {'Authorization': 'foo', 'Content-Type': 'application/json'} + + body = json.dumps( + dict( + name='foo', + contact={"name": "bar", "surname": "foo", "email": "foo"}, + ) + ) + + response = self.fetch( + '/lab', method='POST', body=body, headers=headers + ) + + self.assertEqual(response.code, 400) + self.assertEqual( + response.headers['Content-Type'], DEFAULT_CONTENT_TYPE) + + @mock.patch("utils.db.find_one") + def test_post_correct_with_id_lab_id_not_found(self, find_one): + find_one.side_effect = [None] + + headers = {'Authorization': 'foo', 'Content-Type': 'application/json'} + + body = json.dumps( + dict( + name='foo', + contact={"name": "bar", "surname": "foo", "email": "foo"}, + ) + ) + + response = self.fetch( + '/lab/lab-01', method='POST', body=body, headers=headers + ) + + self.assertEqual(response.code, 404) + self.assertEqual( + response.headers['Content-Type'], DEFAULT_CONTENT_TYPE) + + @mock.patch("utils.db.find_one") + def test_post_correct_with_token_not_found(self, find_one): + find_one.side_effect = [None, None] + + headers = {'Authorization': 'foo', 'Content-Type': 'application/json'} + + body = json.dumps( + dict( + name='foo', + contact={"name": "bar", "surname": "foo", "email": "foo"}, + token="token" + ) + ) + + response = self.fetch( + '/lab', method='POST', body=body, headers=headers + ) + + self.assertEqual(response.code, 500) + self.assertEqual( + response.headers['Content-Type'], DEFAULT_CONTENT_TYPE) + + @mock.patch("utils.db.find_one") + def test_post_correct_with_token_found(self, find_one): + token_json = { + "_id": "token_id", + "token": "token", + "email": "foo", + "username": "bar" + } + find_one.side_effect = [None, token_json, None] + + headers = {'Authorization': 'foo', 'Content-Type': 'application/json'} + + body = json.dumps( + dict( + name='foo', + contact={"name": "bar", "surname": "foo", "email": "foo"}, + token="token" + ) + ) + + response = self.fetch( + '/lab', method='POST', body=body, headers=headers + ) + + self.assertEqual(response.code, 201) + self.assertEqual( + response.headers['Content-Type'], DEFAULT_CONTENT_TYPE) + self.assertIsNotNone(response.headers["Location"]) + + @mock.patch("utils.db.find_one") + def test_post_correct_with_id_lab_id_found(self, find_one): + lab_json = { + "name": "foo", + "token": "token-id", + "contact": { + "name": "foo", + "surname": "bar", + "email": "foo" + } + } + find_one.side_effect = [lab_json] + + headers = {'Authorization': 'foo', 'Content-Type': 'application/json'} + + body = json.dumps( + dict( + name='foo', + contact={"name": "bar", "surname": "foo", "email": "foo"}, + address={"street_1": "foo", "city": "bar"}, + private=True + ) + ) + + response = self.fetch( + '/lab/foo', method='POST', body=body, headers=headers + ) + + self.assertEqual(response.code, 200) + self.assertEqual( + response.headers['Content-Type'], DEFAULT_CONTENT_TYPE) + + @mock.patch("utils.db.save") + @mock.patch("utils.db.find_one") + def test_post_correct_with_id_lab_id_found_err_on_save( + self, find_one, save): + lab_json = { + "name": "foo", + "token": "token-id", + "contact": { + "name": "foo", + "surname": "bar", + "email": "foo" + }, + "address": { + "street_1": "foo" + } + } + find_one.side_effect = [lab_json] + save.side_effect = [(500, None)] + + headers = {'Authorization': 'foo', 'Content-Type': 'application/json'} + + body = json.dumps( + dict( + name='foo', + contact={"name": "bar", "surname": "foo", "email": "foo"}, + address={"street_1": "foo"} + ) + ) + + response = self.fetch( + '/lab/foo', method='POST', body=body, headers=headers + ) + + self.assertEqual(response.code, 500) + self.assertEqual( + response.headers['Content-Type'], DEFAULT_CONTENT_TYPE) + + @mock.patch("utils.db.find_one") + def test_post_correct_with_id_lab_id_found_and_token(self, find_one): + old_lab_json = { + "name": "foo", + "token": "token-id", + "contact": { + "name": "foo", + "surname": "bar", + "email": "foo" + }, + "address": { + "street_1": "foo" + } + } + old_token_json = { + "_id": "old-token-id", + "token": "token-id", + "email": "" + } + + new_token_json = { + "_id": "new-token-id", + "token": "token-uuid", + "email": "foo", + "username": "bar" + } + find_one.side_effect = [old_lab_json, old_token_json, new_token_json] + + headers = {'Authorization': 'foo', 'Content-Type': 'application/json'} + + body = json.dumps( + dict( + name='foo', + contact={"name": "bar", "surname": "foo", "email": "foobar"}, + token="token-uuid" + ) + ) + + response = self.fetch( + '/lab/foo', method='POST', body=body, headers=headers + ) + + self.assertEqual(response.code, 200) + self.assertEqual( + response.headers['Content-Type'], DEFAULT_CONTENT_TYPE) + + @mock.patch("utils.db.find_one") + def test_get_by_id_not_found(self, find_one): + find_one.side_effect = [None] + + headers = {'Authorization': 'foo'} + response = self.fetch('/lab/lab-01', headers=headers) + + self.assertEqual(response.code, 404) + self.assertEqual( + response.headers['Content-Type'], DEFAULT_CONTENT_TYPE) + + @mock.patch('utils.db.find_one') + def test_get_by_id_found(self, find_one): + find_one.side_effect = [{"_id": "foo", "name": "lab-01"}] + + expected_body = ( + '{"code": 200, "result": [{"_id": "foo", "name": "lab-01"}]}' + ) + + headers = {'Authorization': 'foo'} + response = self.fetch('/lab/lab-01', headers=headers) + + self.assertEqual(response.code, 200) + self.assertEqual( + response.headers['Content-Type'], DEFAULT_CONTENT_TYPE) + self.assertEqual(response.body, expected_body) diff --git a/app/models/__init__.py b/app/models/__init__.py index d183c42..3acd254 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -78,6 +78,7 @@ SKIP_KEY = 'skip' SORT_KEY = 'sort' SORT_ORDER_KEY = 'sort_order' STATUS_KEY = 'status' +SURNAME_KEY = 'surname' TIME_KEY = 'time' TOKEN_KEY = 'token' UPDATED_KEY = 'updated_on' diff --git a/app/models/lab.py b/app/models/lab.py index c4867e6..a94285c 100644 --- a/app/models/lab.py +++ b/app/models/lab.py @@ -50,12 +50,12 @@ class LabDocument(modb.BaseDocument): json_get = json_obj.get lab_doc = LabDocument(json_get(models.NAME_KEY)) lab_doc.id = json_get(models.ID_KEY, None) - lab_doc.created_on = json_get(models.CREATED_KEY) - lab_doc.private = json_get(models.PRIVATE_KEY) - lab_doc.address = json_get(models.ADDRESS_KEY) - lab_doc.contact = json_get(models.CONTACT_KEY) - lab_doc.token = json_get(models.TOKEN_KEY) - lab_doc.updated_on = json_get(models.UPDATED_KEY) + lab_doc.created_on = json_get(models.CREATED_KEY, None) + lab_doc.private = json_get(models.PRIVATE_KEY, False) + lab_doc.address = json_get(models.ADDRESS_KEY, {}) + lab_doc.contact = json_get(models.CONTACT_KEY, {}) + lab_doc.token = json_get(models.TOKEN_KEY, None) + lab_doc.updated_on = json_get(models.UPDATED_KEY, None) return lab_doc @property @@ -156,7 +156,7 @@ class LabDocument(modb.BaseDocument): [value.get("name"), value.get("surname"), value.get("email")] ): raise ValueError( - "Missing mandatory field (one of): name, surname or email" + "Missing mandatory field (one of): name, surname and email" ) self._contact = value diff --git a/app/urls.py b/app/urls.py index 4f2462b..1d598b4 100644 --- a/app/urls.py +++ b/app/urls.py @@ -17,40 +17,50 @@ from tornado.web import url -from handlers.bisect import BisectHandler -from handlers.batch import BatchHandler -from handlers.boot import BootHandler -from handlers.count import CountHandler -from handlers.defconf import DefConfHandler -from handlers.job import JobHandler -from handlers.subscription import SubscriptionHandler -from handlers.token import TokenHandler +import handlers.batch +import handlers.bisect +import handlers.boot +import handlers.count +import handlers.defconf +import handlers.job +import handlers.lab +import handlers.subscription +import handlers.token -_JOB_URL = url(r'/job(?P<sl>/)?(?P<id>.*)', JobHandler, name='job') +_JOB_URL = url( + r'/job(?P<sl>/)?(?P<id>.*)', handlers.job.JobHandler, name='job' +) _DEFCONF_URL = url( - r'/defconfig(?P<sl>/)?(?P<id>.*)', DefConfHandler, name='defconf' + r'/defconfig(?P<sl>/)?(?P<id>.*)', + handlers.defconf.DefConfHandler, + name='defconf' ) _SUBSCRIPTION_URL = url( r'/subscription(?P<sl>/)?(?P<id>.*)', - SubscriptionHandler, + handlers.subscription.SubscriptionHandler, name='subscription', ) -_BOOT_URL = url(r'/boot(?P<sl>/)?(?P<id>.*)', BootHandler, name='boot') +_BOOT_URL = url( + r'/boot(?P<sl>/)?(?P<id>.*)', handlers.boot.BootHandler, name='boot' +) _COUNT_URL = url( - r'/count(?P<sl>/)?(?P<id>.*)', CountHandler, name='count' + r'/count(?P<sl>/)?(?P<id>.*)', handlers.count.CountHandler, name='count' ) _TOKEN_URL = url( - r'/token(?P<sl>/)?(?P<id>.*)', TokenHandler, name='token' + r'/token(?P<sl>/)?(?P<id>.*)', handlers.token.TokenHandler, name='token' ) _BATCH_URL = url( - r'/batch', BatchHandler, name='batch' + r'/batch', handlers.batch.BatchHandler, name='batch' ) _BISECT_URL = url( r"/bisect/(?P<collection>.*)/(?P<id>.*)", - BisectHandler, + handlers.bisect.BisectHandler, name="bisect" ) +_LAB_URL = url( + r"/lab(?P<sl>/)?(?P<id>.*)", handlers.lab.LabHandler, name="lab" +) APP_URLS = [ _BATCH_URL, @@ -59,6 +69,7 @@ APP_URLS = [ _COUNT_URL, _DEFCONF_URL, _JOB_URL, + _LAB_URL, _SUBSCRIPTION_URL, _TOKEN_URL, ] diff --git a/app/utils/db.py b/app/utils/db.py index a66fb45..027c4cd 100644 --- a/app/utils/db.py +++ b/app/utils/db.py @@ -196,9 +196,15 @@ def save(database, document, manipulate=False): doc_id = database[document.collection].save( to_save, manipulate=manipulate ) - LOG.info("Document '%s' saved with ID '%s'", document.name, doc_id) + LOG.info( + "Document '%s' saved with ID '%s' (%s)", + document.name, doc_id, document.collection + ) except OperationFailure, ex: - LOG.error("Error saving the following document: %s", document.name) + LOG.error( + "Error saving the following document: %s (%s)", + document.name, document.collection + ) LOG.exception(ex) ret_value = 500 diff --git a/app/utils/tests/test_validator.py b/app/utils/tests/test_validator.py index fee29af..f670feb 100644 --- a/app/utils/tests/test_validator.py +++ b/app/utils/tests/test_validator.py @@ -250,3 +250,86 @@ class TestBatchValidator(unittest.TestCase): self.assertFalse( utilsv.is_valid_batch_json(json_obj, batch_key, accepted_keys) ) + + def test_validate_contact_object_wrong(self): + json_obj = { + "contact": {} + } + self.assertFalse(utilsv.is_valid_lab_contact_data(json_obj)[0]) + + json_obj = { + "contact": ["a"] + } + self.assertFalse(utilsv.is_valid_lab_contact_data(json_obj)[0]) + + json_obj = { + "contact": "a" + } + self.assertFalse(utilsv.is_valid_lab_contact_data(json_obj)[0]) + + json_obj = { + "contact": { + "foo": "bar", + "baz": "foo" + } + } + self.assertFalse(utilsv.is_valid_lab_contact_data(json_obj)[0]) + + json_obj = { + "contact": { + "name": "bar", + "surname": "foo" + } + } + self.assertFalse(utilsv.is_valid_lab_contact_data(json_obj)[0]) + + json_obj = { + "contact": { + "surname": "foo" + } + } + self.assertFalse(utilsv.is_valid_lab_contact_data(json_obj)[0]) + + json_obj = { + "contact": { + "name": "foo" + } + } + self.assertFalse(utilsv.is_valid_lab_contact_data(json_obj)[0]) + + json_obj = { + "contact": { + "email": "foo" + } + } + self.assertFalse(utilsv.is_valid_lab_contact_data(json_obj)[0]) + + json_obj = { + "contact": { + "name": "foo", + "email": "foo" + } + } + self.assertFalse(utilsv.is_valid_lab_contact_data(json_obj)[0]) + + json_obj = { + "contact": { + "surname": "foo", + "email": "foo" + } + } + self.assertFalse(utilsv.is_valid_lab_contact_data(json_obj)[0]) + + def test_validate_contact_object_correct(self): + + json_obj = { + "contact": { + "name": "foo", + "surname": "foo", + "email": "foo", + } + } + + validated = utilsv.is_valid_lab_contact_data(json_obj) + self.assertTrue(validated[0]) + self.assertIsNone(validated[1]) diff --git a/app/utils/validator.py b/app/utils/validator.py index 1a480b9..f131da8 100644 --- a/app/utils/validator.py +++ b/app/utils/validator.py @@ -108,14 +108,14 @@ def _complex_json_validation(json_obj, accepted_keys): mandatory_keys = set(accepted_keys.get(models.MANDATORY_KEYS)) valid_keys = set(accepted_keys.get(models.ACCEPTED_KEYS)) - missing_keys = mandatory_keys - json_keys + missing_keys = list(mandatory_keys - json_keys) if missing_keys: is_valid = False error_message = ( "One or more mandatory keys are missing: %s" % str(missing_keys) ) else: - strange_keys = json_keys - valid_keys + strange_keys = list(json_keys - valid_keys) if strange_keys: error_message = ( "Found non recognizable keys, they will not be considered: %s" % @@ -165,3 +165,37 @@ def is_valid_batch_json(json_obj, batch_key, accepted_keys): is_valid = False return is_valid + + +def is_valid_lab_contact_data(json_obj): + """Validate a `contact` data structure for the Lab model. + + :param json_obj: The JSON object containing the `contact` data. + :type json_obj: dict + :return A tuple: True or False, and an error message if False or None. + """ + is_valid = True + reason = None + + contact = json_obj.get(models.CONTACT_KEY) + if all([contact, isinstance(contact, types.DictionaryType)]): + mandatory_keys = set( + [models.NAME_KEY, models.SURNAME_KEY, models.EMAIL_KEY] + ) + provided_keys = set(contact.keys()) + # Does the provided dict contain all the mandatory keys? + if not (provided_keys >= mandatory_keys): + missing_keys = list(mandatory_keys - provided_keys) + is_valid = False + reason = ( + "Missing mandatory keys for '%s' JSON object: %s" % + (models.CONTACT_KEY, str(missing_keys)) + ) + else: + is_valid = False + reason = ( + "Provided '%s' data structure is not a JSON object or " + "is empty" % models.CONTACT_KEY + ) + + return is_valid, reason |