diff --git a/deckhand/control/validations.py b/deckhand/control/validations.py index 77000783..26689c36 100644 --- a/deckhand/control/validations.py +++ b/deckhand/control/validations.py @@ -80,7 +80,7 @@ class ValidationsResource(api_base.BaseResource): try: entry = db_api.validation_get_entry( revision_id, validation_name, entry_id) - except errors.RevisionNotFound as e: + except (errors.RevisionNotFound, errors.ValidationNotFound) as e: raise falcon.HTTPNotFound(description=e.format_message()) resp_body = self.view_builder.show_entry(entry) diff --git a/deckhand/db/sqlalchemy/api.py b/deckhand/db/sqlalchemy/api.py index adf7a1a7..16f94acc 100644 --- a/deckhand/db/sqlalchemy/api.py +++ b/deckhand/db/sqlalchemy/api.py @@ -1100,4 +1100,9 @@ def validation_get_entry(revision_id, val_name, entry_id, session=None): session = session or get_session() entries = validation_get_all_entries( revision_id, val_name, session=session) - return entries[entry_id] + try: + return entries[entry_id] + except IndexError: + raise errors.ValidationNotFound( + revision_id=revision_id, validation_name=val_name, + entry_id=entry_id) diff --git a/deckhand/engine/document_validation.py b/deckhand/engine/document_validation.py index 3083a1ec..8dddcea3 100644 --- a/deckhand/engine/document_validation.py +++ b/deckhand/engine/document_validation.py @@ -113,7 +113,8 @@ class DocumentValidation(object): # Can't use `startswith` below to avoid namespace false # positives like `CertificateKey` and `Certificate`. if schema_id == schema['id']: - matching_schemas.append(schema) + if schema not in matching_schemas: + matching_schemas.append(schema) return matching_schemas @classmethod @@ -201,6 +202,7 @@ class DocumentValidation(object): LOG.info('Skipping schema validation for abstract ' 'document: %s.', raw_dict) else: + for schema_to_use in schemas_to_use: try: if isinstance(schema_to_use['schema'], dict): @@ -217,7 +219,7 @@ class DocumentValidation(object): result['errors'].append({ 'schema': document.get_schema(), 'name': document.get_name(), - 'message': e.message.replace('\\', '') + 'message': e.message.replace('u\'', '\'') }) if result['errors']: diff --git a/deckhand/errors.py b/deckhand/errors.py index 3d49fc09..361e4cbb 100644 --- a/deckhand/errors.py +++ b/deckhand/errors.py @@ -196,12 +196,6 @@ class SingletonDocumentConflict(DeckhandException): code = 409 -class LayeringPolicyNotFound(DeckhandException): - msg_fmt = ("LayeringPolicy with schema %(schema)s not found in the " - "system.") - code = 400 - - class LayeringPolicyMalformed(DeckhandException): msg_fmt = ("LayeringPolicy with schema %(schema)s is improperly formatted:" " %(document)s.") @@ -239,6 +233,12 @@ class DocumentNotFound(DeckhandException): code = 404 +class LayeringPolicyNotFound(DeckhandException): + msg_fmt = ("LayeringPolicy with schema %(schema)s not found in the " + "system.") + code = 404 + + class RevisionNotFound(DeckhandException): msg_fmt = "The requested revision %(revision)s was not found." code = 404 @@ -250,6 +250,13 @@ class RevisionTagNotFound(DeckhandException): code = 404 +class ValidationNotFound(DeckhandException): + msg_fmt = ("The requested validation entry %(entry_id)s was not found " + "for validation name %(validation_name)s and revision ID " + "%(revision_id)s.") + code = 404 + + class RevisionTagBadFormat(DeckhandException): msg_fmt = ("The requested tag data %(data)s must either be null or " "dictionary.") diff --git a/deckhand/tests/unit/control/test_validations_controller.py b/deckhand/tests/unit/control/test_validations_controller.py index 949268eb..4c19611f 100644 --- a/deckhand/tests/unit/control/test_validations_controller.py +++ b/deckhand/tests/unit/control/test_validations_controller.py @@ -251,10 +251,10 @@ class TestValidationsController(test_base.BaseControllerTest): revision_id = self._create_revision() validation_name = test_utils.rand_name('validation') - resp = self._create_validation(revision_id, validation_name, - VALIDATION_RESULT) - resp = resp = self._create_validation(revision_id, validation_name, - VALIDATION_RESULT_ALT) + self._create_validation(revision_id, validation_name, + VALIDATION_RESULT) + self._create_validation(revision_id, validation_name, + VALIDATION_RESULT_ALT) resp = self.app.simulate_get( '/api/v1.0/revisions/%s/validations/%s' % (revision_id, @@ -279,8 +279,8 @@ class TestValidationsController(test_base.BaseControllerTest): revision_id = self._create_revision() validation_name = test_utils.rand_name('validation') - resp = resp = self._create_validation(revision_id, validation_name, - VALIDATION_RESULT) + resp = self._create_validation(revision_id, validation_name, + VALIDATION_RESULT) resp = self.app.simulate_get( '/api/v1.0/revisions/%s/validations/%s/0' % (revision_id, @@ -312,6 +312,28 @@ class TestValidationsController(test_base.BaseControllerTest): } self.assertEqual(expected_body, body) + def test_show_nonexistent_validation_entry_returns_404(self): + rules = {'deckhand:create_cleartext_documents': '@', + 'deckhand:create_validation': '@', + 'deckhand:show_validation': '@'} + self.policy.set_rules(rules) + + revision_id = self._create_revision() + validation_name = test_utils.rand_name('validation') + resp = self._create_validation(revision_id, validation_name, + VALIDATION_RESULT) + self.assertEqual(201, resp.status_code) + expected_error = ('The requested validation entry 5 was not found for ' + 'validation name %s and revision ID %d.' % ( + validation_name, revision_id)) + + resp = self.app.simulate_get( + '/api/v1.0/revisions/%s/validations/%s/5' % (revision_id, + validation_name), + headers={'Content-Type': 'application/x-yaml'}) + self.assertEqual(404, resp.status_code) + self.assertEqual(expected_error, yaml.safe_load(resp.text)['message']) + def test_validation_with_registered_data_schema(self): rules = {'deckhand:create_cleartext_documents': '@', 'deckhand:list_validations': '@'} @@ -441,7 +463,8 @@ class TestValidationsController(test_base.BaseControllerTest): def test_validation_with_registered_data_schema_expect_mixed(self): rules = {'deckhand:create_cleartext_documents': '@', - 'deckhand:list_validations': '@'} + 'deckhand:list_validations': '@', + 'deckhand:show_validation': '@'} self.policy.set_rules(rules) # Register a `DataSchema` against which the test document will be @@ -505,6 +528,34 @@ class TestValidationsController(test_base.BaseControllerTest): } self.assertEqual(expected_body, body) + resp = self.app.simulate_get( + '/api/v1.0/revisions/%s/validations/%s' % ( + revision_id, types.DECKHAND_SCHEMA_VALIDATION), + headers={'Content-Type': 'application/x-yaml'}) + self.assertEqual(200, resp.status_code) + body = yaml.safe_load(resp.text) + expected_body = { + 'count': 2, + 'results': [{'id': 0, 'status': 'failure'}, # fail_doc failed. + {'id': 1, 'status': 'success'}] # pass_doc succeeded. + } + self.assertEqual(expected_body, body) + + # Validate that fail_doc validation failed for the expected reason. + resp = self.app.simulate_get( + '/api/v1.0/revisions/%s/validations/%s/0' % ( + revision_id, types.DECKHAND_SCHEMA_VALIDATION), + headers={'Content-Type': 'application/x-yaml'}) + self.assertEqual(200, resp.status_code) + body = yaml.safe_load(resp.text) + expected_errors = [{ + 'schema': 'example/foo/v1', + 'name': 'test_doc', + 'message': "'fail' is not of type 'integer'" + }] + self.assertIn('errors', body) + self.assertEqual(expected_errors, body['errors']) + def test_document_without_data_section_saves_but_fails_validation(self): """Validate that a document without the data section is saved to the database, but fails validation. This is a valid use case because a