def test_list_revision_documents_sorting_by_schema_then_limit(self): rules = {'deckhand:list_cleartext_documents': '@', 'deckhand:list_encrypted_documents': '@', 'deckhand:create_cleartext_documents': '@'} self.policy.set_rules(rules) documents_factory = factories.DocumentFactory(2, [1, 1]) documents = documents_factory.gen_test({ '_SITE_ACTIONS_1_': { 'actions': [{'method': 'merge', 'path': '.'}] } }) schemas = ['deckhand/Certificate/v1', 'deckhand/CertificateKey/v1', 'deckhand/LayeringPolicy/v1'] for idx in range(len(documents)): documents[idx]['schema'] = schemas[idx] for limit in (0, 1, 2, 3): expected_schemas = schemas[:limit] resp = self.app.simulate_put( '/api/v1.0/buckets/mop/documents', headers={'Content-Type': 'application/x-yaml'}, body=yaml.safe_dump_all(documents)) self.assertEqual(200, resp.status_code) revision_id = list(yaml.safe_load_all(resp.text))[0]['status'][ 'revision'] resp = self.app.simulate_get( '/api/v1.0/revisions/%s/documents' % revision_id, params={'sort': 'schema', 'limit': limit}, params_csv=False, headers={'Content-Type': 'application/x-yaml'}) self.assertEqual(200, resp.status_code) retrieved_documents = list(yaml.safe_load_all(resp.text)) self.assertEqual(limit, len(retrieved_documents)) self.assertEqual(expected_schemas, [d['schema'] for d in retrieved_documents])
def test_layering_documents_with_different_schemas(self): """Validate that attempting to layer documents with different schemas results in errors. """ doc_factory = factories.DocumentFactory(3, [1, 1, 1]) documents = doc_factory.gen_test({}) # Region and site documents should result in no parent being found # since their schemas will not match that of their parent's. for idx in range(2, 4): # Only region/site have parent. prev_schema = documents[idx]['schema'] documents[idx]['schema'] = test_utils.rand_name('schema') # Escape '[' and ']' for regex to work. expected_err = ("Missing parent document for document %s." % documents[idx]).replace('[', '\[').replace(']', '\]') self.assertRaisesRegex(errors.MissingDocumentParent, expected_err, layering.DocumentLayering, documents) # Restore schema for next test run. documents[idx]['schema'] = prev_schema
def test_lookup_by_revision_id_cache(self): """Validate ``lookup_by_revision_id`` caching works. Passing in None in lieu of the actual documents proves that: * if the payload is in the cache, then no error is thrown since the cache is hit so no further processing is performed, where otherwise a method would be called on `None` * if the payload is not in the cache, then following logic above, method is called on `None`, raising AttributeError """ document_factory = factories.DocumentFactory(1, [1]) documents = document_factory.gen_test({}) # Validate that caching the ref returns expected payload. rendered_documents, cache_hit = cache.lookup_by_revision_id( 1, documents) self.assertIsInstance(rendered_documents, list) self.assertFalse(cache_hit) # Validate that the cache actually works. next_rendered_documents, cache_hit = cache.lookup_by_revision_id( 1, None) self.assertEqual(rendered_documents, next_rendered_documents) self.assertTrue(cache_hit) # No documents passed in and revision ID 2 isn't cached - so expect # this to blow up. with testtools.ExpectedException(AttributeError): cache.lookup_by_revision_id(2, None) # Invalidate the cache and ensure the original data isn't there. cache.invalidate() # The cache won't be hit this time - expect AttributeError. with testtools.ExpectedException(AttributeError): cache.lookup_by_revision_id(1, None)
def test_list_cleartext_rendered_documents_insufficient_permissions(self): rules = { 'deckhand:list_cleartext_documents': 'rule:admin_api', 'deckhand:create_cleartext_documents': '@' } self.policy.set_rules(rules) # Create a document for a bucket. documents_factory = factories.DocumentFactory(1, [1]) payload = [documents_factory.gen_test({})[0]] resp = self.app.simulate_put( '/api/v1.0/buckets/mop/documents', headers={'Content-Type': 'application/x-yaml'}, body=yaml.safe_dump_all(payload)) self.assertEqual(200, resp.status_code) revision_id = list(yaml.safe_load_all( resp.text))[0]['status']['revision'] # Verify that the created document was not returned. resp = self.app.simulate_get( '/api/v1.0/revisions/%s/rendered-documents' % revision_id, headers={'Content-Type': 'application/x-yaml'}) self.assertEqual(403, resp.status_code)
def test_layering_invalid_substitution_format_raises_exc(self): doc_factory = factories.DocumentFactory(1, [1]) layering_policy, document_template = doc_factory.gen_test( { "_GLOBAL_SUBSTITUTIONS_1_": [{ "dest": { "path": ".c" }, "src": { "schema": "deckhand/Certificate/v1", "name": "global-cert", "path": "." } }], }, global_abstract=False) for key in ('src', 'dest'): document = copy.deepcopy(document_template) del document['metadata']['substitutions'][0][key] self.assertRaises(errors.InvalidDocumentFormat, self._test_layering, [layering_policy, document], validate=True) for key in ('schema', 'name', 'path'): document = copy.deepcopy(document_template) del document['metadata']['substitutions'][0]['src'][key] self.assertRaises(errors.InvalidDocumentFormat, self._test_layering, [layering_policy, document], validate=True) for key in ('path', ): document = copy.deepcopy(document_template) del document['metadata']['substitutions'][0]['dest'][key] self.assertRaises(errors.InvalidDocumentFormat, self._test_layering, [layering_policy, document], validate=True)
def test_layering_two_concrete_regions_one_child_each(self): """Scenario: Initially: r1: {"c": 3, "d": 4}, r2: {"e": 5, "f": 6} Merge "." (g -> r1): {"a": 1, "b": 2, "c": 3, "d": 4} Merge "." (r1 -> s1): {"a": 1, "b": 2, "c": 3, "d": 4, "g": 7, "h": 8} Merge "." (g -> r2): {"a": 1, "b": 2, "e": 5, "f": 6} Merge "." (r2 -> s2): {"a": 1, "b": 2, "e": 5, "f": 6, "i": 9, "j": 10} """ mapping = { "_GLOBAL_DATA_1_": {"data": {"a": 1, "b": 2}}, "_REGION_DATA_1_": {"data": {"c": 3, "d": 4}}, "_REGION_ACTIONS_1_": { "actions": [{"method": "merge", "path": "."}]}, "_REGION_DATA_2_": {"data": {"e": 5, "f": 6}}, "_REGION_ACTIONS_2_": { "actions": [{"method": "merge", "path": "."}]}, "_SITE_DATA_1_": {"data": {"g": 7, "h": 8}}, "_SITE_ACTIONS_1_": { "actions": [{"method": "merge", "path": "."}]}, "_SITE_DATA_2_": {"data": {"i": 9, "j": 10}}, "_SITE_ACTIONS_2_": { "actions": [{"method": "merge", "path": "."}]} } doc_factory = factories.DocumentFactory(3, [1, 2, 2]) documents = doc_factory.gen_test( mapping, region_abstract=False, site_abstract=False, site_parent_selectors=[ {'region': 'region1'}, {'region': 'region2'}]) site_expected = [{"a": 1, "b": 2, "c": 3, "d": 4, "g": 7, "h": 8}, {"a": 1, "b": 2, "e": 5, "f": 6, "i": 9, "j": 10}] region_expected = [{"a": 1, "b": 2, "c": 3, "d": 4}, {"a": 1, "b": 2, "e": 5, "f": 6}] global_expected = {"a": 1, "b": 2} self._test_layering(documents, site_expected, region_expected, global_expected)
def test_layering_site_and_global_abstract(self): mapping = { "_GLOBAL_DATA_1_": { "data": { "a": { "x": 1, "y": 2 }, "c": 9 } }, "_SITE_DATA_1_": { "data": { "a": { "x": 7, "z": 3 }, "b": 4 } }, "_SITE_ACTIONS_1_": { "actions": [{ "method": "delete", "path": '.a' }] } } doc_factory = factories.DocumentFactory(2, [1, 1]) documents = doc_factory.gen_test(mapping, site_abstract=True, global_abstract=True) site_expected = {"a": {"x": 7, "z": 3}, "b": 4} global_expected = {'a': {'x': 1, 'y': 2}, 'c': 9} self._test_layering(documents, site_expected, global_expected=global_expected)
def test_layering_site_region_and_global_concrete(self): # Both the site and region data should be updated as they're both # concrete docs. mapping = { "_GLOBAL_DATA_1_": {"data": {"a": {"x": 1, "y": 2}}}, "_REGION_DATA_1_": {"data": {"a": {"z": 3}, "b": 5}}, "_SITE_DATA_1_": {"data": {"b": 4}}, "_REGION_ACTIONS_1_": { "actions": [{"method": "replace", "path": ".a"}]}, "_SITE_ACTIONS_1_": { "actions": [{"method": "merge", "path": "."}]} } doc_factory = factories.DocumentFactory(3, [1, 1, 1]) documents = doc_factory.gen_test( mapping, site_abstract=False, region_abstract=False, global_abstract=False) site_expected = {'a': {'z': 3}, 'b': 4} region_expected = {'a': {'z': 3}} # Global data remains unchanged as there's no layer higher than it in # this example. global_expected = {'a': {'x': 1, 'y': 2}} self._test_layering(documents, site_expected, region_expected, global_expected)
def test_layering_multiple_delete(self): """Scenario: Initially: {'x': 1, 'y': 2}, 'b': {'v': 3, 'w': 4}} Delete ".": {} Delete ".": {} Merge ".": {'b': 4} """ mapping = { "_GLOBAL_DATA_1_": { "data": {'a': {'x': 1, 'y': 2}, 'b': {'v': 3, 'w': 4}}}, "_REGION_DATA_1_": {"data": {"a": {"z": 3}}}, "_SITE_DATA_1_": {"data": {"b": 4}}, "_REGION_ACTIONS_1_": { "actions": [{'path': '.', 'method': 'delete'}, {'path': '.', 'method': 'delete'}]}, "_SITE_ACTIONS_1_": { "actions": [{"method": "merge", "path": "."}]} } doc_factory = factories.DocumentFactory(3, [1, 1, 1]) documents = doc_factory.gen_test(mapping, site_abstract=False) site_expected = {'b': 4} self._test_layering(documents, site_expected)
def test_list_encrypted_rendered_documents_insufficient_permissions(self): rules = { 'deckhand:list_cleartext_documents': '@', 'deckhand:list_encrypted_documents': 'rule:admin_api', 'deckhand:create_cleartext_documents': '@', 'deckhand:create_encrypted_documents': '@' } self.policy.set_rules(rules) # Create a document for a bucket. documents_factory = factories.DocumentFactory(1, [1]) layering_policy = documents_factory.gen_test({})[0] secrets_factory = factories.DocumentSecretFactory() encrypted_document = secrets_factory.gen_test('Certificate', 'encrypted') payload = [layering_policy, encrypted_document] with mock.patch.object(secrets_manager, 'SecretsManager', autospec=True) as mock_secrets_mgr: mock_secrets_mgr.create.return_value = payload[0]['data'] resp = self.app.simulate_put( '/api/v1.0/buckets/mop/documents', headers={'Content-Type': 'application/x-yaml'}, body=yaml.safe_dump_all(payload)) self.assertEqual(200, resp.status_code) revision_id = list(yaml.safe_load_all( resp.text))[0]['status']['revision'] # Verify that the created document was not returned. resp = self.app.simulate_get( '/api/v1.0/revisions/%s/rendered-documents' % revision_id, headers={'Content-Type': 'application/x-yaml'}, params={'schema': encrypted_document['schema']}) self.assertEqual(200, resp.status_code) self.assertEmpty(list(yaml.safe_load_all(resp.text)))
def test_layering_multiple_replace_2(self): """Scenario: Initially: {'a': {'x': 1, 'y': 2}, 'b': {'v': 3, 'w': 4}} Replace ".a": {'a': {'z': 5}, 'b': {'v': 3, 'w': 4}} Replace ".b": {'a': {'z': 5}, 'b': [109]} Merge ".": {'a': {'z': 5}, 'b': [32]} """ mapping = { "_GLOBAL_DATA_1_": { "data": {'a': {'x': 1, 'y': 2}, 'b': {'v': 3, 'w': 4}}}, "_REGION_DATA_1_": {"data": {'a': {'z': 5}, 'b': [109]}}, "_SITE_DATA_1_": {"data": {"b": [32]}}, "_REGION_ACTIONS_1_": { "actions": [{'path': '.a', 'method': 'replace'}, {'path': '.b', 'method': 'replace'}]}, "_SITE_ACTIONS_1_": { "actions": [{"method": "merge", "path": "."}]} } doc_factory = factories.DocumentFactory(3, [1, 1, 1]) documents = doc_factory.gen_test(mapping, site_abstract=False) site_expected = {'a': {'z': 5}, 'b': [32]} self._test_layering(documents, site_expected)
def test_layering_with_substitution_cycle_fails(self): """Validate that a substitution dependency cycle raises a critical failure. In the case below, the cycle exists between site-1 -> site-2 -> site-3 -> site-1 """ mapping = { "_GLOBAL_DATA_1_": { "data": { "a": { "x": 1, "y": 2 } } }, "_SITE_NAME_1_": "site-1", "_SITE_DATA_1_": { "data": { "c": "placeholder" } }, "_SITE_ACTIONS_1_": { "actions": [{ "method": "merge", "path": "." }] }, "_SITE_SUBSTITUTIONS_1_": [{ "dest": { "path": ".c" }, "src": { "schema": "example/Kind/v1", "name": "site-3", "path": "." } }], "_SITE_NAME_2_": "site-2", "_SITE_DATA_2_": { "data": { "d": "placeholder" } }, "_SITE_ACTIONS_2_": { "actions": [{ "method": "merge", "path": "." }] }, "_SITE_SUBSTITUTIONS_2_": [{ "dest": { "path": ".d" }, "src": { "schema": "example/Kind/v1", "name": "site-1", "path": ".c" } }], "_SITE_NAME_3_": "site-3", "_SITE_DATA_3_": { "data": { "e": "placeholder" } }, "_SITE_ACTIONS_3_": { "actions": [{ "method": "merge", "path": "." }] }, "_SITE_SUBSTITUTIONS_3_": [{ "dest": { "path": ".e" }, "src": { "schema": "example/Kind/v1", "name": "site-2", "path": ".d" } }] } doc_factory = factories.DocumentFactory(2, [1, 3]) documents = doc_factory.gen_test(mapping, site_abstract=False) # Pass in the documents in reverse order to ensure that the dependency # chain by default is not linear and thus requires sorting. self.assertRaises(errors.SubstitutionDependencyCycle, layering.DocumentLayering, documents, substitution_sources=documents)
def test_layering_with_substitution_dependency_chain(self): """Validate that parent with multiple children that substitute from each other works no matter the order of the documents. """ mapping = { "_GLOBAL_DATA_1_": { "data": { "a": { "x": 1, "y": 2 } } }, "_GLOBAL_SUBSTITUTIONS_1_": [{ "dest": { "path": ".b" }, "src": { "schema": "deckhand/Certificate/v1", "name": "global-cert", "path": "." } }], "_SITE_NAME_1_": "site-1", "_SITE_DATA_1_": { "data": { "c": "placeholder" } }, "_SITE_ACTIONS_1_": { "actions": [{ "method": "merge", "path": "." }] }, "_SITE_SUBSTITUTIONS_1_": [{ "dest": { "path": ".c" }, "src": { "schema": "deckhand/CertificateKey/v1", "name": "site-cert", "path": "." } }], "_SITE_NAME_2_": "site-2", "_SITE_DATA_2_": { "data": { "d": "placeholder" } }, "_SITE_ACTIONS_2_": { "actions": [{ "method": "merge", "path": "." }] }, "_SITE_SUBSTITUTIONS_2_": [{ "dest": { "path": ".d" }, "src": { "schema": "example/Kind/v1", "name": "site-1", "path": ".c" } }], "_SITE_NAME_3_": "site-3", "_SITE_DATA_3_": { "data": { "e": "placeholder" } }, "_SITE_ACTIONS_3_": { "actions": [{ "method": "merge", "path": "." }] }, "_SITE_SUBSTITUTIONS_3_": [{ "dest": { "path": ".e" }, "src": { "schema": "example/Kind/v1", "name": "site-2", "path": ".d" } }] } doc_factory = factories.DocumentFactory(2, [1, 3]) documents = doc_factory.gen_test(mapping, site_abstract=False, global_abstract=False) secrets_factory = factories.DocumentSecretFactory() global_expected = {'a': {'x': 1, 'y': 2}, 'b': 'global-secret'} site_expected = [{ 'a': { 'x': 1, 'y': 2 }, 'b': 'global-secret', 'c': 'site-secret' }, { 'a': { 'x': 1, 'y': 2 }, 'b': 'global-secret', 'd': 'site-secret' }, { 'a': { 'x': 1, 'y': 2 }, 'b': 'global-secret', 'e': 'site-secret' }] certificate = secrets_factory.gen_test('Certificate', 'cleartext', data='global-secret', name='global-cert') certificate_key = secrets_factory.gen_test('CertificateKey', 'cleartext', data='site-secret', name='site-cert') documents.extend([certificate] + [certificate_key]) # Pass in the documents in reverse order to ensure that the dependency # chain by default is not linear and thus requires sorting. self._test_layering(documents, site_expected=site_expected, global_expected=global_expected, strict=False) # Try different permutations of document orders for good measure. for documents in list(itertools.permutations(documents))[:10]: self._test_layering(documents, site_expected=site_expected, global_expected=global_expected, strict=False)
def test_parent_and_child_undergo_layering_and_substitution_empty_layers( self, mock_log): """Validate that parent and child documents both undergo substitution and layering. empty layer -> discard | v empty layer -> discard | v global -> requires substitution | v empty layer -> discard | V site -> requires substitution (layered with global) Where the site's parent is actually the global document. """ mapping = { "_GLOBAL_DATA_1_": { "data": { "a": { "x": 1, "y": 2 } } }, "_GLOBAL_SUBSTITUTIONS_1_": [{ "dest": { "path": ".b" }, "src": { "schema": "deckhand/Certificate/v1", "name": "global-cert", "path": "." } }], "_SITE_DATA_1_": { "data": { "c": "need-site-secret" } }, "_SITE_ACTIONS_1_": { "actions": [{ "method": "merge", "path": "." }] }, "_SITE_SUBSTITUTIONS_1_": [{ "dest": { "path": ".c" }, "src": { "schema": "deckhand/CertificateKey/v1", "name": "site-cert", "path": "." } }], } doc_factory = factories.DocumentFactory(2, [1, 1]) documents = doc_factory.gen_test(mapping, site_abstract=False, global_abstract=False) documents[0]['data']['layerOrder'] = [ 'empty_1', 'empty_2', 'global', 'empty_3', 'site' ] secrets_factory = factories.DocumentSecretFactory() global_expected = {'a': {'x': 1, 'y': 2}, 'b': 'global-secret'} site_expected = { 'a': { 'x': 1, 'y': 2 }, 'b': 'global-secret', 'c': 'site-secret' } certificate = secrets_factory.gen_test('Certificate', 'cleartext', data='global-secret', name='global-cert') certificate_key = secrets_factory.gen_test('CertificateKey', 'cleartext', data='site-secret', name='site-cert') documents.extend([certificate] + [certificate_key]) self._test_layering(documents, site_expected=site_expected, global_expected=global_expected, strict=False) expected_message = ( '%s is an empty layer with no documents. It will be discarded ' 'from the layerOrder during the layering process.') expected_log_calls = [ mock.call(expected_message, layer) for layer in ('empty_1', 'empty_2', 'empty_3') ] mock_log.info.assert_has_calls(expected_log_calls)
def test_parent_with_multi_child_layering_and_multi_substitutions(self): """Validate that parent and children documents both undergo layering and multiple substitutions. global -> requires substitution | v site1 -> requires multiple substitutions site2 -> requires multiple substitutions """ mapping = { "_GLOBAL_DATA_1_": { "data": { "a": { "x": 1, "y": 2 } } }, "_GLOBAL_SUBSTITUTIONS_1_": [{ "dest": { "path": ".b" }, "src": { "schema": "deckhand/Certificate/v1", "name": "global-cert", "path": "." } }], "_SITE_DATA_1_": { "data": {} }, "_SITE_ACTIONS_1_": { "actions": [{ "method": "merge", "path": "." }] }, "_SITE_SUBSTITUTIONS_1_": [{ "dest": { "path": ".c" }, "src": { "schema": "deckhand/CertificateKey/v1", "name": "site-1-cert-key", "path": "." }, }, { "dest": { "path": ".d" }, "src": { "schema": "deckhand/Certificate/v1", "name": "site-1-cert", "path": "." } }], "_SITE_DATA_2_": { "data": {} }, "_SITE_ACTIONS_2_": { "actions": [{ "method": "merge", "path": "." }] }, "_SITE_SUBSTITUTIONS_2_": [{ "dest": { "path": ".e" }, "src": { "schema": "deckhand/CertificateKey/v1", "name": "site-2-cert-key", "path": "." }, }, { "dest": { "path": ".f" }, "src": { "schema": "deckhand/Certificate/v1", "name": "site-2-cert", "path": "." } }] } doc_factory = factories.DocumentFactory(2, [1, 2]) documents = doc_factory.gen_test(mapping, site_abstract=False, global_abstract=False) secrets_factory = factories.DocumentSecretFactory() global_expected = {'a': {'x': 1, 'y': 2}, 'b': 'global-secret'} site_expected = [{ 'a': { 'x': 1, 'y': 2 }, 'b': 'global-secret', 'c': 'site-1-sec-key', 'd': 'site-1-sec' }, { 'a': { 'x': 1, 'y': 2 }, 'b': 'global-secret', 'e': 'site-2-sec-key', 'f': 'site-2-sec' }] certificate = secrets_factory.gen_test('Certificate', 'cleartext', data='global-secret', name='global-cert') certificate_keys = [ secrets_factory.gen_test('CertificateKey', 'cleartext', data='site-%d-sec-key' % idx, name='site-%d-cert-key' % idx) for idx in range(1, 3) ] certificates = [ secrets_factory.gen_test('Certificate', 'cleartext', data='site-%d-sec' % idx, name='site-%d-cert' % idx) for idx in range(1, 3) ] documents.extend([certificate] + certificate_keys + certificates) self._test_layering(documents, site_expected=site_expected, global_expected=global_expected, strict=False)
def test_parent_and_child_layering_and_substitution_same_paths(self): """Validate that parent and child documents both undergo layering and substitution where the substitution occurs at the same path. global -> requires substitution | v site -> requires substitution """ mapping = { "_GLOBAL_DATA_1_": { "data": { "a": { "x": 1, "y": 2 } } }, "_GLOBAL_SUBSTITUTIONS_1_": [{ "dest": { "path": ".b" }, "src": { "schema": "deckhand/Certificate/v1", "name": "global-cert", "path": "." } }], "_SITE_DATA_1_": { "data": {} }, "_SITE_ACTIONS_1_": { "actions": [{ "method": "merge", "path": "." }] }, "_SITE_SUBSTITUTIONS_1_": [{ "dest": { "path": ".b" }, "src": { "schema": "deckhand/CertificateKey/v1", "name": "site-cert", "path": "." } }], } doc_factory = factories.DocumentFactory(2, [1, 1]) documents = doc_factory.gen_test(mapping, site_abstract=False, global_abstract=False) secrets_factory = factories.DocumentSecretFactory() global_expected = {'a': {'x': 1, 'y': 2}, 'b': 'global-secret'} site_expected = {'a': {'x': 1, 'y': 2}, 'b': 'site-secret'} certificate = secrets_factory.gen_test('Certificate', 'cleartext', data='global-secret', name='global-cert') certificate_key = secrets_factory.gen_test('CertificateKey', 'cleartext', data='site-secret', name='site-cert') documents.extend([certificate, certificate_key]) self._test_layering(documents, site_expected=site_expected, global_expected=global_expected, strict=False)
def test_validation_with_registered_data_schema_expect_multi_failure(self): rules = { 'deckhand:create_cleartext_documents': '@', 'deckhand:list_validations': '@', 'deckhand:show_validation': '@' } self.policy.set_rules(rules) # Create a `DataSchema` against which the test document will be # validated. data_schema_factory = factories.DataSchemaFactory() metadata_name = 'example/foo/v1' schema_to_use = { '$schema': 'http://json-schema.org/schema#', 'type': 'object', 'properties': { 'a': { 'type': 'integer' # Test doc will fail b/c of wrong type. } }, 'required': ['a'] } data_schema = data_schema_factory.gen_test(metadata_name, data=schema_to_use) # Failure #1. # Create the test document that fails the validation due to the # schema defined by the `DataSchema` document. doc_factory = factories.DocumentFactory(1, [1]) doc_to_test = doc_factory.gen_test( {'_GLOBAL_DATA_1_': { 'data': { 'a': 'fail' } }}, global_abstract=False)[-1] doc_to_test['schema'] = 'example/foo/v1' doc_to_test['metadata']['name'] = 'test_doc' # Failure #2. # Remove required metadata property, causing error to be generated. del doc_to_test['metadata']['layeringDefinition'] revision_id = self._create_revision(payload=[doc_to_test, data_schema]) # Validate that the validation was created and reports failure. resp = self.app.simulate_get( '/api/v1.0/revisions/%s/validations' % revision_id, headers={'Content-Type': 'application/x-yaml'}) self.assertEqual(200, resp.status_code) body = yaml.safe_load(resp.text) expected_body = { 'count': 1, 'results': [{ 'name': types.DECKHAND_SCHEMA_VALIDATION, 'status': 'failure' }] } self.assertEqual(expected_body, body) # Validate that both expected errors are present for validation. expected_errors = [{ 'error_section': { 'data': { 'a': 'fail' }, 'metadata': { 'labels': { 'global': 'global1' }, 'name': 'test_doc', 'schema': 'metadata/Document/v1.0' }, 'schema': 'example/foo/v1' }, 'name': 'test_doc', 'path': '.metadata', 'schema': 'example/foo/v1', 'message': "'layeringDefinition' is a required property", 'validation_schema': document_schema.schema, 'schema_path': '.properties.metadata.required' }, { 'error_section': { 'a': 'fail' }, 'name': 'test_doc', 'path': '.data.a', 'schema': 'example/foo/v1', 'message': "'fail' is not of type 'integer'", 'validation_schema': schema_to_use, 'schema_path': '.properties.a.type' }] resp = self.app.simulate_get( '/api/v1.0/revisions/%s/validations/%s/entries/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) self.assertEqual('failure', body['status']) 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 document in a bucket can be created without a data section, which depends on substitution from another document. """ rules = { 'deckhand:create_cleartext_documents': '@', 'deckhand:list_validations': '@', 'deckhand:show_validation': '@' } self.policy.set_rules(rules) documents_factory = factories.DocumentFactory(1, [1]) document = documents_factory.gen_test({}, global_abstract=False)[-1] del document['data'] data_schema_factory = factories.DataSchemaFactory() data_schema = data_schema_factory.gen_test(document['schema'], {}) revision_id = self._create_revision(payload=[document, data_schema]) # Validate that the entry is present. 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' }, # Document. { 'id': 1, 'status': 'success' } ] # DataSchema. } self.assertEqual(expected_body, body) # Validate that the created document failed validation for the expected # reason. resp = self.app.simulate_get( '/api/v1.0/revisions/%s/validations/%s/entries/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 = [{ 'error_section': { 'data': None, 'metadata': { 'labels': { 'global': 'global1' }, 'layeringDefinition': { 'abstract': False, 'actions': [], 'layer': 'global' }, 'name': document['metadata']['name'], 'schema': 'metadata/Document/v1.0' }, 'schema': document['schema'] }, 'name': document['metadata']['name'], 'path': '.data', 'schema': document['schema'], 'message': ("None is not of type 'string', 'integer', 'array', 'object'"), 'validation_schema': document_schema.schema, 'schema_path': '.properties.data.type' }] self.assertIn('errors', body) self.assertEqual(expected_errors, body['errors'])
def test_layering_two_parents_one_child_each_2(self): """Scenario: Initially: p1: {"a": {"x": 1, "y": 2}}, p2: {"b": {"f": -9, "g": 71}} Where: c1 references p1 and c2 references p2 Merge "." (p1 -> c1): {"a": {"x": 1, "y": 2, "b": 4}} Merge "." (p2 -> c2): {"b": {"f": -9, "g": 71}, "c": 3} Delete ".c" (p2 -> c2): {"b": {"f": -9, "g": 71}} """ mapping = { "_GLOBAL_DATA_1_": { "data": { "a": { "x": 1, "y": 2 } } }, "_GLOBAL_DATA_2_": { "data": { "b": { "f": -9, "g": 71 } } }, "_SITE_DATA_1_": { "data": { "b": 4 } }, "_SITE_ACTIONS_1_": { "actions": [{ "method": "merge", "path": "." }] }, "_SITE_DATA_2_": { "data": { "c": 3 } }, "_SITE_ACTIONS_2_": { "actions": [{ "method": "merge", "path": "." }, { "method": "delete", "path": ".c" }] } } doc_factory = factories.DocumentFactory(2, [2, 2]) documents = doc_factory.gen_test(mapping, site_abstract=False, site_parent_selectors=[{ 'global': 'global1' }, { 'global': 'global2' }]) site_expected = [{ 'a': { 'x': 1, 'y': 2 }, 'b': 4 }, { "b": { "f": -9, "g": 71 } }] self._test_layering(documents, site_expected)
def test_list_rendered_documents_multiple_buckets(self): """Validates that only the documents from the most recent revision for each bucket in the DB are used for layering. """ rules = { 'deckhand:list_cleartext_documents': '@', 'deckhand:list_encrypted_documents': '@', 'deckhand:create_cleartext_documents': '@' } self.policy.set_rules(rules) bucket_names = ['first', 'first', 'second', 'second'] # Create 2 documents for each revision. (1 `LayeringPolicy` is created # during the very 1st revision). Total = 9. for x in range(4): bucket_name = bucket_names[x] documents_factory = factories.DocumentFactory(2, [1, 1]) payload = documents_factory.gen_test( { '_SITE_ACTIONS_1_': { 'actions': [{ 'method': 'merge', 'path': '.' }] } }, global_abstract=False, site_abstract=False) # Fix up the labels so that each document has a unique parent to # avoid layering errors. payload[-2]['metadata']['labels'] = {'global': bucket_name} payload[-1]['metadata']['layeringDefinition']['parentSelector'] = { 'global': bucket_name } if x > 0: payload = payload[1:] resp = self.app.simulate_put( '/api/v1.0/buckets/%s/documents' % bucket_name, headers={'Content-Type': 'application/x-yaml'}, body=yaml.safe_dump_all(payload)) self.assertEqual(200, resp.status_code) revision_id = list(yaml.safe_load_all( resp.text))[0]['status']['revision'] # Although 9 documents have been created, 4 of those documents are # stale: they were created in older revisions, so expect 5 documents. resp = self.app.simulate_get( '/api/v1.0/revisions/%s/rendered-documents' % revision_id, headers={'Content-Type': 'application/x-yaml'}) self.assertEqual(200, resp.status_code) documents = list(yaml.safe_load_all(resp.text)) documents = sorted(documents, key=lambda x: x['status']['bucket']) # Validate that the LayeringPolicy was returned, then remove it # from documents to validate the rest of them. layering_policies = [ d for d in documents if d['schema'].startswith(types.LAYERING_POLICY_SCHEMA) ] self.assertEqual(1, len(layering_policies)) documents.remove(layering_policies[0]) first_revision_ids = [ d['status']['revision'] for d in documents if d['status']['bucket'] == 'first' ] second_revision_ids = [ d['status']['revision'] for d in documents if d['status']['bucket'] == 'second' ] # Validate correct number of documents, the revision and bucket for # each document. self.assertEqual(4, len(documents)) self.assertEqual(['first', 'first', 'second', 'second'], [d['status']['bucket'] for d in documents]) self.assertEqual(2, len(first_revision_ids)) self.assertEqual(2, len(second_revision_ids)) self.assertEqual([2, 2], first_revision_ids) self.assertEqual([4, 4], second_revision_ids)
def test_layering_without_layering_policy_raises_exc(self): doc_factory = factories.DocumentFactory(1, [1]) documents = doc_factory.gen_test({}, site_abstract=False)[1:] self.assertRaises(errors.LayeringPolicyNotFound, layering.DocumentLayering, documents)
def _test_list_rendered_documents(self, cleartext_secrets): """Validates that destination document that substitutes from an encrypted document is appropriately redacted when ``cleartext_secrets`` is True. """ rules = { 'deckhand:list_cleartext_documents': '@', 'deckhand:list_encrypted_documents': '@', 'deckhand:create_cleartext_documents': '@', 'deckhand:create_encrypted_documents': '@' } self.policy.set_rules(rules) doc_factory = factories.DocumentFactory(1, [1]) layering_policy = doc_factory.gen_test({})[0] layering_policy['data']['layerOrder'] = ['global', 'site'] certificate_data = 'sample-certificate' certificate_ref = ('http://127.0.0.1/key-manager/v1/secrets/%s' % test_utils.rand_uuid_hex()) redacted_data = dd.redact(certificate_ref) doc1 = { 'data': certificate_data, 'schema': 'deckhand/Certificate/v1', 'name': 'example-cert', 'layer': 'site', 'metadata': { 'schema': 'metadata/Document/v1', 'name': 'example-cert', 'layeringDefinition': { 'abstract': False, 'layer': 'site' }, 'storagePolicy': 'encrypted', 'replacement': False } } original_substitutions = [{ 'dest': { 'path': '.' }, 'src': { 'schema': 'deckhand/Certificate/v1', 'name': 'example-cert', 'path': '.' } }] doc2 = { 'data': {}, 'schema': 'example/Kind/v1', 'name': 'deckhand-global', 'layer': 'global', 'metadata': { 'labels': { 'global': 'global1' }, 'storagePolicy': 'cleartext', 'layeringDefinition': { 'abstract': False, 'layer': 'global' }, 'name': 'deckhand-global', 'schema': 'metadata/Document/v1', 'substitutions': original_substitutions, 'replacement': False } } payload = [layering_policy, doc1, doc2] # Create both documents and mock out SecretsManager.create to return # a fake Barbican ref. with mock.patch.object( # noqa secrets_manager.SecretsManager, 'create', return_value=certificate_ref): resp = self.app.simulate_put( '/api/v1.0/buckets/mop/documents', headers={'Content-Type': 'application/x-yaml'}, body=yaml.safe_dump_all(payload)) self.assertEqual(200, resp.status_code) revision_id = list(yaml.safe_load_all( resp.text))[0]['status']['revision'] # Retrieve rendered documents and simulate a Barbican lookup by # causing the actual certificate data to be returned. with mock.patch.object( secrets_manager.SecretsManager, 'get', # noqa return_value=certificate_data): resp = self.app.simulate_get( '/api/v1.0/revisions/%s/rendered-documents' % revision_id, headers={'Content-Type': 'application/x-yaml'}, params={ 'metadata.name': ['example-cert', 'deckhand-global'], 'cleartext-secrets': str(cleartext_secrets) }, params_csv=False) self.assertEqual(200, resp.status_code) rendered_documents = list(yaml.safe_load_all(resp.text)) self.assertEqual(2, len(rendered_documents)) if cleartext_secrets is True: # Expect the cleartext data to be returned. self.assertTrue( all( map(lambda x: x['data'] == certificate_data, rendered_documents))) else: # Expect redacted data for both documents to be returned - # because the destination document should receive redacted data. self.assertTrue( all( map(lambda x: x['data'] == redacted_data, rendered_documents))) destination_doc = next( iter( filter( lambda x: x['metadata']['name'] == 'deckhand-global', rendered_documents))) substitutions = destination_doc['metadata']['substitutions'] self.assertNotEqual(original_substitutions, substitutions)
def setUp(self): super(TestSecretsSubstitutionNegative, self).setUp() self.document_factory = factories.DocumentFactory(1, [1]) self.secrets_factory = factories.DocumentSecretFactory()
def test_layering_two_parents_one_child_each_1(self): mapping = { "_GLOBAL_DATA_1_": { "data": { "a": { "x": 1, "y": 2 } } }, "_GLOBAL_DATA_2_": { "data": { "a": { "x": 1, "y": 2 } } }, "_SITE_DATA_1_": { "data": { "b": 4 } }, "_SITE_ACTIONS_1_": { "actions": [{ "method": "merge", "path": "." }] }, "_SITE_DATA_2_": { "data": { "b": 3 } }, "_SITE_ACTIONS_2_": { "actions": [{ "method": "merge", "path": "." }] } } doc_factory = factories.DocumentFactory(2, [2, 2]) documents = doc_factory.gen_test(mapping, site_abstract=False, site_parent_selectors=[{ 'global': 'global1' }, { 'global': 'global2' }]) site_expected = [{ 'a': { 'x': 1, 'y': 2 }, 'b': 4 }, { 'a': { 'x': 1, 'y': 2 }, 'b': 3 }] self._test_layering(documents, site_expected)
def test_validation_data_schema_different_revision_expect_failure(self): """Validates that creating a ``DataSchema`` in one revision and then creating a document in another revision that relies on the previously created ``DataSchema`` results in an expected failure. """ rules = { 'deckhand:create_cleartext_documents': '@', 'deckhand:list_validations': '@', 'deckhand:list_cleartext_documents': '@', 'deckhand:list_encrypted_documents': '@' } self.policy.set_rules(rules) # Create a `DataSchema` against which the test document will be # validated. data_schema_factory = factories.DataSchemaFactory() metadata_name = 'example/foo/v1' schema_to_use = { '$schema': 'http://json-schema.org/schema#', 'type': 'object', 'properties': { 'a': { 'type': 'integer' # Test doc will fail b/c of wrong type. } }, 'required': ['a'] } data_schema = data_schema_factory.gen_test(metadata_name, data=schema_to_use) revision_id = self._create_revision(payload=[data_schema]) # Validate that the internal deckhand validation was created. resp = self.app.simulate_get( '/api/v1.0/revisions/%s/validations' % revision_id, headers={'Content-Type': 'application/x-yaml'}) self.assertEqual(200, resp.status_code) body = yaml.safe_load(resp.text) expected_body = { 'count': 1, 'results': [{ 'name': types.DECKHAND_SCHEMA_VALIDATION, 'status': 'success' }] } self.assertEqual(expected_body, body) # Create the test document that fails the validation due to the # schema defined by the `DataSchema` document. doc_factory = factories.DocumentFactory(1, [1]) docs_to_test = doc_factory.gen_test( {'_GLOBAL_DATA_1_': { 'data': { 'a': 'fail' } }}, global_abstract=False) docs_to_test[1]['schema'] = 'example/foo/v1' docs_to_test[1]['metadata']['name'] = 'test_doc' revision_id = self._create_revision(payload=docs_to_test + [data_schema]) # Validate that the validation was created and reports failure. resp = self.app.simulate_get( '/api/v1.0/revisions/%s/validations' % revision_id, headers={'Content-Type': 'application/x-yaml'}) self.assertEqual(200, resp.status_code) body = yaml.safe_load(resp.text) expected_body = { 'count': 1, 'results': [{ 'name': types.DECKHAND_SCHEMA_VALIDATION, 'status': 'failure' }] } self.assertEqual(expected_body, body) # Validate that the validation was created and reports failure. resp = self.app.simulate_get( '/api/v1.0/revisions/%s/rendered-documents' % revision_id, headers={'Content-Type': 'application/x-yaml'}) self.assertEqual(400, resp.status_code)
def test_substitution_without_parent_document(self): """Validate that a document with no parent undergoes substitution. global -> do nothing site -> (no parent & no children) requires substitution """ mapping = { "_GLOBAL_DATA_1_": { "data": { "a": { "x": 1, "y": 2 } } }, "_SITE_DATA_1_": { "data": { "b": 4 } }, "_SITE_SUBSTITUTIONS_1_": [{ "dest": { "path": ".c" }, "src": { "schema": "deckhand/Certificate/v1", "name": "site-cert", "path": "." } }], # No layering should be applied as the document has no parent. "_SITE_ACTIONS_1_": { "actions": [{ "method": "merge", "path": "." }] } } doc_factory = factories.DocumentFactory(2, [1, 1]) documents = doc_factory.gen_test(mapping, site_abstract=False, global_abstract=False) # Remove the labels from the global document so that the site document # (the child) has no parent. documents[1]['metadata']['labels'] = {} secrets_factory = factories.DocumentSecretFactory() certificate = secrets_factory.gen_test('Certificate', 'cleartext', data='site-secret', name='site-cert') documents.append(certificate) global_expected = {'a': {'x': 1, 'y': 2}} site_expected = {'b': 4, 'c': 'site-secret'} self._test_layering(documents, site_expected=site_expected, global_expected=global_expected, strict=False)
def test_validation_with_registered_data_schema_expect_mixed(self): rules = { 'deckhand:create_cleartext_documents': '@', 'deckhand:list_validations': '@', 'deckhand:show_validation': '@' } self.policy.set_rules(rules) # Create a `DataSchema` against which the test document will be # validated. data_schema_factory = factories.DataSchemaFactory() metadata_name = 'example/foo/v1' schema_to_use = { '$schema': 'http://json-schema.org/schema#', 'type': 'object', 'properties': { 'a': { 'type': 'integer' # Test doc will fail b/c of wrong type. } }, 'required': ['a'] } expected_errors = [{ 'error_section': { 'a': 'fail' }, 'name': 'test_doc', 'path': '.data.a', 'schema': 'example/foo/v1', 'message': "'fail' is not of type 'integer'", 'validation_schema': schema_to_use, 'schema_path': '.properties.a.type' }] data_schema = data_schema_factory.gen_test(metadata_name, data=schema_to_use) # Create a document that passes validation and another that fails it. doc_factory = factories.DocumentFactory(1, [1]) fail_doc = doc_factory.gen_test( {'_GLOBAL_DATA_1_': { 'data': { 'a': 'fail' } }}, global_abstract=False)[-1] fail_doc['schema'] = 'example/foo/v1' fail_doc['metadata']['name'] = 'test_doc' pass_doc = copy.deepcopy(fail_doc) pass_doc['data']['a'] = 5 revision_id = self._create_revision( payload=[fail_doc, pass_doc, data_schema]) # Validate that the validation reports failure since `fail_doc` # should've failed validation. resp = self.app.simulate_get( '/api/v1.0/revisions/%s/validations' % revision_id, headers={'Content-Type': 'application/x-yaml'}) self.assertEqual(200, resp.status_code) body = yaml.safe_load(resp.text) expected_body = { 'count': 1, 'results': [{ 'name': types.DECKHAND_SCHEMA_VALIDATION, 'status': 'failure' }] } 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': 3, 'results': [ { 'id': 0, 'status': 'failure' }, # fail_doc failed. { 'id': 1, 'status': 'success' }, # DataSchema passed. { 'id': 2, '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/entries/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 = [{ 'error_section': { 'a': 'fail' }, 'name': 'test_doc', 'path': '.data.a', 'schema': 'example/foo/v1', 'message': "'fail' is not of type 'integer'", 'validation_schema': schema_to_use, 'schema_path': '.properties.a.type' }] self.assertIn('errors', body) self.assertEqual(expected_errors, body['errors'])
def test_rendered_documents_fail_post_validation(self): """Validates that when fully rendered documents fail schema validation, a 400 is raised. For this scenario a DataSchema checks that the relevant document has a key in its data section, a key which is removed during the rendering process as the document uses a delete action. This triggers post-rendering validation failure. """ rules = { 'deckhand:list_cleartext_documents': '@', 'deckhand:list_encrypted_documents': '@', 'deckhand:create_cleartext_documents': '@' } self.policy.set_rules(rules) # Create a document for a bucket. documents_factory = factories.DocumentFactory(2, [1, 1]) payload = documents_factory.gen_test( { "_GLOBAL_DATA_1_": { "data": { "a": "b" } }, "_SITE_DATA_1_": { "data": { "a": "b" } }, "_SITE_ACTIONS_1_": { "actions": [{ "method": "delete", "path": "." }] } }, site_abstract=False) data_schema_factory = factories.DataSchemaFactory() metadata_name = payload[-1]['schema'] schema_to_use = { '$schema': 'http://json-schema.org/schema#', 'type': 'object', 'properties': { 'a': { 'type': 'string' } }, 'required': ['a'], 'additionalProperties': False } data_schema = data_schema_factory.gen_test(metadata_name, data=schema_to_use) payload.append(data_schema) resp = self.app.simulate_put( '/api/v1.0/buckets/mop/documents', headers={'Content-Type': 'application/x-yaml'}, body=yaml.safe_dump_all(payload)) self.assertEqual(200, resp.status_code) revision_id = list(yaml.safe_load_all( resp.text))[0]['status']['revision'] resp = self.app.simulate_get( '/api/v1.0/revisions/%s/rendered-documents' % revision_id, headers={'Content-Type': 'application/x-yaml'}) self.assertEqual(400, resp.status_code)
def test_validation_only_new_data_schema_registered(self): """Validate whether newly created DataSchemas replace old DataSchemas when it comes to validation. """ rules = { 'deckhand:create_cleartext_documents': '@', 'deckhand:list_validations': '@' } self.policy.set_rules(rules) # Create 2 DataSchemas that will fail if they're used. These shouldn't # be used for validation. data_schema_factory = factories.DataSchemaFactory() metadata_names = ['exampleA/Doc/v1', 'exampleB/Doc/v1'] schemas_to_use = [{ '$schema': 'http://json-schema.org/schema#', 'type': 'object', 'properties': { 'a': { 'type': 'integer' } }, 'required': ['a'], 'additionalProperties': False }] * 2 old_data_schemas = [ data_schema_factory.gen_test(metadata_names[i], data=schemas_to_use[i]) for i in range(2) ] # Save the DataSchemas in the first revision. revision_id = self._create_revision(payload=old_data_schemas) # Create 2 DataSchemas that will pass if they're used. These should # be used for validation. for schema_to_use in schemas_to_use: schema_to_use['properties']['a']['type'] = 'string' new_data_schemas = [ data_schema_factory.gen_test(metadata_names[i], data=schemas_to_use[i]) for i in range(2) ] doc_factory = factories.DocumentFactory(1, [1]) example1_doc = doc_factory.gen_test( {'_GLOBAL_DATA_1_': { 'data': { 'a': 'whatever' } }}, global_abstract=False)[-1] example1_doc['schema'] = metadata_names[0] example2_doc = copy.deepcopy(example1_doc) example2_doc['schema'] = metadata_names[1] # Save the documents that will be validated alongside the DataSchemas # that will be used to validate them. revision_id = self._create_revision( payload=[example1_doc, example2_doc] + new_data_schemas) # Validate that the validation was created and succeeded: This means # that the new DataSchemas were used, not the old ones. resp = self.app.simulate_get( '/api/v1.0/revisions/%s/validations' % revision_id, headers={'Content-Type': 'application/x-yaml'}) self.assertEqual(200, resp.status_code) body = yaml.safe_load(resp.text) expected_body = { 'count': 1, 'results': [{ 'name': types.DECKHAND_SCHEMA_VALIDATION, 'status': 'success' }] } self.assertEqual(expected_body, body)
def test_validation_with_registered_data_schema(self): rules = {'deckhand:create_cleartext_documents': '@', 'deckhand:list_validations': '@'} self.policy.set_rules(rules) # Register a `DataSchema` against which the test document will be # validated. data_schema_factory = factories.DataSchemaFactory() metadata_name = 'example/Doc/v1' schema_to_use = { '$schema': 'http://json-schema.org/schema#', 'type': 'object', 'properties': { 'a': { 'type': 'string' } }, 'required': ['a'], 'additionalProperties': False } data_schema = data_schema_factory.gen_test( metadata_name, data=schema_to_use) revision_id = self._create_revision(payload=[data_schema]) # Validate that the internal deckhand validation was created. resp = self.app.simulate_get( '/api/v1.0/revisions/%s/validations' % revision_id, headers={'Content-Type': 'application/x-yaml'}) self.assertEqual(200, resp.status_code) body = yaml.safe_load(resp.text) expected_body = { 'count': 1, 'results': [ {'name': types.DECKHAND_SCHEMA_VALIDATION, 'status': 'success'} ] } self.assertEqual(expected_body, body) # Create the test document whose data section adheres to the # `DataSchema` above. doc_factory = factories.DocumentFactory(1, [1]) doc_to_test = doc_factory.gen_test( {'_GLOBAL_DATA_1_': {'data': {'a': 'whatever'}}}, global_abstract=False)[-1] doc_to_test['schema'] = 'example/Doc/v1' revision_id = self._create_revision( payload=[doc_to_test]) # Validate that the validation was created and passed. resp = self.app.simulate_get( '/api/v1.0/revisions/%s/validations' % revision_id, headers={'Content-Type': 'application/x-yaml'}) self.assertEqual(200, resp.status_code) body = yaml.safe_load(resp.text) expected_body = { 'count': 1, 'results': [ {'name': types.DECKHAND_SCHEMA_VALIDATION, 'status': 'success'} ] } self.assertEqual(expected_body, body)