From e66673a583d55f270322932d6307643e95ed35d8 Mon Sep 17 00:00:00 2001 From: Mike Fedosin Date: Sun, 18 Sep 2016 12:58:22 +0300 Subject: [PATCH] Refactor functional tests 1. Move all schema tests to separate file 2. Group all functional tests by their meaning 3. Move testing router to functional/__init__.py file 4. Get rid of lifecycle test, because it duplicate the functionality of other existing tests. Change-Id: I74c038bafee0d2b11ebdeab2cc3aceb4707fd83c --- glare/tests/functional/__init__.py | 13 +- ...t_artifacts.py => test_sample_artifact.py} | 1957 ++++++----------- glare/tests/functional/test_schemas.py | 599 +++++ 3 files changed, 1226 insertions(+), 1343 deletions(-) rename glare/tests/functional/{test_artifacts.py => test_sample_artifact.py} (68%) create mode 100644 glare/tests/functional/test_schemas.py diff --git a/glare/tests/functional/__init__.py b/glare/tests/functional/__init__.py index 4e3e61b..4ad49fe 100644 --- a/glare/tests/functional/__init__.py +++ b/glare/tests/functional/__init__.py @@ -41,7 +41,10 @@ from six.moves import range import six.moves.urllib.parse as urlparse import testtools +from glare.api.v1 import resource +from glare.api.v1 import router from glare.common import utils +from glare.common import wsgi from glare.db.sqlalchemy import api as db_api from glare import tests as glare_tests from glare.tests import utils as test_utils @@ -328,7 +331,7 @@ pipeline = faultwrapper versionnegotiation context glarev1api [app:glarev1api] paste.app_factory = - glare.tests.functional.test_artifacts:TestRouter.factory + glare.tests.functional:TestRouter.factory [filter:faultwrapper] paste.filter_factory = @@ -636,3 +639,11 @@ class FunctionalTest(test_utils.BaseTestCase): for log in logs: if os.path.exists(log): testtools.content.attach_file(self, log) + + +class TestRouter(router.API): + def _get_artifacts_resource(self): + deserializer = resource.RequestDeserializer() + serializer = resource.ResponseSerializer() + controller = resource.ArtifactsController() + return wsgi.Resource(controller, deserializer, serializer) diff --git a/glare/tests/functional/test_artifacts.py b/glare/tests/functional/test_sample_artifact.py similarity index 68% rename from glare/tests/functional/test_artifacts.py rename to glare/tests/functional/test_sample_artifact.py index 40d8ad6..39d8299 100644 --- a/glare/tests/functional/test_artifacts.py +++ b/glare/tests/functional/test_sample_artifact.py @@ -13,30 +13,14 @@ # License for the specific language governing permissions and limitations # under the License. -import jsonschema import uuid from oslo_serialization import jsonutils import requests -from glare.api.v1 import resource -from glare.api.v1 import router -from glare.common import wsgi from glare.tests import functional -def _create_resource(): - deserializer = resource.RequestDeserializer() - serializer = resource.ResponseSerializer() - controller = resource.ArtifactsController() - return wsgi.Resource(controller, deserializer, serializer) - - -class TestRouter(router.API): - def _get_artifacts_resource(self): - return _create_resource() - - def sort_results(lst, target='name'): return sorted(lst, key=lambda x: x[target]) @@ -154,225 +138,47 @@ class TestArtifact(functional.FunctionalTest): return self._check_artifact_method("put", url, data, status=status, headers=headers) - def test_artifact_lifecycle(self): - # test that artifact is available artifact type - response = self.get(url='/schemas', status=200) - self.assertIn('sample_artifact', response['schemas']) + # the test cases below are written in accordance with use cases + # each test tries to cover separate use case in Glare + # all code inside each test tries to cover all operators and data + # involved in use case execution + # each tests represents part of artifact lifecycle + # so we can easily define where is the failed code - # Getting empty artifact list - url = '/sample_artifact' - response = self.get(url=url, status=200) - expected = {'first': '/artifacts/sample_artifact', - 'sample_artifact': [], - 'schema': '/schemas/sample_artifact'} - self.assertEqual(expected, response) + make_active = [{"op": "replace", "path": "/status", "value": "active"}] - # Create an artifact (without any properties) - af = self.create_artifact({'name': 'name5', - 'version': '1.0', - 'tags': ['tag4', 'tag5'], - 'int1': 2048, - 'float1': 987.654, - 'str1': 'lalala', - 'bool1': False}) - self.assertIsNotNone(af['id']) - - # Get the artifact which should have a generated id and status - # 'drafted' - url = '/sample_artifact/%s' % af['id'] - af = self.get(url=url, status=200) - self.assertEqual('drafted', af['status']) - self.assertEqual('private', af['visibility']) - - # Artifact list should now have one entry - url = '/sample_artifact' - response = self.get(url=url, status=200) - self.assertEqual(1, len(response['sample_artifact'])) - - # Change artifact properties with patch request - url = '/sample_artifact/%s' % af['id'] - patch = [{'op': 'replace', - 'value': 'I am the string', - 'path': '/string_mutable'}] - af = self.patch(url=url, data=patch, status=200) - self.assertEqual('I am the string', af['string_mutable']) - patch = [{'op': 'replace', - 'value': 'test', - 'path': '/description'}, - {'op': 'replace', - 'value': 'I am another string', - 'path': '/str1'}] - af = self.patch(url=url, data=patch, status=200) - self.assertEqual('I am another string', af['str1']) - self.assertEqual('test', af['description']) - - # Check that owner cannot be modified - system_update_patch = [ - {'op': 'replace', - 'value': 'any_value', - 'path': '/owner'} - ] - self.patch(url=url, data=system_update_patch, status=403) - - # Add new values to artifact metadata - patch = [{'op': 'add', - 'value': 'custom_value1', - 'path': '/metadata/custom_prop1'}, - {'op': 'add', - 'value': 'custom_value2', - 'path': '/metadata/custom_prop2'} - ] - af = self.patch(url=url, data=patch, status=200) - self.assertEqual('custom_value1', af['metadata']['custom_prop1']) - self.assertEqual('custom_value2', af['metadata']['custom_prop2']) - - # Remove prop from artifact metadata - patch = [{'op': 'remove', - 'path': '/metadata/custom_prop1'}] - af = self.patch(url=url, data=patch, status=200) - self.assertNotIn('custom_prop', af['metadata']) - self.assertEqual('custom_value2', af['metadata']['custom_prop2']) - - # Adding new property 'foo' to the artifact returns 400 error - patch = [{'op': 'add', - 'value': 'bar', - 'path': '/foo'}] - self.patch(url=url, data=patch, status=400) - - # Removing property 'name' from the artifact returns 400 error - patch = [{'op': 'remove', - 'value': 'name', - 'path': '/name'}] - self.patch(url=url, data=patch, status=400) - - # Activation of the artifact should fail with 400 error - url = '/sample_artifact/%s' % af['id'] - data = [{ - "op": "replace", - "path": "/status", - "value": "active" - }] - self.patch(url=url, data=data, status=400) - - # Uploading file to the property 'name' of the artifact should fail - # with 400 error - headers = {'Content-Type': 'application/octet-stream'} - data = "data" * 100 - self.put(url=url + '/name', data=data, status=400, headers=headers) - - # Downloading 'blob' from the artifact should fail with 400 error - self.get(url=url + '/blob', status=400) - - # Upload file to the artifact - af = self.put(url=url + '/blob', data=data, status=200, - headers=headers) - self.assertEqual('active', af['blob']['status']) - - # Modifying status for blob leads to 400 error - patch = [{'op': 'replace', - 'value': 'saving', - 'path': '/blob/status'}] - self.patch(url=url, data=patch, status=400) - - # Set required string - patch = [{'op': 'replace', - 'value': 'I am required string', - 'path': '/string_required'}] - af = self.patch(url=url, data=patch, status=200) - self.assertEqual('I am required string', af['string_required']) - - # Get the artifact, blob property should have status 'active' - af = self.get(url=url, status=200) - self.assertEqual('active', af['blob']['status']) - - # Activate the artifact and check that it has status 'active' - data = [{ - "op": "replace", - "path": "/status", - "value": "active" - }] - af = self.patch(url=url, data=data, status=200) - self.assertEqual('active', af['status']) - - # Changing immutable container format of the artifact fails with - # 400 error - patch = [{'op': 'replace', - 'value': 'I am new string', - 'path': '/string_required'}] - self.patch(url=url, data=patch, status=403) - - # Adding a description of the artifact after activation is okay - patch = [{'op': 'add', - 'value': 'I am the artifact!', - 'path': '/description'}] - self.patch(url=url, data=patch, status=200) - - # Deactivate the artifact with admin and check that it has status - # 'deactivated' + def activate_with_admin(self, artifact_id, status=200): + cur_user = self.current_user self.set_user('admin') - data = [{ - "op": "replace", - "path": "/status", - "value": "deactivated" - }] - af = self.patch(url=url, data=data, status=200) - self.assertEqual('deactivated', af['status']) + url = '/sample_artifact/%s' % artifact_id + af = self.patch(url=url, data=self.make_active, status=status) + self.set_user(cur_user) + return af - # Only admin can download de-activated artifacts - self.assertEqual("data" * 100, - self.get(url=url + '/blob', status=200)) + make_deactivated = [{"op": "replace", "path": "/status", + "value": "deactivated"}] - # Reactivate the artifact and check that it has status 'active' - data = [{ - "op": "replace", - "path": "/status", - "value": "active" - }] - af = self.patch(url=url, data=data, status=200) - self.assertEqual('active', af['status']) + def deactivate_with_admin(self, artifact_id, status=200): + cur_user = self.current_user + self.set_user('admin') + url = '/sample_artifact/%s' % artifact_id + af = self.patch(url=url, data=self.make_deactivated, status=status) + self.set_user(cur_user) + return af - # Delete the artifact - self.set_user('user1') - self.delete(url=url, status=204) - self.get(url=url, status=404) + make_public = [{"op": "replace", "path": "/visibility", "value": "public"}] - def test_blob_dicts(self): - # Getting empty artifact list - url = '/sample_artifact' - response = self.get(url=url, status=200) - expected = {'first': '/artifacts/sample_artifact', - 'sample_artifact': [], - 'schema': '/schemas/sample_artifact'} - self.assertEqual(expected, response) + def publish_with_admin(self, artifact_id, status=200): + cur_user = self.current_user + self.set_user('admin') + url = '/sample_artifact/%s' % artifact_id + af = self.patch(url=url, data=self.make_public, status=status) + self.set_user(cur_user) + return af - # Create a test artifact - art = self.create_artifact(status=201, data={'name': 'test', - 'version': '1.0', - 'string_required': '123'}) - self.assertIsNotNone(art['id']) - # Get the artifact which should have a generated id and status - # 'drafted' - url = '/sample_artifact/%s' % art['id'] - art_1 = self.get(url=url, status=200) - self.assertIsNotNone(art_1['id']) - self.assertEqual('drafted', art_1['status']) - - # Upload data to blob dict - headers = {'Content-Type': 'application/octet-stream'} - data = "data" * 100 - - self.put(url=url + '/dict_of_blobs/new_blob', - data=data, status=200, headers=headers) - - # Download data from blob dict - self.assertEqual(data, self.get(url=url + '/dict_of_blobs/new_blob', - status=200)) - - # download blob from undefined dict property - self.get(url=url + '/not_a_dict/not_a_blob', status=400) - - def test_artifact_marker_and_limit(self): +class TestList(TestArtifact): + def test_list_marker_and_limit(self): # Create artifacts art_list = [self.create_artifact({'name': 'name%s' % i, 'version': '1.0', @@ -417,7 +223,7 @@ class TestArtifact(functional.FunctionalTest): result = self.get(url=marker[10:]) self.assertEqual([art_list[0]], result['sample_artifact']) - def test_artifact_filters(self): + def test_list_base_filters(self): # Create artifact art_list = [self.create_artifact({'name': 'name%s' % i, 'version': '1.0', @@ -619,7 +425,7 @@ class TestArtifact(functional.FunctionalTest): result = sort_results(self.get(url=url)['sample_artifact']) self.assertEqual(art_list[5:], result) - def test_artifact_dict_prop_filters(self): + def test_list_dict_prop_filters(self): # Create artifact art_list = [self.create_artifact({'name': 'name0', 'version': '1.0', @@ -691,124 +497,104 @@ class TestArtifact(functional.FunctionalTest): url = '/sample_artifact?dict_of_int.1=lala' self.get(url=url, status=400) - def test_artifact_tags(self): - # Create artifact - art = self.create_artifact({'name': 'name5', - 'version': '1.0', - 'tags': ['tag1', 'tag2', 'tag3'], - 'int1': 2048, - 'float1': 987.654, - 'str1': 'lalala', - 'bool1': False, - 'string_required': '123'}) - self.assertIsNotNone(art['id']) + def test_list_sorted(self): + art_list = [self.create_artifact({'name': 'name%s' % i, + 'version': '1.0', + 'tags': ['tag%s' % i], + 'int1': i, + 'float1': 123.456 + (-0.9) ** i, + 'str1': 'bugaga', + 'bool1': True, + 'list_of_int': [11, 22, - i], + 'dict_of_int': {'one': 4 * i, + 'two': (-2) ** i}}) + for i in range(5)] - url = '/sample_artifact/%s' % art['id'] - data = [{ - "op": "replace", - "path": "/status", - "value": "active" - }] - art = self.patch(url=url, data=data, status=200) - self.assertEqual('active', art['status']) - art = self.publish_with_admin(art['id']) - self.assertEqual('public', art['visibility']) - # only admins can update tags for public artifacts - self.set_user("admin") - # Check that tags created correctly - url = '/sample_artifact/%s/tags' % art['id'] - tags = self.get(url=url, status=200) - for tag in ['tag1', 'tag2', 'tag3']: - self.assertIn(tag, tags['tags']) + # sorted by string 'asc' + url = '/sample_artifact?sort=name:asc' + result = self.get(url=url) + expected = sort_results(art_list) + self.assertEqual(expected, result['sample_artifact']) - # Get the list of tags - url = '/sample_artifact/%s/tags' % art['id'] - tags = self.get(url=url, status=200) - for tag in ['tag1', 'tag2', 'tag3']: - self.assertIn(tag, tags['tags']) + # sorted by string 'desc' + url = '/sample_artifact?sort=name:desc' + result = self.get(url=url) + expected = sort_results(art_list) + expected.reverse() + self.assertEqual(expected, result['sample_artifact']) - # Set new tag list to the art - body = {"tags": ["new_tag1", "new_tag2", "new_tag3"]} - tags = self.put(url=url, data=body, status=200) - for tag in ['new_tag1', 'new_tag2', 'new_tag3']: - self.assertIn(tag, tags['tags']) + # sorted by int 'asc' + url = '/sample_artifact?sort=int1:asc' + result = self.get(url=url) + expected = sort_results(art_list, target='int1') + self.assertEqual(expected, result['sample_artifact']) - # Delete all tags from the art - url = '/sample_artifact/%s/tags' % art['id'] - self.delete(url=url, status=204) + # sorted by int 'desc' + url = '/sample_artifact?sort=int1:desc' + result = self.get(url=url) + expected = sort_results(art_list, target='int1') + expected.reverse() + self.assertEqual(expected, result['sample_artifact']) - # Get the list of tags - url = '/sample_artifact/%s/tags' % art['id'] - tags = self.get(url=url, status=200) - self.assertEqual([], tags['tags']) + # sorted by float 'asc' + url = '/sample_artifact?sort=float1:asc' + result = self.get(url=url) + expected = sort_results(art_list, target='float1') + self.assertEqual(expected, result['sample_artifact']) - # Modifing tags with PATCH leads to 400 error - url = '/sample_artifact/%s' % art['id'] - patch = [{'op': 'remove', - 'path': '/tags'}] - self.patch(url=url, data=patch, status=400) + # sorted by float 'desc' + url = '/sample_artifact?sort=float1:desc' + result = self.get(url=url) + expected = sort_results(art_list, target='float1') + expected.reverse() + self.assertEqual(expected, result['sample_artifact']) - def test_add_custom_location(self): - # Create artifact - art = self.create_artifact({'name': 'name5', - 'version': '1.0', - 'tags': ['tag1', 'tag2', 'tag3'], - 'int1': 2048, - 'float1': 987.654, - 'str1': 'lalala', - 'bool1': False, - 'string_required': '123'}) - self.assertIsNotNone(art['id']) + # sorted by unsorted 'asc' + url = '/sample_artifact?sort=bool1:asc' + self.get(url=url, status=400) - # Set custom location - url = '/sample_artifact/%s' % art['id'] - body = jsonutils.dumps( - {'url': 'https://www.apache.org/licenses/LICENSE-2.0.txt', - 'checksum': "fake"}) - headers = {'Content-Type': - 'application/vnd+openstack.glare-custom-location+json'} - self.put(url=url + '/blob', data=body, - status=200, headers=headers) + # sorted by unsorted 'desc' + url = '/sample_artifact?sort=bool1:desc' + self.get(url=url, status=400) - # test re-add failed - self.put(url=url + '/blob', data=body, status=409, headers=headers) - # add to non-existing property - self.put(url=url + '/blob_non_exist', data=body, status=400, - headers=headers) + # sorted by non-existent 'asc' + url = '/sample_artifact?sort=non_existent:asc' + self.get(url=url, status=400) - # Get the artifact, blob property should have status 'active' - art = self.get(url=url, status=200) - self.assertEqual('active', art['blob']['status']) - self.assertIsNotNone(art['blob']['checksum']) - self.assertIsNone(art['blob']['size']) - self.assertIsNone(art['blob']['content_type']) - self.assertEqual('https://www.apache.org/licenses/LICENSE-2.0.txt', - art['blob']['url']) - self.assertNotIn('id', art['blob']) + # sorted by non-existent 'desc' + url = '/sample_artifact?sort=non_existent:desc' + self.get(url=url, status=400) - # Set custom location - url = '/sample_artifact/%s' % art['id'] - self.put(url=url + '/dict_of_blobs/blob', data=body, - status=200, headers=headers) + # sorted by invalid op + url = '/sample_artifact?sort=name:invalid_op' + self.get(url=url, status=400) - # Get the artifact, blob property should have status 'active' - art = self.get(url=url, status=200) - self.assertEqual('active', art['dict_of_blobs']['blob']['status']) - self.assertIsNotNone(art['dict_of_blobs']['blob']['checksum']) - self.assertIsNone(art['dict_of_blobs']['blob']['size']) - self.assertIsNone(art['dict_of_blobs']['blob']['content_type']) - self.assertEqual('https://www.apache.org/licenses/LICENSE-2.0.txt', - art['dict_of_blobs']['blob']['url']) - self.assertNotIn('id', art['dict_of_blobs']['blob']) - # test re-add failed - self.put(url=url + '/dict_of_blobs/blob', data=body, status=409, - headers=headers) + # sorted without op + url = '/sample_artifact?sort=name' + result = self.get(url=url) + expected = sort_results(art_list) + expected.reverse() + self.assertEqual(expected, result['sample_artifact']) - # test request failed with non-json containment - self.put(url=url + '/dict_of_blobs/blob_incorrect', data="incorrect", - status=400, headers=headers) + # sorted by list + url = '/sample_artifact?sort=list_of_int:asc' + self.get(url=url, status=400) - def test_artifact_version(self): + # sorted by dict + url = '/sample_artifact?sort=dict_of_int:asc' + self.get(url=url, status=400) + + # sorted by element of dict + url = '/sample_artifact?sort=dict_of_int.one:asc' + self.get(url=url, status=400) + + # sorted by any prop + url = '/sample_artifact?sort=name:asc,int1:desc' + result = self.get(url=url) + expected = sort_results(sort_results(art_list), target='int1') + self.assertEqual(expected, result['sample_artifact']) + + def test_list_versions(self): # Create artifacts with versions version_list = ['1.0', '1.1', '2.0.0', '2.0.1-beta', '2.0.1', '20.0'] @@ -890,45 +676,316 @@ class TestArtifact(functional.FunctionalTest): result = self.get(url=url)['sample_artifact'] self.assertEqual(list(reversed(art_list)), result) - # the test cases below are written in accordance with use cases - # each test tries to cover separate use case in Glare - # all code inside each test tries to cover all operators and data - # involved in use case execution - # each tests represents part of artifact lifecycle - # so we can easily define where is the failed code + def test_list_latest_filter(self): + # Create artifacts with versions + group1_versions = ['1.0', '20.0', '2.0.0', '2.0.1-beta', '2.0.1'] + group2_versions = ['1', '1000.0.1-beta', '99.0', + '1000.0.1-alpha', '1000.0.1'] - make_active = [{"op": "replace", "path": "/status", "value": "active"}] + for i in range(5): + self.create_artifact( + {'name': 'group1', + 'version': group1_versions[i], + 'tags': ['tag%s' % i], + 'int1': 2048, + 'float1': 123.456, + 'str1': 'bugaga', + 'bool1': True}) + self.create_artifact( + {'name': 'group2', + 'version': group2_versions[i], + 'tags': ['tag%s' % i], + 'int1': 2048, + 'float1': 123.456, + 'str1': 'bugaga', + 'bool1': True}) - def activate_with_admin(self, artifact_id, status=200): - cur_user = self.current_user + url = '/sample_artifact?version=latest&sort=name:asc' + res = self.get(url=url, status=200)['sample_artifact'] + self.assertEqual(2, len(res)) + self.assertEqual('20.0.0', res[0]['version']) + self.assertEqual('1000.0.1', res[1]['version']) + + url = '/sample_artifact?version=latest&name=group1' + res = self.get(url=url, status=200)['sample_artifact'] + self.assertEqual(1, len(res)) + self.assertEqual('20.0.0', res[0]['version']) + + url = '/sample_artifact?version=latest&name=group2' + res = self.get(url=url, status=200)['sample_artifact'] + self.assertEqual(1, len(res)) + self.assertEqual('1000.0.1', res[0]['version']) + + def test_list_support_unicode_filters(self): + unicode_text = u'\u041f\u0420\u0418\u0412\u0415\u0422' + art1 = self.create_artifact(data={'name': unicode_text}) + self.assertEqual(unicode_text, art1['name']) + + mixed_text = u'la\u041f' + art2 = self.create_artifact(data={'name': mixed_text}) + self.assertEqual(mixed_text, art2['name']) + + headers = {'Content-Type': 'text/html; charset=UTF-8'} + url = u'/sample_artifact?name=\u041f\u0420\u0418\u0412\u0415\u0422' + response_url = u'/artifacts/sample_artifact?name=' \ + u'%D0%9F%D0%A0%D0%98%D0%92%D0%95%D0%A2' + result = self.get(url=url, headers=headers) + self.assertEqual(art1, result['sample_artifact'][0]) + self.assertEqual(response_url, result['first']) + + +class TestBlobs(TestArtifact): + def test_blob_dicts(self): + # Getting empty artifact list + url = '/sample_artifact' + response = self.get(url=url, status=200) + expected = {'first': '/artifacts/sample_artifact', + 'sample_artifact': [], + 'schema': '/schemas/sample_artifact'} + self.assertEqual(expected, response) + + # Create a test artifact + art = self.create_artifact(status=201, + data={'name': 'test', + 'version': '1.0', + 'string_required': '123'}) + self.assertIsNotNone(art['id']) + + # Get the artifact which should have a generated id and status + # 'drafted' + url = '/sample_artifact/%s' % art['id'] + art_1 = self.get(url=url, status=200) + self.assertIsNotNone(art_1['id']) + self.assertEqual('drafted', art_1['status']) + + # Upload data to blob dict + headers = {'Content-Type': 'application/octet-stream'} + data = "data" * 100 + + self.put(url=url + '/dict_of_blobs/new_blob', + data=data, status=200, headers=headers) + + # Download data from blob dict + self.assertEqual(data, + self.get(url=url + '/dict_of_blobs/new_blob', + status=200)) + + # download blob from undefined dict property + self.get(url=url + '/not_a_dict/not_a_blob', status=400) + + def test_blob_upload(self): + # create artifact with blob + data = 'data' + self.create_artifact( + data={'name': 'test_af', 'blob': data, + 'version': '0.0.1'}, status=400) + art = self.create_artifact(data={'name': 'test_af', + 'version': '0.0.1', + 'string_required': 'test'}) + url = '/sample_artifact/%s' % art['id'] + headers = {'Content-Type': 'application/octet-stream'} + + # upload to non-existing property + self.put(url=url + '/blob_non_exist', data=data, status=400, + headers=headers) + + # upload too big value + big_data = "this is the smallest big data" + self.put(url=url + '/small_blob', data=big_data, status=413, + headers=headers) + # upload correct blob value + self.put(url=url + '/small_blob', data=big_data[:2], headers=headers) + + # Upload artifact via different user + self.set_user('user2') + self.put(url=url + '/blob', data=data, status=404, + headers=headers) + + # Upload file to the artifact + self.set_user('user1') + art = self.put(url=url + '/blob', data=data, status=200, + headers=headers) + self.assertEqual('active', art['blob']['status']) + self.assertEqual('application/octet-stream', + art['blob']['content_type']) + self.assertIn('url', art['blob']) + self.assertNotIn('id', art['blob']) + + # reUpload file to artifact + self.put(url=url + '/blob', data=data, status=409, + headers=headers) + # upload blob dict + self.put(url + '/dict_of_blobs/test_key', data=data, headers=headers) + # test re-upload failed + self.put(url + '/dict_of_blobs/test_key', data=data, headers=headers, + status=409) + + # upload few other blobs to the dict + for elem in ('aaa', 'bbb', 'ccc', 'ddd'): + self.put(url + '/dict_of_blobs/' + elem, data=data, + headers=headers) + + # upload to active artifact + self.patch(url, self.make_active) + self.put(url + '/dict_of_blobs/key2', data=data, status=403, + headers=headers) + + self.delete(url) + + def test_blob_download(self): + data = 'data' + art = self.create_artifact(data={'name': 'test_af', + 'version': '0.0.1'}) + url = '/sample_artifact/%s' % art['id'] + + # download not uploaded blob + self.get(url=url + '/blob', status=400) + + # download blob from not existing artifact + self.get(url=url + '1/blob', status=404) + + # download blob from undefined property + self.get(url=url + '/not_a_blob', status=400) + + headers = {'Content-Type': 'application/octet-stream'} + art = self.put(url=url + '/blob', data=data, status=200, + headers=headers) + self.assertEqual('active', art['blob']['status']) + + blob_data = self.get(url=url + '/blob') + self.assertEqual(data, blob_data) + + # download artifact via admin self.set_user('admin') - url = '/sample_artifact/%s' % artifact_id - af = self.patch(url=url, data=self.make_active, status=status) - self.set_user(cur_user) - return af + blob_data = self.get(url=url + '/blob') + self.assertEqual(data, blob_data) - make_deactivated = [{"op": "replace", "path": "/status", - "value": "deactivated"}] + # try to download blob via different user + self.set_user('user2') + self.get(url=url + '/blob', status=404) - def deactivate_with_admin(self, artifact_id, status=200): - cur_user = self.current_user - self.set_user('admin') - url = '/sample_artifact/%s' % artifact_id - af = self.patch(url=url, data=self.make_deactivated, status=status) - self.set_user(cur_user) - return af + def test_blob_add_custom_location(self): + # Create artifact + art = self.create_artifact({'name': 'name5', + 'version': '1.0', + 'tags': ['tag1', 'tag2', 'tag3'], + 'int1': 2048, + 'float1': 987.654, + 'str1': 'lalala', + 'bool1': False, + 'string_required': '123'}) + self.assertIsNotNone(art['id']) - make_public = [{"op": "replace", "path": "/visibility", "value": "public"}] + # Set custom location + url = '/sample_artifact/%s' % art['id'] + body = jsonutils.dumps( + {'url': 'https://www.apache.org/licenses/LICENSE-2.0.txt', + 'checksum': "fake"}) + headers = {'Content-Type': + 'application/vnd+openstack.glare-custom-location+json'} + self.put(url=url + '/blob', data=body, + status=200, headers=headers) - def publish_with_admin(self, artifact_id, status=200): - cur_user = self.current_user - self.set_user('admin') - url = '/sample_artifact/%s' % artifact_id - af = self.patch(url=url, data=self.make_public, status=status) - self.set_user(cur_user) - return af + # test re-add failed + self.put(url=url + '/blob', data=body, status=409, headers=headers) + # add to non-existing property + self.put(url=url + '/blob_non_exist', data=body, status=400, + headers=headers) - def test_create_artifact(self): + # Get the artifact, blob property should have status 'active' + art = self.get(url=url, status=200) + self.assertEqual('active', art['blob']['status']) + self.assertIsNotNone(art['blob']['checksum']) + self.assertIsNone(art['blob']['size']) + self.assertIsNone(art['blob']['content_type']) + self.assertEqual('https://www.apache.org/licenses/LICENSE-2.0.txt', + art['blob']['url']) + self.assertNotIn('id', art['blob']) + + # Set custom location + url = '/sample_artifact/%s' % art['id'] + self.put(url=url + '/dict_of_blobs/blob', data=body, + status=200, headers=headers) + + # Get the artifact, blob property should have status 'active' + art = self.get(url=url, status=200) + self.assertEqual('active', art['dict_of_blobs']['blob']['status']) + self.assertIsNotNone(art['dict_of_blobs']['blob']['checksum']) + self.assertIsNone(art['dict_of_blobs']['blob']['size']) + self.assertIsNone(art['dict_of_blobs']['blob']['content_type']) + self.assertEqual('https://www.apache.org/licenses/LICENSE-2.0.txt', + art['dict_of_blobs']['blob']['url']) + self.assertNotIn('id', art['dict_of_blobs']['blob']) + # test re-add failed + self.put(url=url + '/dict_of_blobs/blob', data=body, status=409, + headers=headers) + + # test request failed with non-json containment + self.put(url=url + '/dict_of_blobs/blob_incorrect', data="incorrect", + status=400, headers=headers) + + +class TestTags(TestArtifact): + def test_tags(self): + # Create artifact + art = self.create_artifact({'name': 'name5', + 'version': '1.0', + 'tags': ['tag1', 'tag2', 'tag3'], + 'int1': 2048, + 'float1': 987.654, + 'str1': 'lalala', + 'bool1': False, + 'string_required': '123'}) + self.assertIsNotNone(art['id']) + + url = '/sample_artifact/%s' % art['id'] + data = [{ + "op": "replace", + "path": "/status", + "value": "active" + }] + art = self.patch(url=url, data=data, status=200) + self.assertEqual('active', art['status']) + art = self.publish_with_admin(art['id']) + self.assertEqual('public', art['visibility']) + # only admins can update tags for public artifacts + self.set_user("admin") + # Check that tags created correctly + url = '/sample_artifact/%s/tags' % art['id'] + tags = self.get(url=url, status=200) + for tag in ['tag1', 'tag2', 'tag3']: + self.assertIn(tag, tags['tags']) + + # Get the list of tags + url = '/sample_artifact/%s/tags' % art['id'] + tags = self.get(url=url, status=200) + for tag in ['tag1', 'tag2', 'tag3']: + self.assertIn(tag, tags['tags']) + + # Set new tag list to the art + body = {"tags": ["new_tag1", "new_tag2", "new_tag3"]} + tags = self.put(url=url, data=body, status=200) + for tag in ['new_tag1', 'new_tag2', 'new_tag3']: + self.assertIn(tag, tags['tags']) + + # Delete all tags from the art + url = '/sample_artifact/%s/tags' % art['id'] + self.delete(url=url, status=204) + + # Get the list of tags + url = '/sample_artifact/%s/tags' % art['id'] + tags = self.get(url=url, status=200) + self.assertEqual([], tags['tags']) + + # Modifing tags with PATCH leads to 400 error + url = '/sample_artifact/%s' % art['id'] + patch = [{'op': 'remove', + 'path': '/tags'}] + self.patch(url=url, data=patch, status=400) + + +class TestArtifactOps(TestArtifact): + def test_create(self): """All tests related to artifact creation""" # check that cannot create artifact for non-existent artifact type self.post('/incorrect_artifact', {}, status=404) @@ -1032,26 +1089,184 @@ class TestArtifact(functional.FunctionalTest): self.create_artifact(data={"name": "test_af", "string_required": "test_str"}) - def test_manage_dependencies(self): - some_af = self.create_artifact(data={"name": "test_af"}) - dep_af = self.create_artifact(data={"name": "test_dep_af"}) - dep_url = "/artifacts/sample_artifact/%s" % some_af['id'] + def test_activate(self): + # create artifact to update + private_art = self.create_artifact( + data={"name": "test_af", + "version": "0.0.1"}) + # cannot activate artifact without required for activate attributes + url = '/sample_artifact/%s' % private_art['id'] + self.patch(url=url, data=self.make_active, status=400) + add_required = [{ + "op": "replace", + "path": "/string_required", + "value": "string" + }] + self.patch(url=url, data=add_required) + # cannot activate if body contains non status changes + incorrect = self.make_active + [{"op": "replace", + "path": "/name", + "value": "test"}] + self.patch(url=url, data=incorrect, status=400) + # can activate if body contains only status changes + make_active_without_updates = self.make_active + add_required + active_art = self.patch(url=url, data=make_active_without_updates) + private_art['status'] = 'active' + private_art['activated_at'] = active_art['activated_at'] + private_art['updated_at'] = active_art['updated_at'] + private_art['string_required'] = 'string' + self.assertEqual(private_art, active_art) + # check that active artifact is not available for other user + self.set_user("user2") + self.get(url, status=404) + self.set_user("user1") - # set valid dependency - patch = [{"op": "replace", "path": "/dependency1", "value": dep_url}] - url = '/sample_artifact/%s' % dep_af['id'] - af = self.patch(url=url, data=patch) - self.assertEqual(af['dependency1'], dep_url) + # test that activate is idempotent + self.patch(url=url, data=self.make_active) + # test activate deleted artifact + self.delete(url=url) + self.patch(url=url, data=self.make_active, status=404) - # remove dependency from artifact - patch = [{"op": "replace", "path": "/dependency1", "value": None}] - af = self.patch(url=url, data=patch) - self.assertIsNone(af['dependency1']) + def test_publish(self): + # create artifact to update + self.set_user('admin') + private_art = self.create_artifact( + data={"name": "test_af", "string_required": "test_str", + "version": "0.0.1"}) - # try to set invalid dependency - patch = [{"op": "replace", "path": "/dependency1", "value": "Invalid"}] + url = '/sample_artifact/%s' % private_art['id'] + self.patch(url=url, data=self.make_active) + + # test that only visibility must be specified in the request + incorrect = self.make_public + [{"op": "replace", + "path": "/string_mutable", + "value": "test"}] + self.patch(url=url, data=incorrect, status=400) + # check public artifact + public_art = self.patch(url=url, data=self.make_public) + private_art['activated_at'] = public_art['activated_at'] + private_art['visibility'] = 'public' + private_art['status'] = 'active' + private_art['updated_at'] = public_art['updated_at'] + self.assertEqual(private_art, public_art) + # check that public artifact available for simple user + self.set_user("user1") + self.get(url) + self.set_user("admin") + # test that artifact publish with the same name and version failed + duplicate_art = self.create_artifact( + data={"name": "test_af", "string_required": "test_str", + "version": "0.0.1"}) + dup_url = '/sample_artifact/%s' % duplicate_art['id'] + # test that we cannot publish drafted artifact + self.patch(url=dup_url, data=self.make_public, status=400) + # proceed with duplicate testing + self.patch(url=dup_url, data=self.make_active) + self.patch(url=dup_url, data=self.make_public, status=409) + # test that cannot publish deactivated artifact + self.patch(dup_url, data=self.make_deactivated) + self.patch(dup_url, data=self.make_public, status=400) + + def test_delete(self): + # try ro delete not existing artifact + url = '/sample_artifact/111111' + self.delete(url=url, status=404) + + # check that we can delete artifact with soft dependency + art = self.create_artifact( + data={"name": "test_af", "string_required": "test_str", + "version": "0.0.1"}) + artd = self.create_artifact( + data={"name": "test_afd", "string_required": "test_str", + "version": "0.0.1", + "dependency1": '/artifacts/sample_artifact/%s' % art['id']}) + + url = '/sample_artifact/%s' % artd['id'] + self.delete(url=url, status=204) + + # try to change status of artifact to deleting + url = '/sample_artifact/%s' % art['id'] + patch = [{'op': 'replace', + 'value': 'deleting', + 'path': '/status'}] self.patch(url=url, data=patch, status=400) + # delete artifact via different user (non admin) + self.set_user('user2') + self.delete(url=url, status=404) + + # delete artifact via admin user + self.set_user('admin') + self.delete(url=url, status=204) + + # delete public artifact via different user + self.set_user('user1') + art = self.create_artifact( + data={"name": "test_af", "string_required": "test_str", + "version": "0.0.1"}) + url = '/sample_artifact/%s' % art['id'] + self.patch(url=url, data=self.make_active) + self.publish_with_admin(art['id']) + self.set_user('user2') + self.delete(url=url, status=403) + + self.set_user('user1') + self.delete(url=url, status=403) + self.set_user('admin') + self.delete(url=url) + + # delete deactivated artifact + art = self.create_artifact( + data={"name": "test_af", "string_required": "test_str", + "version": "0.0.1"}) + url = '/sample_artifact/%s' % art['id'] + self.patch(url=url, data=self.make_active) + self.patch(url=url, data=self.make_deactivated) + self.delete(url=url, status=204) + self.get(url=url, status=404) + self.assertEqual(0, len(self.get( + url='/sample_artifact')['sample_artifact'])) + + def test_deactivate(self): + # test artifact deactivate for non-active artifact + private_art = self.create_artifact( + data={"name": "test_af", "string_required": "test_str", + "version": "0.0.1"}) + url = '/sample_artifact/%s' % private_art['id'] + self.deactivate_with_admin(private_art['id'], 400) + self.patch(url, self.make_active) + self.set_user('admin') + # test cannot deactivate if there is something else in request + incorrect = self.make_deactivated + [{"op": "replace", + "path": "/name", + "value": "test"}] + self.patch(url, incorrect, 400) + self.set_user('user1') + # test artifact deactivate success + deactive_art = self.deactivate_with_admin(private_art['id']) + self.assertEqual("deactivated", deactive_art["status"]) + # test deactivate is idempotent + self.patch(url, self.make_deactivated) + + def test_reactivate(self): + self.set_user('admin') + private_art = self.create_artifact( + data={"name": "test_af", "string_required": "test_str", + "version": "0.0.1"}) + url = '/sample_artifact/%s' % private_art['id'] + self.patch(url, self.make_active) + self.deactivate_with_admin(private_art['id']) + # test cannot reactivate if there is something else in request + incorrect = self.make_active + [{"op": "replace", + "path": "/name", + "value": "test"}] + self.patch(url, incorrect, 400) + # test artifact reactivate success + deactive_art = self.patch(url, self.make_active) + self.assertEqual("active", deactive_art["status"]) + + +class TestUpdate(TestArtifact): def test_update_artifact_before_activate(self): """Test updates for artifact before activation""" # create artifact to update @@ -1178,85 +1393,7 @@ class TestArtifact(functional.FunctionalTest): self.publish_with_admin(private_art['id']) self.patch(url=dupv_url, data=change_version) - def test_artifact_activate(self): - # create artifact to update - private_art = self.create_artifact( - data={"name": "test_af", - "version": "0.0.1"}) - # cannot activate artifact without required for activate attributes - url = '/sample_artifact/%s' % private_art['id'] - self.patch(url=url, data=self.make_active, status=400) - add_required = [{ - "op": "replace", - "path": "/string_required", - "value": "string" - }] - self.patch(url=url, data=add_required) - # cannot activate if body contains non status changes - incorrect = self.make_active + [{"op": "replace", - "path": "/name", - "value": "test"}] - self.patch(url=url, data=incorrect, status=400) - # can activate if body contains only status changes - make_active_without_updates = self.make_active + add_required - active_art = self.patch(url=url, data=make_active_without_updates) - private_art['status'] = 'active' - private_art['activated_at'] = active_art['activated_at'] - private_art['updated_at'] = active_art['updated_at'] - private_art['string_required'] = 'string' - self.assertEqual(private_art, active_art) - # check that active artifact is not available for other user - self.set_user("user2") - self.get(url, status=404) - self.set_user("user1") - - # test that activate is idempotent - self.patch(url=url, data=self.make_active) - # test activate deleted artifact - self.delete(url=url) - self.patch(url=url, data=self.make_active, status=404) - - def test_artifact_publish(self): - # create artifact to update - self.set_user('admin') - private_art = self.create_artifact( - data={"name": "test_af", "string_required": "test_str", - "version": "0.0.1"}) - - url = '/sample_artifact/%s' % private_art['id'] - self.patch(url=url, data=self.make_active) - - # test that only visibility must be specified in the request - incorrect = self.make_public + [{"op": "replace", - "path": "/string_mutable", - "value": "test"}] - self.patch(url=url, data=incorrect, status=400) - # check public artifact - public_art = self.patch(url=url, data=self.make_public) - private_art['activated_at'] = public_art['activated_at'] - private_art['visibility'] = 'public' - private_art['status'] = 'active' - private_art['updated_at'] = public_art['updated_at'] - self.assertEqual(private_art, public_art) - # check that public artifact available for simple user - self.set_user("user1") - self.get(url) - self.set_user("admin") - # test that artifact publish with the same name and version failed - duplicate_art = self.create_artifact( - data={"name": "test_af", "string_required": "test_str", - "version": "0.0.1"}) - dup_url = '/sample_artifact/%s' % duplicate_art['id'] - # test that we cannot publish drafted artifact - self.patch(url=dup_url, data=self.make_public, status=400) - # proceed with duplicate testing - self.patch(url=dup_url, data=self.make_active) - self.patch(url=dup_url, data=self.make_public, status=409) - # test that cannot publish deactivated artifact - self.patch(dup_url, data=self.make_deactivated) - self.patch(dup_url, data=self.make_public, status=400) - - def test_artifact_update_after_activate_and_publish(self): + def test_update_after_activate_and_publish(self): # activate artifact private_art = self.create_artifact( data={"name": "test_af", "string_required": "test_str", @@ -1297,196 +1434,7 @@ class TestArtifact(functional.FunctionalTest): self.set_user("admin") self.patch(url, upd_mutable) - def test_artifact_delete(self): - # try ro delete not existing artifact - url = '/sample_artifact/111111' - self.delete(url=url, status=404) - - # check that we can delete artifact with soft dependency - art = self.create_artifact( - data={"name": "test_af", "string_required": "test_str", - "version": "0.0.1"}) - artd = self.create_artifact( - data={"name": "test_afd", "string_required": "test_str", - "version": "0.0.1", - "dependency1": '/artifacts/sample_artifact/%s' % art['id']}) - - url = '/sample_artifact/%s' % artd['id'] - self.delete(url=url, status=204) - - # try to change status of artifact to deleting - url = '/sample_artifact/%s' % art['id'] - patch = [{'op': 'replace', - 'value': 'deleting', - 'path': '/status'}] - self.patch(url=url, data=patch, status=400) - - # delete artifact via different user (non admin) - self.set_user('user2') - self.delete(url=url, status=404) - - # delete artifact via admin user - self.set_user('admin') - self.delete(url=url, status=204) - - # delete public artifact via different user - self.set_user('user1') - art = self.create_artifact( - data={"name": "test_af", "string_required": "test_str", - "version": "0.0.1"}) - url = '/sample_artifact/%s' % art['id'] - self.patch(url=url, data=self.make_active) - self.publish_with_admin(art['id']) - self.set_user('user2') - self.delete(url=url, status=403) - - self.set_user('user1') - self.delete(url=url, status=403) - self.set_user('admin') - self.delete(url=url) - - # delete deactivated artifact - art = self.create_artifact( - data={"name": "test_af", "string_required": "test_str", - "version": "0.0.1"}) - url = '/sample_artifact/%s' % art['id'] - self.patch(url=url, data=self.make_active) - self.patch(url=url, data=self.make_deactivated) - self.delete(url=url, status=204) - self.get(url=url, status=404) - self.assertEqual(0, len(self.get( - url='/sample_artifact')['sample_artifact'])) - - def test_artifact_deactivate(self): - # test artifact deactivate for non-active artifact - private_art = self.create_artifact( - data={"name": "test_af", "string_required": "test_str", - "version": "0.0.1"}) - url = '/sample_artifact/%s' % private_art['id'] - self.deactivate_with_admin(private_art['id'], 400) - self.patch(url, self.make_active) - self.set_user('admin') - # test cannot deactivate if there is something else in request - incorrect = self.make_deactivated + [{"op": "replace", - "path": "/name", - "value": "test"}] - self.patch(url, incorrect, 400) - self.set_user('user1') - # test artifact deactivate success - deactive_art = self.deactivate_with_admin(private_art['id']) - self.assertEqual("deactivated", deactive_art["status"]) - # test deactivate is idempotent - self.patch(url, self.make_deactivated) - - def test_artifact_reactivate(self): - self.set_user('admin') - private_art = self.create_artifact( - data={"name": "test_af", "string_required": "test_str", - "version": "0.0.1"}) - url = '/sample_artifact/%s' % private_art['id'] - self.patch(url, self.make_active) - self.deactivate_with_admin(private_art['id']) - # test cannot reactivate if there is something else in request - incorrect = self.make_active + [{"op": "replace", - "path": "/name", - "value": "test"}] - self.patch(url, incorrect, 400) - # test artifact reactivate success - deactive_art = self.patch(url, self.make_active) - self.assertEqual("active", deactive_art["status"]) - - def test_upload_blob(self): - # create artifact with blob - data = 'data' - self.create_artifact( - data={'name': 'test_af', 'blob': data, - 'version': '0.0.1'}, status=400) - art = self.create_artifact(data={'name': 'test_af', - 'version': '0.0.1', - 'string_required': 'test'}) - url = '/sample_artifact/%s' % art['id'] - headers = {'Content-Type': 'application/octet-stream'} - - # upload to non-existing property - self.put(url=url + '/blob_non_exist', data=data, status=400, - headers=headers) - - # upload too big value - big_data = "this is the smallest big data" - self.put(url=url + '/small_blob', data=big_data, status=413, - headers=headers) - # upload correct blob value - self.put(url=url + '/small_blob', data=big_data[:2], headers=headers) - - # Upload artifact via different user - self.set_user('user2') - self.put(url=url + '/blob', data=data, status=404, - headers=headers) - - # Upload file to the artifact - self.set_user('user1') - art = self.put(url=url + '/blob', data=data, status=200, - headers=headers) - self.assertEqual('active', art['blob']['status']) - self.assertEqual('application/octet-stream', - art['blob']['content_type']) - self.assertIn('url', art['blob']) - self.assertNotIn('id', art['blob']) - - # reUpload file to artifact - self.put(url=url + '/blob', data=data, status=409, - headers=headers) - # upload blob dict - self.put(url + '/dict_of_blobs/test_key', data=data, headers=headers) - # test re-upload failed - self.put(url + '/dict_of_blobs/test_key', data=data, headers=headers, - status=409) - - # upload few other blobs to the dict - for elem in ('aaa', 'bbb', 'ccc', 'ddd'): - self.put(url + '/dict_of_blobs/' + elem, data=data, - headers=headers) - - # upload to active artifact - self.patch(url, self.make_active) - self.put(url + '/dict_of_blobs/key2', data=data, status=403, - headers=headers) - - self.delete(url) - - def test_download_blob(self): - data = 'data' - art = self.create_artifact(data={'name': 'test_af', - 'version': '0.0.1'}) - url = '/sample_artifact/%s' % art['id'] - - # download not uploaded blob - self.get(url=url + '/blob', status=400) - - # download blob from not existing artifact - self.get(url=url + '1/blob', status=404) - - # download blob from undefined property - self.get(url=url + '/not_a_blob', status=400) - - headers = {'Content-Type': 'application/octet-stream'} - art = self.put(url=url + '/blob', data=data, status=200, - headers=headers) - self.assertEqual('active', art['blob']['status']) - - blob_data = self.get(url=url + '/blob') - self.assertEqual(data, blob_data) - - # download artifact via admin - self.set_user('admin') - blob_data = self.get(url=url + '/blob') - self.assertEqual(data, blob_data) - - # try to download blob via different user - self.set_user('user2') - self.get(url=url + '/blob', status=404) - - def test_artifact_validators(self): + def test_update_with_validators(self): data = {'name': 'test_af', 'version': '0.0.1', 'list_validators': ['a', 'b', 'c'], @@ -1589,7 +1537,7 @@ class TestArtifact(functional.FunctionalTest): self.assertEqual(af['dict_validators'], {'abc': 'l', 'def': 'x', 'ghi': 'z'}) - def test_artifact_field_updates(self): + def test_update_base_fields(self): data = {'name': 'test_af', 'version': '0.0.1'} art = self.create_artifact(data=data) @@ -1876,647 +1824,7 @@ class TestArtifact(functional.FunctionalTest): "value": 'aaa'}] self.patch(url=url, data=patch, status=400) - def test_schemas(self): - schema_sample_artifact = { - u'sample_artifact': { - u'name': u'sample_artifact', - u'properties': {u'activated_at': { - u'description': u'Datetime when artifact has became ' - u'active.', - u'filter_ops': [u'eq', - u'neq', - u'in', - u'gt', - u'gte', - u'lt', - u'lte'], - u'format': u'date-time', - u'readOnly': True, - u'required_on_activate': False, - u'sortable': True, - u'type': [ - u'string', - u'null']}, - u'blob': {u'additionalProperties': False, - u'description': u'I am Blob', - u'filter_ops': [], - u'mutable': True, - u'properties': {u'checksum': { - u'type': [u'string', - u'null']}, - u'content_type': { - u'type': u'string'}, - u'external': { - u'type': u'boolean'}, - u'size': {u'type': [ - u'number', - u'null']}, - u'status': { - u'enum': [ - u'saving', - u'active', - u'pending_delete'], - u'type': u'string'}}, - u'required': [u'size', - u'checksum', - u'external', - u'status', - u'content_type'], - u'required_on_activate': False, - u'type': [u'object', - u'null']}, - u'bool1': {u'default': False, - u'filter_ops': [u'eq'], - u'required_on_activate': False, - u'type': [u'string', - u'null']}, - u'bool2': {u'default': False, - u'filter_ops': [u'eq'], - u'required_on_activate': False, - u'type': [u'string', - u'null']}, - u'created_at': { - u'description': u'Datetime when artifact has been ' - u'created.', - u'filter_ops': [u'eq', - u'neq', - u'in', - u'gt', - u'gte', - u'lt', - u'lte'], - u'format': u'date-time', - u'readOnly': True, - u'sortable': True, - u'type': u'string'}, - u'dependency1': {u'filter_ops': [u'eq', - u'neq', - u'in'], - u'required_on_activate': False, - u'type': [u'string', - u'null']}, - u'dependency2': {u'filter_ops': [u'eq', - u'neq', - u'in'], - u'required_on_activate': False, - u'type': [u'string', - u'null']}, - u'description': {u'default': u'', - u'description': u'Artifact description.', - u'filter_ops': [u'eq', - u'neq', - u'in'], - u'maxLength': 4096, - u'mutable': True, - u'required_on_activate': False, - u'type': [u'string', - u'null']}, - u'dict_of_blobs': { - u'additionalProperties': { - u'additionalProperties': False, - u'properties': {u'checksum': { - u'type': [u'string', - u'null']}, - u'content_type': { - u'type': u'string'}, - u'external': { - u'type': u'boolean'}, - u'size': { - u'type': [ - u'number', - u'null']}, - u'status': { - u'enum': [ - u'saving', - u'active', - u'pending_delete'], - u'type': u'string'}}, - u'required': [u'size', - u'checksum', - u'external', - u'status', - u'content_type'], - u'type': [u'object', - u'null']}, - u'default': {}, - u'filter_ops': [], - u'maxProperties': 255, - u'required_on_activate': False, - u'type': [u'object', - u'null']}, - u'dict_of_int': { - u'additionalProperties': { - u'type': u'string'}, - u'default': {}, - u'filter_ops': [u'eq'], - u'maxProperties': 255, - u'required_on_activate': False, - u'type': [u'object', - u'null']}, - u'dict_of_str': { - u'additionalProperties': { - u'type': u'string'}, - u'default': {}, - u'filter_ops': [u'eq'], - u'maxProperties': 255, - u'required_on_activate': False, - u'type': [u'object', - u'null']}, - u'dict_validators': { - u'additionalProperties': False, - u'filter_ops': [u'eq', - u'neq', - u'in'], - u'maxProperties': 3, - u'properties': { - u'abc': {u'type': [u'string', - u'null']}, - u'def': {u'type': [u'string', - u'null']}, - u'ghi': {u'type': [u'string', - u'null']}, - u'jkl': {u'type': [u'string', - u'null']}}, - u'required_on_activate': False, - u'type': [u'object', - u'null']}, - u'float1': {u'filter_ops': [u'eq', - u'neq', - u'in', - u'gt', - u'gte', - u'lt', - u'lte'], - u'required_on_activate': False, - u'sortable': True, - u'type': [u'number', - u'null']}, - u'float2': {u'filter_ops': [u'eq', - u'neq', - u'in', - u'gt', - u'gte', - u'lt', - u'lte'], - u'required_on_activate': False, - u'sortable': True, - u'type': [u'number', - u'null']}, - u'icon': {u'additionalProperties': False, - u'description': u'Artifact icon.', - u'filter_ops': [], - u'properties': {u'checksum': { - u'type': [u'string', - u'null']}, - u'content_type': { - u'type': u'string'}, - u'external': { - u'type': u'boolean'}, - u'size': { - u'type': [ - u'number', - u'null']}, - u'status': { - u'enum': [ - u'saving', - u'active', - u'pending_delete'], - u'type': u'string'}}, - u'required': [u'size', - u'checksum', - u'external', - u'status', - u'content_type'], - u'required_on_activate': False, - u'type': [u'object', - u'null']}, - u'id': {u'description': u'Artifact UUID.', - u'filter_ops': [u'eq', - u'neq', - u'in'], - u'maxLength': 255, - u'pattern': u'^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-' - u'([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-' - u'([0-9a-fA-F]){12}$', - u'readOnly': True, - u'sortable': True, - u'type': u'string'}, - u'int1': {u'filter_ops': [u'eq', - u'neq', - u'in', - u'gt', - u'gte', - u'lt', - u'lte'], - u'required_on_activate': False, - u'sortable': True, - u'type': [u'integer', - u'null']}, - u'int2': {u'filter_ops': [u'eq', - u'neq', - u'in', - u'gt', - u'gte', - u'lt', - u'lte'], - u'required_on_activate': False, - u'sortable': True, - u'type': [u'integer', - u'null']}, - u'int_validators': {u'filter_ops': [u'eq', - u'neq', - u'in', - u'gt', - u'gte', - u'lt', - u'lte'], - u'maximum': 20, - u'minumum': 10, - u'required_on_activate': False, - u'type': [u'integer', - u'null']}, - u'license': { - u'description': u'Artifact license type.', - u'filter_ops': [u'eq', - u'neq', - u'in'], - u'maxLength': 255, - u'required_on_activate': False, - u'type': [u'string', - u'null']}, - u'license_url': { - u'description': u'URL to artifact license.', - u'filter_ops': [u'eq', - u'neq', - u'in'], - u'maxLength': 255, - u'required_on_activate': False, - u'type': [u'string', - u'null']}, - u'list_of_int': {u'default': [], - u'filter_ops': [u'eq'], - u'items': { - u'type': u'string'}, - u'maxItems': 255, - u'required_on_activate': False, - u'type': [u'array', - u'null']}, - u'list_of_str': {u'default': [], - u'filter_ops': [u'eq'], - u'items': { - u'type': u'string'}, - u'maxItems': 255, - u'required_on_activate': False, - u'type': [u'array', - u'null']}, - u'list_validators': {u'default': [], - u'filter_ops': [ - u'eq', - u'neq', - u'in'], - u'items': { - u'type': u'string'}, - u'maxItems': 3, - u'required_on_activate': False, - u'type': [u'array', - u'null'], - u'unique': True}, - u'metadata': {u'additionalProperties': { - u'type': u'string'}, - u'default': {}, - u'description': u'Key-value dict with useful ' - u'information about an artifact.', - u'filter_ops': [u'eq', - u'neq'], - u'maxProperties': 255, - u'required_on_activate': False, - u'type': [u'object', - u'null']}, - u'name': {u'description': u'Artifact Name.', - u'filter_ops': [u'eq', - u'neq', - u'in'], - u'maxLength': 255, - u'required_on_activate': False, - u'sortable': True, - u'type': u'string'}, - u'owner': { - u'description': u'ID of user/tenant who uploaded ' - u'artifact.', - u'filter_ops': [u'eq', - u'neq', - u'in'], - u'maxLength': 255, - u'readOnly': True, - u'required_on_activate': False, - u'sortable': True, - u'type': u'string'}, - u'provided_by': { - u'additionalProperties': False, - u'description': u'Info about artifact authors.', - u'filter_ops': [u'eq', - u'neq', - u'in'], - u'maxProperties': 255, - u'properties': { - u'company': {u'type': u'string'}, - u'href': {u'type': u'string'}, - u'name': {u'type': u'string'}}, - u'required_on_activate': False, - u'type': [u'object', - u'null']}, - u'release': { - u'default': [], - u'description': u'Target Openstack release for ' - u'artifact. It is usually the same ' - u'when artifact was uploaded.', - u'filter_ops': [u'eq', - u'neq', - u'in'], - u'items': {u'type': u'string'}, - u'maxItems': 255, - u'required_on_activate': False, - u'type': [u'array', - u'null'], - u'unique': True}, - u'small_blob': {u'additionalProperties': False, - u'filter_ops': [], - u'mutable': True, - u'properties': {u'checksum': { - u'type': [u'string', - u'null']}, - u'content_type': { - u'type': u'string'}, - u'external': { - u'type': u'boolean'}, - u'size': { - u'type': [ - u'number', - u'null']}, - u'status': { - u'enum': [ - u'saving', - u'active', - u'pending_delete'], - u'type': u'string'}}, - u'required': [u'size', - u'checksum', - u'external', - u'status', - u'content_type'], - u'required_on_activate': False, - u'type': [u'object', - u'null']}, - u'status': {u'default': u'drafted', - u'description': u'Artifact status.', - u'enum': [u'drafted', - u'active', - u'deactivated', - u'deleted'], - u'filter_ops': [u'eq', - u'neq', - u'in'], - u'sortable': True, - u'type': u'string'}, - u'str1': {u'filter_ops': [u'eq', - u'neq', - u'in', - u'gt', - u'gte', - u'lt', - u'lte'], - u'maxLength': 255, - u'required_on_activate': False, - u'sortable': True, - u'type': [u'string', - u'null']}, - u'string_mutable': {u'filter_ops': [u'eq', - u'neq', - u'in', - u'gt', - u'gte', - u'lt', - u'lte'], - u'maxLength': 255, - u'mutable': True, - u'required_on_activate': False, - u'type': [u'string', - u'null']}, - u'string_required': { - u'filter_ops': [u'eq', - u'neq', - u'in', - u'gt', - u'gte', - u'lt', - u'lte'], - u'maxLength': 255, - u'type': [u'string', - u'null']}, - u'string_validators': { - u'enum': [u'aa', - u'bb', - u'ccccccccccc', - None], - u'filter_ops': [u'eq', - u'neq', - u'in', - u'gt', - u'gte', - u'lt', - u'lte'], - u'maxLength': 10, - u'required_on_activate': False, - u'type': [u'string', - u'null']}, - u'supported_by': { - u'additionalProperties': { - u'type': u'string'}, - u'description': u'Info about persons who responsible ' - u'for artifact support', - u'filter_ops': [u'eq', - u'neq', - u'in'], - u'maxProperties': 255, - u'required': [u'name'], - u'required_on_activate': False, - u'type': [u'object', - u'null']}, - u'system_attribute': {u'default': u'default', - u'filter_ops': [u'eq', - u'neq', - u'in'], - u'maxLength': 255, - u'readOnly': True, - u'sortable': True, - u'type': [u'string', - u'null']}, - u'tags': {u'default': [], - u'description': u'List of tags added to ' - u'Artifact.', - u'filter_ops': [u'eq', - u'neq', - u'in'], - u'items': {u'type': u'string'}, - u'maxItems': 255, - u'mutable': True, - u'required_on_activate': False, - u'type': [u'array', - u'null']}, - u'updated_at': { - u'description': u'Datetime when artifact has been ' - u'updated last time.', - u'filter_ops': [u'eq', - u'neq', - u'in', - u'gt', - u'gte', - u'lt', - u'lte'], - u'format': u'date-time', - u'readOnly': True, - u'sortable': True, - u'type': u'string'}, - u'version': {u'default': u'0.0.0', - u'description': u'Artifact version(semver).', - u'filter_ops': [u'eq', - u'neq', - u'in', - u'gt', - u'gte', - u'lt', - u'lte'], - u'pattern': u'/^([0-9]+)\\.([0-9]+)\\.' - u'([0-9]+)(?:-([0-9A-Za-z-]+' - u'(?:\\.[0-9A-Za-z-]+)*))?' - u'(?:\\+[0-9A-Za-z-]+)?$/', - u'required_on_activate': False, - u'sortable': True, - u'type': u'string'}, - u'visibility': { - u'default': u'private', - u'description': u'Artifact visibility that defines if ' - u'artifact can be available to other ' - u'users.', - u'filter_ops': [u'eq'], - u'maxLength': 255, - u'sortable': True, - u'type': u'string'}}, - u'required': [u'name'], - u'title': u'Artifact type sample_artifact of version 1.0', - u'type': u'object'}} - - # Get list schemas of artifacts - result = self.get(url='/schemas') - self.assertEqual({u'schemas': schema_sample_artifact}, result) - - # Get schema of sample_artifact - result = self.get(url='/schemas/sample_artifact') - self.assertEqual({u'schemas': schema_sample_artifact}, result) - - # Validation of schemas - result = self.get(url='/schemas')['schemas'] - for artifact_type, schema in result.items(): - jsonschema.Draft4Validator.check_schema(schema) - - def test_artifact_sorted(self): - art_list = [self.create_artifact({'name': 'name%s' % i, - 'version': '1.0', - 'tags': ['tag%s' % i], - 'int1': i, - 'float1': 123.456 + (-0.9) ** i, - 'str1': 'bugaga', - 'bool1': True, - 'list_of_int': [11, 22, - i], - 'dict_of_int': {'one': 4 * i, - 'two': (-2) ** i}}) - for i in range(5)] - - # sorted by string 'asc' - url = '/sample_artifact?sort=name:asc' - result = self.get(url=url) - expected = sort_results(art_list) - self.assertEqual(expected, result['sample_artifact']) - - # sorted by string 'desc' - url = '/sample_artifact?sort=name:desc' - result = self.get(url=url) - expected = sort_results(art_list) - expected.reverse() - self.assertEqual(expected, result['sample_artifact']) - - # sorted by int 'asc' - url = '/sample_artifact?sort=int1:asc' - result = self.get(url=url) - expected = sort_results(art_list, target='int1') - self.assertEqual(expected, result['sample_artifact']) - - # sorted by int 'desc' - url = '/sample_artifact?sort=int1:desc' - result = self.get(url=url) - expected = sort_results(art_list, target='int1') - expected.reverse() - self.assertEqual(expected, result['sample_artifact']) - - # sorted by float 'asc' - url = '/sample_artifact?sort=float1:asc' - result = self.get(url=url) - expected = sort_results(art_list, target='float1') - self.assertEqual(expected, result['sample_artifact']) - - # sorted by float 'desc' - url = '/sample_artifact?sort=float1:desc' - result = self.get(url=url) - expected = sort_results(art_list, target='float1') - expected.reverse() - self.assertEqual(expected, result['sample_artifact']) - - # sorted by unsorted 'asc' - url = '/sample_artifact?sort=bool1:asc' - self.get(url=url, status=400) - - # sorted by unsorted 'desc' - url = '/sample_artifact?sort=bool1:desc' - self.get(url=url, status=400) - - # sorted by non-existent 'asc' - url = '/sample_artifact?sort=non_existent:asc' - self.get(url=url, status=400) - - # sorted by non-existent 'desc' - url = '/sample_artifact?sort=non_existent:desc' - self.get(url=url, status=400) - - # sorted by invalid op - url = '/sample_artifact?sort=name:invalid_op' - self.get(url=url, status=400) - - # sorted without op - url = '/sample_artifact?sort=name' - result = self.get(url=url) - expected = sort_results(art_list) - expected.reverse() - self.assertEqual(expected, result['sample_artifact']) - - # sorted by list - url = '/sample_artifact?sort=list_of_int:asc' - self.get(url=url, status=400) - - # sorted by dict - url = '/sample_artifact?sort=dict_of_int:asc' - self.get(url=url, status=400) - - # sorted by element of dict - url = '/sample_artifact?sort=dict_of_int.one:asc' - self.get(url=url, status=400) - - # sorted by any prop - url = '/sample_artifact?sort=name:asc,int1:desc' - result = self.get(url=url) - expected = sort_results(sort_results(art_list), target='int1') - self.assertEqual(expected, result['sample_artifact']) - - def test_artifact_field_dict(self): + def test_update_field_dict(self): art1 = self.create_artifact(data={"name": "art1"}) # create artifact without dict prop @@ -2646,7 +1954,7 @@ class TestArtifact(functional.FunctionalTest): url = '/sample_artifact/%s' % art1['id'] self.patch(url=url, data=data, status=400) - def test_artifact_field_list(self): + def test_update_field_list(self): art1 = self.create_artifact(data={"name": "art1"}) # create artifact without list prop @@ -2776,59 +2084,24 @@ class TestArtifact(functional.FunctionalTest): url = '/sample_artifact/%s' % art1['id'] self.patch(url=url, data=data, status=400) - def test_support_unicode(self): - unicode_text = u'\u041f\u0420\u0418\u0412\u0415\u0422' - art1 = self.create_artifact(data={'name': unicode_text}) - self.assertEqual(unicode_text, art1['name']) - mixed_text = u'la\u041f' - art2 = self.create_artifact(data={'name': mixed_text}) - self.assertEqual(mixed_text, art2['name']) +class TestDependencies(TestArtifact): + def test_manage_dependencies(self): + some_af = self.create_artifact(data={"name": "test_af"}) + dep_af = self.create_artifact(data={"name": "test_dep_af"}) + dep_url = "/artifacts/sample_artifact/%s" % some_af['id'] - headers = {'Content-Type': 'text/html; charset=UTF-8'} - url = u'/sample_artifact?name=\u041f\u0420\u0418\u0412\u0415\u0422' - response_url = u'/artifacts/sample_artifact?name=' \ - u'%D0%9F%D0%A0%D0%98%D0%92%D0%95%D0%A2' - result = self.get(url=url, headers=headers) - self.assertEqual(art1, result['sample_artifact'][0]) - self.assertEqual(response_url, result['first']) + # set valid dependency + patch = [{"op": "replace", "path": "/dependency1", "value": dep_url}] + url = '/sample_artifact/%s' % dep_af['id'] + af = self.patch(url=url, data=patch) + self.assertEqual(af['dependency1'], dep_url) - def test_latest_filter(self): - # Create artifacts with versions - group1_versions = ['1.0', '20.0', '2.0.0', '2.0.1-beta', '2.0.1'] - group2_versions = ['1', '1000.0.1-beta', '99.0', - '1000.0.1-alpha', '1000.0.1'] + # remove dependency from artifact + patch = [{"op": "replace", "path": "/dependency1", "value": None}] + af = self.patch(url=url, data=patch) + self.assertIsNone(af['dependency1']) - for i in range(5): - self.create_artifact( - {'name': 'group1', - 'version': group1_versions[i], - 'tags': ['tag%s' % i], - 'int1': 2048, - 'float1': 123.456, - 'str1': 'bugaga', - 'bool1': True}) - self.create_artifact( - {'name': 'group2', - 'version': group2_versions[i], - 'tags': ['tag%s' % i], - 'int1': 2048, - 'float1': 123.456, - 'str1': 'bugaga', - 'bool1': True}) - - url = '/sample_artifact?version=latest&sort=name:asc' - res = self.get(url=url, status=200)['sample_artifact'] - self.assertEqual(2, len(res)) - self.assertEqual('20.0.0', res[0]['version']) - self.assertEqual('1000.0.1', res[1]['version']) - - url = '/sample_artifact?version=latest&name=group1' - res = self.get(url=url, status=200)['sample_artifact'] - self.assertEqual(1, len(res)) - self.assertEqual('20.0.0', res[0]['version']) - - url = '/sample_artifact?version=latest&name=group2' - res = self.get(url=url, status=200)['sample_artifact'] - self.assertEqual(1, len(res)) - self.assertEqual('1000.0.1', res[0]['version']) + # try to set invalid dependency + patch = [{"op": "replace", "path": "/dependency1", "value": "Invalid"}] + self.patch(url=url, data=patch, status=400) diff --git a/glare/tests/functional/test_schemas.py b/glare/tests/functional/test_schemas.py new file mode 100644 index 0000000..d83fead --- /dev/null +++ b/glare/tests/functional/test_schemas.py @@ -0,0 +1,599 @@ +# Copyright 2016 OpenStack Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import jsonschema + +from oslo_serialization import jsonutils +import requests + +from glare.tests import functional + + +class TestArtifact(functional.FunctionalTest): + + def setUp(self): + super(TestArtifact, self).setUp() + self.glare_server.deployment_flavor = 'noauth' + self.glare_server.enabled_artifact_types = 'sample_artifact' + self.glare_server.custom_artifact_types_modules = ( + 'glare.tests.functional.sample_artifact') + self.start_servers(**self.__dict__.copy()) + + def tearDown(self): + self.stop_servers() + self._reset_database(self.glare_server.sql_connection) + super(TestArtifact, self).tearDown() + + def _url(self, path): + return 'http://127.0.0.1:%d%s' % (self.glare_port, path) + + def _check_artifact_method(self, url, status=200): + headers = { + 'X-Identity-Status': 'Confirmed', + } + response = requests.get(self._url(url), headers=headers) + self.assertEqual(status, response.status_code, response.text) + if status >= 400: + return response.text + if ("application/json" in response.headers["content-type"] or + "application/schema+json" in response.headers["content-type"]): + return jsonutils.loads(response.text) + return response.text + + def get(self, url, status=200, headers=None): + return self._check_artifact_method(url, status=status) + + def test_schemas(self): + schema_sample_artifact = { + u'sample_artifact': { + u'name': u'sample_artifact', + u'properties': {u'activated_at': { + u'description': u'Datetime when artifact has became ' + u'active.', + u'filter_ops': [u'eq', + u'neq', + u'in', + u'gt', + u'gte', + u'lt', + u'lte'], + u'format': u'date-time', + u'readOnly': True, + u'required_on_activate': False, + u'sortable': True, + u'type': [ + u'string', + u'null']}, + u'blob': {u'additionalProperties': False, + u'description': u'I am Blob', + u'filter_ops': [], + u'mutable': True, + u'properties': {u'checksum': { + u'type': [u'string', + u'null']}, + u'content_type': { + u'type': u'string'}, + u'external': { + u'type': u'boolean'}, + u'size': {u'type': [ + u'number', + u'null']}, + u'status': { + u'enum': [ + u'saving', + u'active', + u'pending_delete'], + u'type': u'string'}}, + u'required': [u'size', + u'checksum', + u'external', + u'status', + u'content_type'], + u'required_on_activate': False, + u'type': [u'object', + u'null']}, + u'bool1': {u'default': False, + u'filter_ops': [u'eq'], + u'required_on_activate': False, + u'type': [u'string', + u'null']}, + u'bool2': {u'default': False, + u'filter_ops': [u'eq'], + u'required_on_activate': False, + u'type': [u'string', + u'null']}, + u'created_at': { + u'description': u'Datetime when artifact has been ' + u'created.', + u'filter_ops': [u'eq', + u'neq', + u'in', + u'gt', + u'gte', + u'lt', + u'lte'], + u'format': u'date-time', + u'readOnly': True, + u'sortable': True, + u'type': u'string'}, + u'dependency1': {u'filter_ops': [u'eq', + u'neq', + u'in'], + u'required_on_activate': False, + u'type': [u'string', + u'null']}, + u'dependency2': {u'filter_ops': [u'eq', + u'neq', + u'in'], + u'required_on_activate': False, + u'type': [u'string', + u'null']}, + u'description': {u'default': u'', + u'description': u'Artifact description.', + u'filter_ops': [u'eq', + u'neq', + u'in'], + u'maxLength': 4096, + u'mutable': True, + u'required_on_activate': False, + u'type': [u'string', + u'null']}, + u'dict_of_blobs': { + u'additionalProperties': { + u'additionalProperties': False, + u'properties': {u'checksum': { + u'type': [u'string', + u'null']}, + u'content_type': { + u'type': u'string'}, + u'external': { + u'type': u'boolean'}, + u'size': { + u'type': [ + u'number', + u'null']}, + u'status': { + u'enum': [ + u'saving', + u'active', + u'pending_delete'], + u'type': u'string'}}, + u'required': [u'size', + u'checksum', + u'external', + u'status', + u'content_type'], + u'type': [u'object', + u'null']}, + u'default': {}, + u'filter_ops': [], + u'maxProperties': 255, + u'required_on_activate': False, + u'type': [u'object', + u'null']}, + u'dict_of_int': { + u'additionalProperties': { + u'type': u'string'}, + u'default': {}, + u'filter_ops': [u'eq'], + u'maxProperties': 255, + u'required_on_activate': False, + u'type': [u'object', + u'null']}, + u'dict_of_str': { + u'additionalProperties': { + u'type': u'string'}, + u'default': {}, + u'filter_ops': [u'eq'], + u'maxProperties': 255, + u'required_on_activate': False, + u'type': [u'object', + u'null']}, + u'dict_validators': { + u'additionalProperties': False, + u'filter_ops': [u'eq', + u'neq', + u'in'], + u'maxProperties': 3, + u'properties': { + u'abc': {u'type': [u'string', + u'null']}, + u'def': {u'type': [u'string', + u'null']}, + u'ghi': {u'type': [u'string', + u'null']}, + u'jkl': {u'type': [u'string', + u'null']}}, + u'required_on_activate': False, + u'type': [u'object', + u'null']}, + u'float1': {u'filter_ops': [u'eq', + u'neq', + u'in', + u'gt', + u'gte', + u'lt', + u'lte'], + u'required_on_activate': False, + u'sortable': True, + u'type': [u'number', + u'null']}, + u'float2': {u'filter_ops': [u'eq', + u'neq', + u'in', + u'gt', + u'gte', + u'lt', + u'lte'], + u'required_on_activate': False, + u'sortable': True, + u'type': [u'number', + u'null']}, + u'icon': {u'additionalProperties': False, + u'description': u'Artifact icon.', + u'filter_ops': [], + u'properties': {u'checksum': { + u'type': [u'string', + u'null']}, + u'content_type': { + u'type': u'string'}, + u'external': { + u'type': u'boolean'}, + u'size': { + u'type': [ + u'number', + u'null']}, + u'status': { + u'enum': [ + u'saving', + u'active', + u'pending_delete'], + u'type': u'string'}}, + u'required': [u'size', + u'checksum', + u'external', + u'status', + u'content_type'], + u'required_on_activate': False, + u'type': [u'object', + u'null']}, + u'id': {u'description': u'Artifact UUID.', + u'filter_ops': [u'eq', + u'neq', + u'in'], + u'maxLength': 255, + u'pattern': u'^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-' + u'([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-' + u'([0-9a-fA-F]){12}$', + u'readOnly': True, + u'sortable': True, + u'type': u'string'}, + u'int1': {u'filter_ops': [u'eq', + u'neq', + u'in', + u'gt', + u'gte', + u'lt', + u'lte'], + u'required_on_activate': False, + u'sortable': True, + u'type': [u'integer', + u'null']}, + u'int2': {u'filter_ops': [u'eq', + u'neq', + u'in', + u'gt', + u'gte', + u'lt', + u'lte'], + u'required_on_activate': False, + u'sortable': True, + u'type': [u'integer', + u'null']}, + u'int_validators': {u'filter_ops': [u'eq', + u'neq', + u'in', + u'gt', + u'gte', + u'lt', + u'lte'], + u'maximum': 20, + u'minumum': 10, + u'required_on_activate': False, + u'type': [u'integer', + u'null']}, + u'license': { + u'description': u'Artifact license type.', + u'filter_ops': [u'eq', + u'neq', + u'in'], + u'maxLength': 255, + u'required_on_activate': False, + u'type': [u'string', + u'null']}, + u'license_url': { + u'description': u'URL to artifact license.', + u'filter_ops': [u'eq', + u'neq', + u'in'], + u'maxLength': 255, + u'required_on_activate': False, + u'type': [u'string', + u'null']}, + u'list_of_int': {u'default': [], + u'filter_ops': [u'eq'], + u'items': { + u'type': u'string'}, + u'maxItems': 255, + u'required_on_activate': False, + u'type': [u'array', + u'null']}, + u'list_of_str': {u'default': [], + u'filter_ops': [u'eq'], + u'items': { + u'type': u'string'}, + u'maxItems': 255, + u'required_on_activate': False, + u'type': [u'array', + u'null']}, + u'list_validators': {u'default': [], + u'filter_ops': [ + u'eq', + u'neq', + u'in'], + u'items': { + u'type': u'string'}, + u'maxItems': 3, + u'required_on_activate': False, + u'type': [u'array', + u'null'], + u'unique': True}, + u'metadata': {u'additionalProperties': { + u'type': u'string'}, + u'default': {}, + u'description': u'Key-value dict with useful ' + u'information about an artifact.', + u'filter_ops': [u'eq', + u'neq'], + u'maxProperties': 255, + u'required_on_activate': False, + u'type': [u'object', + u'null']}, + u'name': {u'description': u'Artifact Name.', + u'filter_ops': [u'eq', + u'neq', + u'in'], + u'maxLength': 255, + u'required_on_activate': False, + u'sortable': True, + u'type': u'string'}, + u'owner': { + u'description': u'ID of user/tenant who uploaded ' + u'artifact.', + u'filter_ops': [u'eq', + u'neq', + u'in'], + u'maxLength': 255, + u'readOnly': True, + u'required_on_activate': False, + u'sortable': True, + u'type': u'string'}, + u'provided_by': { + u'additionalProperties': False, + u'description': u'Info about artifact authors.', + u'filter_ops': [u'eq', + u'neq', + u'in'], + u'maxProperties': 255, + u'properties': { + u'company': {u'type': u'string'}, + u'href': {u'type': u'string'}, + u'name': {u'type': u'string'}}, + u'required_on_activate': False, + u'type': [u'object', + u'null']}, + u'release': { + u'default': [], + u'description': u'Target Openstack release for ' + u'artifact. It is usually the same ' + u'when artifact was uploaded.', + u'filter_ops': [u'eq', + u'neq', + u'in'], + u'items': {u'type': u'string'}, + u'maxItems': 255, + u'required_on_activate': False, + u'type': [u'array', + u'null'], + u'unique': True}, + u'small_blob': {u'additionalProperties': False, + u'filter_ops': [], + u'mutable': True, + u'properties': {u'checksum': { + u'type': [u'string', + u'null']}, + u'content_type': { + u'type': u'string'}, + u'external': { + u'type': u'boolean'}, + u'size': { + u'type': [ + u'number', + u'null']}, + u'status': { + u'enum': [ + u'saving', + u'active', + u'pending_delete'], + u'type': u'string'}}, + u'required': [u'size', + u'checksum', + u'external', + u'status', + u'content_type'], + u'required_on_activate': False, + u'type': [u'object', + u'null']}, + u'status': {u'default': u'drafted', + u'description': u'Artifact status.', + u'enum': [u'drafted', + u'active', + u'deactivated', + u'deleted'], + u'filter_ops': [u'eq', + u'neq', + u'in'], + u'sortable': True, + u'type': u'string'}, + u'str1': {u'filter_ops': [u'eq', + u'neq', + u'in', + u'gt', + u'gte', + u'lt', + u'lte'], + u'maxLength': 255, + u'required_on_activate': False, + u'sortable': True, + u'type': [u'string', + u'null']}, + u'string_mutable': {u'filter_ops': [u'eq', + u'neq', + u'in', + u'gt', + u'gte', + u'lt', + u'lte'], + u'maxLength': 255, + u'mutable': True, + u'required_on_activate': False, + u'type': [u'string', + u'null']}, + u'string_required': { + u'filter_ops': [u'eq', + u'neq', + u'in', + u'gt', + u'gte', + u'lt', + u'lte'], + u'maxLength': 255, + u'type': [u'string', + u'null']}, + u'string_validators': { + u'enum': [u'aa', + u'bb', + u'ccccccccccc', + None], + u'filter_ops': [u'eq', + u'neq', + u'in', + u'gt', + u'gte', + u'lt', + u'lte'], + u'maxLength': 10, + u'required_on_activate': False, + u'type': [u'string', + u'null']}, + u'supported_by': { + u'additionalProperties': { + u'type': u'string'}, + u'description': u'Info about persons who responsible ' + u'for artifact support', + u'filter_ops': [u'eq', + u'neq', + u'in'], + u'maxProperties': 255, + u'required': [u'name'], + u'required_on_activate': False, + u'type': [u'object', + u'null']}, + u'system_attribute': {u'default': u'default', + u'filter_ops': [u'eq', + u'neq', + u'in'], + u'maxLength': 255, + u'readOnly': True, + u'sortable': True, + u'type': [u'string', + u'null']}, + u'tags': {u'default': [], + u'description': u'List of tags added to ' + u'Artifact.', + u'filter_ops': [u'eq', + u'neq', + u'in'], + u'items': {u'type': u'string'}, + u'maxItems': 255, + u'mutable': True, + u'required_on_activate': False, + u'type': [u'array', + u'null']}, + u'updated_at': { + u'description': u'Datetime when artifact has been ' + u'updated last time.', + u'filter_ops': [u'eq', + u'neq', + u'in', + u'gt', + u'gte', + u'lt', + u'lte'], + u'format': u'date-time', + u'readOnly': True, + u'sortable': True, + u'type': u'string'}, + u'version': {u'default': u'0.0.0', + u'description': u'Artifact version(semver).', + u'filter_ops': [u'eq', + u'neq', + u'in', + u'gt', + u'gte', + u'lt', + u'lte'], + u'pattern': u'/^([0-9]+)\\.([0-9]+)\\.' + u'([0-9]+)(?:-([0-9A-Za-z-]+' + u'(?:\\.[0-9A-Za-z-]+)*))?' + u'(?:\\+[0-9A-Za-z-]+)?$/', + u'required_on_activate': False, + u'sortable': True, + u'type': u'string'}, + u'visibility': { + u'default': u'private', + u'description': u'Artifact visibility that defines if ' + u'artifact can be available to other ' + u'users.', + u'filter_ops': [u'eq'], + u'maxLength': 255, + u'sortable': True, + u'type': u'string'}}, + u'required': [u'name'], + u'title': u'Artifact type sample_artifact of version 1.0', + u'type': u'object'}} + + # Get list schemas of artifacts + result = self.get(url='/schemas') + self.assertEqual({u'schemas': schema_sample_artifact}, result) + + # Get schema of sample_artifact + result = self.get(url='/schemas/sample_artifact') + self.assertEqual({u'schemas': schema_sample_artifact}, result) + + # Validation of schemas + result = self.get(url='/schemas')['schemas'] + for artifact_type, schema in result.items(): + jsonschema.Draft4Validator.check_schema(schema)