aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMilo Casagrande <milo.casagrande@linaro.org>2014-11-12 17:35:34 +0100
committerMilo Casagrande <milo.casagrande@linaro.org>2014-11-12 17:35:34 +0100
commitfa9c001a2b88a9c7ee43015528c4e938cb46c0db (patch)
treee1a10cd8173a06d124bd57e66d3412763e0010b2
parenta6d2a1d4df3c2544fa0425dc492b11fcca1c70ef (diff)
Add /lab URL handler.
Change-Id: I89a770993556b1c1795108e200a0e2bfb956c761
-rw-r--r--app/handlers/base.py9
-rw-r--r--app/handlers/common.py26
-rw-r--r--app/handlers/lab.py284
-rw-r--r--app/handlers/response.py2
-rw-r--r--app/handlers/tests/test_lab_handler.py396
-rw-r--r--app/models/__init__.py1
-rw-r--r--app/models/lab.py14
-rw-r--r--app/urls.py43
-rw-r--r--app/utils/db.py10
-rw-r--r--app/utils/tests/test_validator.py83
-rw-r--r--app/utils/validator.py38
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