Implemented input data validation

Change-Id: I8407cf4cbb69e60b7def891625522f3ca3c822fe
Implements:  blueprint unify-input-data
This commit is contained in:
Bulat Gaifullin 2016-01-26 11:12:40 +03:00 committed by Uladzimir_Niakhai
parent d99d1228ef
commit 9e107cde9a
18 changed files with 683 additions and 45 deletions

View File

@ -19,6 +19,7 @@
from collections import defaultdict from collections import defaultdict
import logging import logging
import jsonschema
import six import six
from packetary.controllers import RepositoryController from packetary.controllers import RepositoryController
@ -28,7 +29,8 @@ from packetary.objects import PackageRelation
from packetary.objects import PackagesForest from packetary.objects import PackagesForest
from packetary.objects import PackagesTree from packetary.objects import PackagesTree
from packetary.objects.statistics import CopyStatistics from packetary.objects.statistics import CopyStatistics
from packetary.schemas import PACKAGE_FILES_SCHEMA
from packetary.schemas import PACKAGES_SCHEMA
logger = logging.getLogger(__package__) logger = logging.getLogger(__package__)
@ -122,6 +124,7 @@ class RepositoryApi(object):
:param package_files: The list of URLs of packages :param package_files: The list of URLs of packages
""" """
self._validate_repo_data(repo_data) self._validate_repo_data(repo_data)
self._validate_package_files(package_files)
return self.controller.create_repository(repo_data, package_files) return self.controller.create_repository(repo_data, package_files)
def get_packages(self, repos_data, requirements_data=None, def get_packages(self, repos_data, requirements_data=None,
@ -215,7 +218,6 @@ class RepositoryApi(object):
self._validate_requirements_data(requirements_data) self._validate_requirements_data(requirements_data)
result = [] result = []
for r in requirements_data: for r in requirements_data:
self._validate_requirements_data(r)
versions = r.get('versions', None) versions = r.get('versions', None)
if versions is None: if versions is None:
result.append(PackageRelation.from_args((r['name'],))) result.append(PackageRelation.from_args((r['name'],)))
@ -227,9 +229,34 @@ class RepositoryApi(object):
return result return result
def _validate_repo_data(self, repo_data): def _validate_repo_data(self, repo_data):
# TODO(bgaifullin) implement me schema = self.controller.get_repository_data_schema()
pass self._validate_data(repo_data, schema)
def _validate_requirements_data(self, requirements_data): def _validate_requirements_data(self, requirements_data):
# TODO(bgaifullin) implement me self._validate_data(requirements_data, PACKAGES_SCHEMA)
pass
def _validate_package_files(self, package_files):
self._validate_data(package_files, PACKAGE_FILES_SCHEMA)
def _validate_data(self, data, schema):
"""Validate the input data using jsonschema validation.
:param data: a data to validate represented as a dict
:param schema: a schema to validate represented as a dict;
must be in JSON Schema Draft 4 format.
"""
try:
jsonschema.validate(data, schema)
except jsonschema.ValidationError as e:
self._raise_validation_error("data", e.message, e.absolute_path)
except jsonschema.SchemaError as e:
self._raise_validation_error(
"schema", e.message, e.absolute_schema_path
)
@staticmethod
def _raise_validation_error(what, details, path):
message = "Invalid {0}: {1}.".format(what, details)
if path:
message = "\n".join((message, "Field: {0}".format(".".join(path))))
raise ValueError(message)

View File

@ -137,6 +137,13 @@ class RepositoryController(object):
self.assign_packages(repo, packages) self.assign_packages(repo, packages)
return repo return repo
def get_repository_data_schema(self):
"""Return jsonschema to validate data for required driver.
:return : Return a jsonschema represented as a dict
"""
return self.driver.repository_data_schema()
def _copy_packages(self, target, packages, observer): def _copy_packages(self, target, packages, observer):
with self.context.async_section() as section: with self.context.async_section() as section:
for package in packages: for package in packages:

View File

@ -111,3 +111,7 @@ class RepositoryDriverBase(object):
:return: the integer value that is relevant repository`s priority :return: the integer value that is relevant repository`s priority
less number means greater priority less number means greater priority
""" """
@abc.abstractmethod
def get_repository_data_schema(self):
"""Gets the json scheme for repository data validation."""

View File

@ -36,6 +36,7 @@ from packetary.objects import FileChecksum
from packetary.objects import Package from packetary.objects import Package
from packetary.objects import PackageRelation from packetary.objects import PackageRelation
from packetary.objects import Repository from packetary.objects import Repository
from packetary.schemas import DEB_REPO_SCHEMA
_OPERATORS_MAPPING = { _OPERATORS_MAPPING = {
@ -83,6 +84,9 @@ _checksum_collector = checksum_composite('md5', 'sha1', 'sha256')
class DebRepositoryDriver(RepositoryDriverBase): class DebRepositoryDriver(RepositoryDriverBase):
def get_repository_data_schema(self):
return DEB_REPO_SCHEMA
def priority_sort(self, repo_data): def priority_sort(self, repo_data):
# DEB repository expects general values from 0 to 1000. 0 # DEB repository expects general values from 0 to 1000. 0
# to have lowest priority and 1000 -- the highest. Note that a # to have lowest priority and 1000 -- the highest. Note that a
@ -94,7 +98,7 @@ class DebRepositoryDriver(RepositoryDriverBase):
return -priority return -priority
def get_repository(self, connection, repository_data, arch, consumer): def get_repository(self, connection, repository_data, arch, consumer):
url = utils.normalize_repository_url(repository_data['url']) url = utils.normalize_repository_url(repository_data['uri'])
suite = repository_data['suite'] suite = repository_data['suite']
components = repository_data.get('section') components = repository_data.get('section')
path = repository_data.get('path') path = repository_data.get('path')
@ -202,7 +206,7 @@ class DebRepositoryDriver(RepositoryDriverBase):
return new_repo return new_repo
def create_repository(self, repository_data, arch): def create_repository(self, repository_data, arch):
url = utils.normalize_repository_url(repository_data['url']) url = utils.normalize_repository_url(repository_data['uri'])
suite = repository_data['suite'] suite = repository_data['suite']
component = repository_data.get('section') component = repository_data.get('section')
path = repository_data.get('path') path = repository_data.get('path')

View File

@ -36,6 +36,7 @@ from packetary.objects import PackageRelation
from packetary.objects import PackageVersion from packetary.objects import PackageVersion
from packetary.objects import Repository from packetary.objects import Repository
from packetary.objects import VersionRange from packetary.objects import VersionRange
from packetary.schemas import RPM_REPO_SCHEMA
urljoin = six.moves.urllib.parse.urljoin urljoin = six.moves.urllib.parse.urljoin
@ -86,6 +87,9 @@ class CreaterepoCallBack(object):
class RpmRepositoryDriver(RepositoryDriverBase): class RpmRepositoryDriver(RepositoryDriverBase):
def get_repository_data_schema(self):
return RPM_REPO_SCHEMA
def priority_sort(self, repo_data): def priority_sort(self, repo_data):
# DEB repository expects general values from 0 to 1000. 0 # DEB repository expects general values from 0 to 1000. 0
# to have lowest priority and 1000 -- the highest. Note that a # to have lowest priority and 1000 -- the highest. Note that a
@ -99,7 +103,7 @@ class RpmRepositoryDriver(RepositoryDriverBase):
def get_repository(self, connection, repository_data, arch, consumer): def get_repository(self, connection, repository_data, arch, consumer):
consumer(Repository( consumer(Repository(
name=repository_data['name'], name=repository_data['name'],
url=utils.normalize_repository_url(repository_data["url"]), url=utils.normalize_repository_url(repository_data["uri"]),
architecture=arch, architecture=arch,
origin="" origin=""
)) ))
@ -195,7 +199,7 @@ class RpmRepositoryDriver(RepositoryDriverBase):
def create_repository(self, repository_data, arch): def create_repository(self, repository_data, arch):
repository = Repository( repository = Repository(
name=repository_data['name'], name=repository_data['name'],
url=utils.normalize_repository_url(repository_data["url"]), url=utils.normalize_repository_url(repository_data["uri"]),
architecture=arch, architecture=arch,
origin=repository_data.get('origin') origin=repository_data.get('origin')
) )

View File

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Mirantis, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from packetary.schemas.deb_repo_schema import DEB_REPO_SCHEMA
from packetary.schemas.package_files_schema import PACKAGE_FILES_SCHEMA
from packetary.schemas.packages_schema import PACKAGES_SCHEMA
from packetary.schemas.rpm_repo_schema import RPM_REPO_SCHEMA
__all__ = [
"DEB_REPO_SCHEMA",
"PACKAGES_SCHEMA",
"RPM_REPO_SCHEMA",
"PACKAGE_FILES_SCHEMA"
]

View File

@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Mirantis, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
DEB_REPO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"required": [
"name",
"uri",
"suite",
],
"properties": {
"name": {
"type": "string"
},
"uri": {
"type": "string"
},
"path": {
"type": "string"
},
"priority": {
"anyOf": [
{
"type": "number",
"minimum": 0,
},
{
"type": "null"
}
]
},
"suite": {
"type": "string"
},
"section": {
"type": "array",
"items": {"type": "string"}
}
}
}

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Mirantis, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
PACKAGE_FILES_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "array",
"items": {
"type": "string",
"pattern": "^(\/|file:\/\/|https?:\/\/).+$"
}
}

View File

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Mirantis, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
PACKAGES_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "array",
"items": {
"type": "object",
"required": [
"name",
"versions"
],
"properties": {
"name": {
"type": "string"
},
"versions": {
"type": "array",
"items": [
{
"type": "string",
"pattern": "^([<>]=?|=)\s+.+$"
}
]
}
}
}
}

View File

@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Mirantis, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
RPM_REPO_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"required": [
"name",
"uri",
],
"properties": {
"name": {
"type": "string"
},
"uri": {
"type": "string"
},
"path": {
"type": "string"
},
"priority": {
"anyOf": [
{
"type": "number",
"minimum": 1,
"maximum": 99,
},
{
"type": "null"
}
]
}
}
}

View File

@ -31,3 +31,9 @@ class TestCase(unittest.TestCase):
assertion( assertion(
exp, method(*value), "{0} != f({1})".format(exp, value) exp, method(*value), "{0} != f({1})".format(exp, value)
) )
def assertNotRaises(self, exception, method, *args, **kwargs):
try:
method(*args, **kwargs)
except exception as e:
self.fail("Unexpected error: {0}".format(e))

View File

@ -22,6 +22,7 @@ from packetary import objects
def gen_repository(name="test", url="file:///test", def gen_repository(name="test", url="file:///test",
architecture="x86_64", origin="Test", **kwargs): architecture="x86_64", origin="Test", **kwargs):
"""Helper to create Repository object with default attributes.""" """Helper to create Repository object with default attributes."""
url = kwargs.pop("uri", url)
return objects.Repository(name, url, architecture, origin, **kwargs) return objects.Repository(name, url, architecture, origin, **kwargs)

View File

@ -21,6 +21,7 @@ import os.path as path
import six import six
from packetary.drivers import deb_driver from packetary.drivers import deb_driver
from packetary.schemas import DEB_REPO_SCHEMA
from packetary.tests import base from packetary.tests import base
from packetary.tests.stubs.generator import gen_package from packetary.tests.stubs.generator import gen_package
from packetary.tests.stubs.generator import gen_repository from packetary.tests.stubs.generator import gen_repository
@ -66,7 +67,7 @@ class TestDebDriver(base.TestCase):
def test_get_repository(self): def test_get_repository(self):
repos = [] repos = []
repo_data = { repo_data = {
"name": "repo1", "url": "http://host", "suite": "trusty", "name": "repo1", "uri": "http://host", "suite": "trusty",
"section": ["main", "universe"], "path": "my_path" "section": ["main", "universe"], "path": "my_path"
} }
self.connection.open_stream.return_value = {"Origin": "Ubuntu"} self.connection.open_stream.return_value = {"Origin": "Ubuntu"}
@ -99,7 +100,7 @@ class TestDebDriver(base.TestCase):
def test_get_repository_if_release_does_not_exist(self): def test_get_repository_if_release_does_not_exist(self):
repo_data = { repo_data = {
"name": "repo1", "url": "http://host", "suite": "trusty", "name": "repo1", "uri": "http://host", "suite": "trusty",
"section": ["main"], "path": "my_path" "section": ["main"], "path": "my_path"
} }
repos = [] repos = []
@ -115,7 +116,7 @@ class TestDebDriver(base.TestCase):
def test_get_repository_fail_if_error(self): def test_get_repository_fail_if_error(self):
repo_data = { repo_data = {
"name": "repo1", "url": "http://host", "suite": "trusty", "name": "repo1", "uri": "http://host", "suite": "trusty",
"section": ["main"], "path": "my_path" "section": ["main"], "path": "my_path"
} }
repos = [] repos = []
@ -132,7 +133,7 @@ class TestDebDriver(base.TestCase):
with self.assertRaisesRegexp(ValueError, "does not supported"): with self.assertRaisesRegexp(ValueError, "does not supported"):
self.driver.get_repository( self.driver.get_repository(
self.connection, self.connection,
{"url": "http://host", "suite": "trusty"}, {"uri": "http://host", "suite": "trusty"},
"x86_64", "x86_64",
lambda x: None lambda x: None
) )
@ -313,14 +314,14 @@ class TestDebDriver(base.TestCase):
@mock.patch("packetary.drivers.deb_driver.utils.ensure_dir_exist") @mock.patch("packetary.drivers.deb_driver.utils.ensure_dir_exist")
def test_create_repository(self, mkdir_mock, deb822, gzip, open, os): def test_create_repository(self, mkdir_mock, deb822, gzip, open, os):
repository_data = { repository_data = {
"name": "Test", "url": "file:///repo", "suite": "trusty", "name": "Test", "uri": "file:///repo", "suite": "trusty",
"section": "main", "type": "rpm", "priority": "100", "section": "main", "type": "rpm", "priority": "100",
"origin": "Origin", "path": "/repo" "origin": "Origin", "path": "/repo"
} }
repo = self.driver.create_repository(repository_data, "x86_64") repo = self.driver.create_repository(repository_data, "x86_64")
self.assertEqual(repository_data["name"], repo.name) self.assertEqual(repository_data["name"], repo.name)
self.assertEqual("x86_64", repo.architecture) self.assertEqual("x86_64", repo.architecture)
self.assertEqual(repository_data["url"] + "/", repo.url) self.assertEqual(repository_data["uri"] + "/", repo.url)
self.assertEqual(repository_data["origin"], repo.origin) self.assertEqual(repository_data["origin"], repo.origin)
self.assertEqual( self.assertEqual(
(repository_data["suite"], repository_data["section"]), (repository_data["suite"], repository_data["section"]),
@ -340,7 +341,7 @@ class TestDebDriver(base.TestCase):
def test_createrepository_fails_if_invalid_data(self): def test_createrepository_fails_if_invalid_data(self):
repository_data = { repository_data = {
"name": "Test", "url": "file:///repo", "suite": "trusty", "name": "Test", "uri": "file:///repo", "suite": "trusty",
"type": "rpm", "priority": "100", "type": "rpm", "priority": "100",
"origin": "Origin", "path": "/repo" "origin": "Origin", "path": "/repo"
} }
@ -397,3 +398,7 @@ class TestDebDriver(base.TestCase):
) )
rel_path = self.driver.get_relative_path(repo, "test.pkg") rel_path = self.driver.get_relative_path(repo, "test.pkg")
self.assertEqual("pool/main/t/test.pkg", rel_path) self.assertEqual("pool/main/t/test.pkg", rel_path)
def test_get_repository_data_scheme(self):
schema = self.driver.get_repository_data_schema()
self.assertIs(DEB_REPO_SCHEMA, schema)

View File

@ -19,21 +19,31 @@
import copy import copy
import mock import mock
import jsonschema
from packetary.api import Configuration from packetary.api import Configuration
from packetary.api import Context from packetary.api import Context
from packetary.api import RepositoryApi from packetary.api import RepositoryApi
from packetary.schemas import PACKAGE_FILES_SCHEMA
from packetary.schemas import PACKAGES_SCHEMA
from packetary.tests import base from packetary.tests import base
from packetary.tests.stubs import generator from packetary.tests.stubs import generator
from packetary.tests.stubs.helpers import CallbacksAdapter from packetary.tests.stubs.helpers import CallbacksAdapter
@mock.patch("packetary.api.jsonschema")
class TestRepositoryApi(base.TestCase): class TestRepositoryApi(base.TestCase):
def setUp(self): def setUp(self):
self.controller = CallbacksAdapter() self.controller = CallbacksAdapter()
self.api = RepositoryApi(self.controller) self.api = RepositoryApi(self.controller)
self.repo_data = {"name": "repo1", "url": "file:///repo1"} self.repo_data = {"name": "repo1", "uri": "file:///repo1"}
self.requirements_data = [
{"name": "test1"}, {"name": "test2", "versions": ["< 3", "> 1"]}
]
self.schema = {}
self.repo = generator.gen_repository(**self.repo_data) self.repo = generator.gen_repository(**self.repo_data)
self.controller.load_repositories.return_value = [self.repo] self.controller.load_repositories.return_value = [self.repo]
self.controller.get_repository_data_schema.return_value = self.schema
self._generate_packages() self._generate_packages()
def _generate_packages(self): def _generate_packages(self):
@ -56,7 +66,8 @@ class TestRepositoryApi(base.TestCase):
@mock.patch("packetary.api.RepositoryController") @mock.patch("packetary.api.RepositoryController")
@mock.patch("packetary.api.ConnectionsManager") @mock.patch("packetary.api.ConnectionsManager")
def test_create_with_config(self, connection_mock, controller_mock): def test_create_with_config(self, connection_mock, controller_mock,
jsonschema_mock):
config = Configuration( config = Configuration(
http_proxy="http://localhost", https_proxy="https://localhost", http_proxy="http://localhost", https_proxy="https://localhost",
retries_num=10, retry_interval=1, threads_num=8, retries_num=10, retry_interval=1, threads_num=8,
@ -75,7 +86,8 @@ class TestRepositoryApi(base.TestCase):
@mock.patch("packetary.api.RepositoryController") @mock.patch("packetary.api.RepositoryController")
@mock.patch("packetary.api.ConnectionsManager") @mock.patch("packetary.api.ConnectionsManager")
def test_create_with_context(self, connection_mock, controller_mock): def test_create_with_context(self, connection_mock, controller_mock,
jsonschema_mock):
config = Configuration( config = Configuration(
http_proxy="http://localhost", https_proxy="https://localhost", http_proxy="http://localhost", https_proxy="https://localhost",
retries_num=10, retry_interval=1, threads_num=8, retries_num=10, retry_interval=1, threads_num=8,
@ -93,42 +105,67 @@ class TestRepositoryApi(base.TestCase):
context, "deb", "x86_64" context, "deb", "x86_64"
) )
def test_create_repository(self): def test_create_repository(self, jsonschema_mock):
file_urls = ["file://test1.pkg"] file_urls = ["file://test1.pkg"]
self.api.create_repository(self.repo_data, file_urls) self.api.create_repository(self.repo_data, file_urls)
self.controller.create_repository.assert_called_once_with( self.controller.create_repository.assert_called_once_with(
self.repo_data, file_urls self.repo_data, file_urls
) )
jsonschema_mock.validate.assert_has_calls(
[
mock.call(self.repo_data, self.schema),
mock.call(file_urls, PACKAGE_FILES_SCHEMA),
]
)
def test_get_packages_as_is(self): def test_get_packages_as_is(self, jsonschema_mock):
packages = self.api.get_packages([self.repo_data], None) packages = self.api.get_packages([self.repo_data], None)
self.assertEqual(5, len(packages)) self.assertEqual(5, len(packages))
self.assertItemsEqual( self.assertItemsEqual(
self.packages, self.packages,
packages packages
) )
jsonschema_mock.validate.assert_called_once_with(
self.repo_data, self.schema
)
def test_get_packages_by_requirements_with_mandatory(self): def test_get_packages_by_requirements_with_mandatory(self,
jsonschema_mock):
requirements = [{"name": "package1"}]
packages = self.api.get_packages( packages = self.api.get_packages(
[self.repo_data], [{"name": "package1"}], True [self.repo_data], requirements, True
) )
self.assertEqual(3, len(packages)) self.assertEqual(3, len(packages))
self.assertItemsEqual( self.assertItemsEqual(
["package1", "package2", "package3"], ["package1", "package2", "package3"],
(x.name for x in packages) (x.name for x in packages)
) )
jsonschema_mock.validate.assert_has_calls(
[
mock.call(self.repo_data, self.schema),
mock.call(requirements, PACKAGES_SCHEMA),
]
)
def test_get_packages_by_requirements_without_mandatory(self): def test_get_packages_by_requirements_without_mandatory(self,
jsonschema_mock):
requirements = [{"name": "package4"}]
packages = self.api.get_packages( packages = self.api.get_packages(
[self.repo_data], [{"name": "package4"}], False [self.repo_data], requirements, False
) )
self.assertEqual(2, len(packages)) self.assertEqual(2, len(packages))
self.assertItemsEqual( self.assertItemsEqual(
["package1", "package4"], ["package1", "package4"],
(x.name for x in packages) (x.name for x in packages)
) )
jsonschema_mock.validate.assert_has_calls(
[
mock.call(self.repo_data, self.schema),
mock.call(requirements, PACKAGES_SCHEMA),
]
)
def test_clone_repositories_as_is(self): def test_clone_repositories_as_is(self, jsonschema_mock):
# return value is used as statistics # return value is used as statistics
mirror = copy.copy(self.repo) mirror = copy.copy(self.repo)
mirror.url = "file:///mirror/repo" mirror.url = "file:///mirror/repo"
@ -143,15 +180,19 @@ class TestRepositoryApi(base.TestCase):
) )
self.assertEqual(6, stats.total) self.assertEqual(6, stats.total)
self.assertEqual(4, stats.copied) self.assertEqual(4, stats.copied)
jsonschema_mock.validate.assert_called_once_with(
self.repo_data, self.schema
)
def test_clone_by_requirements_with_mandatory(self): def test_clone_by_requirements_with_mandatory(self, jsonschema_mock):
# return value is used as statistics # return value is used as statistics
mirror = copy.copy(self.repo) mirror = copy.copy(self.repo)
mirror.url = "file:///mirror/repo" mirror.url = "file:///mirror/repo"
requirements = [{"name": "package1"}]
self.controller.fork_repository.return_value = mirror self.controller.fork_repository.return_value = mirror
self.controller.assign_packages.return_value = [0, 1, 1] self.controller.assign_packages.return_value = [0, 1, 1]
stats = self.api.clone_repositories( stats = self.api.clone_repositories(
[self.repo_data], [{"name": "package1"}], [self.repo_data], requirements,
"/mirror", include_mandatory=True "/mirror", include_mandatory=True
) )
packages = {self.packages[0], self.packages[1], self.packages[2]} packages = {self.packages[0], self.packages[1], self.packages[2]}
@ -163,15 +204,23 @@ class TestRepositoryApi(base.TestCase):
) )
self.assertEqual(3, stats.total) self.assertEqual(3, stats.total)
self.assertEqual(2, stats.copied) self.assertEqual(2, stats.copied)
jsonschema_mock.validate.assert_has_calls(
[
mock.call(self.repo_data, self.schema),
mock.call(requirements, PACKAGES_SCHEMA),
]
)
def test_clone_by_requirements_without_mandatory(self): def test_clone_by_requirements_without_mandatory(self,
jsonschema_mock):
# return value is used as statistics # return value is used as statistics
mirror = copy.copy(self.repo) mirror = copy.copy(self.repo)
mirror.url = "file:///mirror/repo" mirror.url = "file:///mirror/repo"
requirements = [{"name": "package4"}]
self.controller.fork_repository.return_value = mirror self.controller.fork_repository.return_value = mirror
self.controller.assign_packages.return_value = [0, 4] self.controller.assign_packages.return_value = [0, 4]
stats = self.api.clone_repositories( stats = self.api.clone_repositories(
[self.repo_data], [{"name": "package4"}], [self.repo_data], requirements,
"/mirror", include_mandatory=False "/mirror", include_mandatory=False
) )
packages = {self.packages[0], self.packages[3]} packages = {self.packages[0], self.packages[3]}
@ -183,30 +232,65 @@ class TestRepositoryApi(base.TestCase):
) )
self.assertEqual(2, stats.total) self.assertEqual(2, stats.total)
self.assertEqual(1, stats.copied) self.assertEqual(1, stats.copied)
jsonschema_mock.validate.assert_has_calls(
[
mock.call(self.repo_data, self.schema),
mock.call(requirements, PACKAGES_SCHEMA),
]
)
def test_get_unresolved(self): def test_get_unresolved(self, jsonschema_mock):
unresolved = self.api.get_unresolved_dependencies([self.repo_data]) unresolved = self.api.get_unresolved_dependencies([self.repo_data])
self.assertItemsEqual(["package6"], (x.name for x in unresolved)) self.assertItemsEqual(["package6"], (x.name for x in unresolved))
jsonschema_mock.validate.assert_called_once_with(
self.repo_data, self.schema
)
def test_load_requirements(self): def test_load_requirements(self, jsonschema_mock):
expected = { expected = {
generator.gen_relation("test1"), generator.gen_relation("test1"),
generator.gen_relation("test2", ["<", "3"]), generator.gen_relation("test2", ["<", "3"]),
generator.gen_relation("test2", [">", "1"]), generator.gen_relation("test2", [">", "1"]),
} }
actual = set(self.api._load_requirements( actual = set(self.api._load_requirements(
[{"name": "test1"}, {"name": "test2", "versions": ["< 3", "> 1"]}] self.requirements_data
)) ))
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
self.assertIsNone(self.api._load_requirements(None)) self.assertIsNone(self.api._load_requirements(None))
jsonschema_mock.validate.assert_called_once_with(
self.requirements_data,
PACKAGES_SCHEMA
)
def test_validate_repo_data(self): def test_validate_data(self, jsonschema_mock):
# TODO(bgaifullin) implement me self.api._validate_data(self.repo_data, self.schema)
pass jsonschema_mock.validate.assert_called_once_with(
self.repo_data, self.schema
)
def test_validate_requirements_data(self): def test_validate_invalid_data(self, jschema_m):
# TODO(bgaifullin) implement me jschema_m.ValidationError = jsonschema.ValidationError
pass jschema_m.SchemaError = jsonschema.SchemaError
paths = [("a", "b"), ()]
for path in paths:
msg = "Invalid data: error."
details = "\nField: {0}".format(".".join(path)) if path else ""
with self.assertRaisesRegexp(ValueError, msg + details):
jschema_m.validate.side_effect = jsonschema.ValidationError(
"error", path=path
)
self.api._validate_data([], {})
jschema_m.validate.assert_called_with([], {})
jschema_m.validate.reset_mock()
msg = "Invalid schema: error."
with self.assertRaisesRegexp(ValueError, msg + details):
jschema_m.validate.side_effect = jsonschema.SchemaError(
"error", schema_path=path
)
self.api._validate_data([], {})
jschema_m.validate.assert_called_with([], {})
class TestContext(base.TestCase): class TestContext(base.TestCase):

View File

@ -53,7 +53,7 @@ class TestRepositoryController(base.TestCase):
self.assertIs(self.driver, controller.driver) self.assertIs(self.driver, controller.driver)
def test_load_repositories(self): def test_load_repositories(self):
repo_data = {"name": "test", "url": "file:///test1"} repo_data = {"name": "test", "uri": "file:///test1"}
repo = gen_repository(**repo_data) repo = gen_repository(**repo_data)
self.driver.get_repository = CallbacksAdapter() self.driver.get_repository = CallbacksAdapter()
self.driver.get_repository.side_effect = [repo] self.driver.get_repository.side_effect = [repo]
@ -150,7 +150,7 @@ class TestRepositoryController(base.TestCase):
def test_create_repository(self): def test_create_repository(self):
repository_data = { repository_data = {
"name": "Test", "url": "file:///repo/", "name": "Test", "uri": "file:///repo/",
"section": ("trusty", "main"), "origin": "Test" "section": ("trusty", "main"), "origin": "Test"
} }
repo = gen_repository(**repository_data) repo = gen_repository(**repository_data)

View File

@ -23,6 +23,7 @@ import sys
import six import six
from packetary.objects import FileChecksum from packetary.objects import FileChecksum
from packetary.schemas import RPM_REPO_SCHEMA
from packetary.tests import base from packetary.tests import base
from packetary.tests.stubs.generator import gen_repository from packetary.tests.stubs.generator import gen_repository
from packetary.tests.stubs.helpers import get_compressed from packetary.tests.stubs.helpers import get_compressed
@ -68,7 +69,7 @@ class TestRpmDriver(base.TestCase):
def test_get_repository(self): def test_get_repository(self):
repos = [] repos = []
repo_data = {"name": "os", "url": "http://host/centos/os/x86_64/"} repo_data = {"name": "os", "uri": "http://host/centos/os/x86_64/"}
self.driver.get_repository( self.driver.get_repository(
self.connection, self.connection,
repo_data, repo_data,
@ -220,13 +221,13 @@ class TestRpmDriver(base.TestCase):
@mock.patch("packetary.drivers.rpm_driver.utils.ensure_dir_exist") @mock.patch("packetary.drivers.rpm_driver.utils.ensure_dir_exist")
def test_create_repository(self, ensure_dir_exists_mock): def test_create_repository(self, ensure_dir_exists_mock):
repository_data = { repository_data = {
"name": "Test", "url": "file:///repo/os/x86_64", "origin": "Test" "name": "Test", "uri": "file:///repo/os/x86_64", "origin": "Test"
} }
repo = self.driver.create_repository(repository_data, "x86_64") repo = self.driver.create_repository(repository_data, "x86_64")
ensure_dir_exists_mock.assert_called_once_with("/repo/os/x86_64/") ensure_dir_exists_mock.assert_called_once_with("/repo/os/x86_64/")
self.assertEqual(repository_data["name"], repo.name) self.assertEqual(repository_data["name"], repo.name)
self.assertEqual("x86_64", repo.architecture) self.assertEqual("x86_64", repo.architecture)
self.assertEqual(repository_data["url"] + "/", repo.url) self.assertEqual(repository_data["uri"] + "/", repo.url)
self.assertEqual(repository_data["origin"], repo.origin) self.assertEqual(repository_data["origin"], repo.origin)
@mock.patch("packetary.drivers.rpm_driver.utils") @mock.patch("packetary.drivers.rpm_driver.utils")
@ -278,3 +279,7 @@ class TestRpmDriver(base.TestCase):
) )
rel_path = self.driver.get_relative_path(repo, "test.pkg") rel_path = self.driver.get_relative_path(repo, "test.pkg")
self.assertEqual("packages/test.pkg", rel_path) self.assertEqual("packages/test.pkg", rel_path)
def test_get_repository_data_schema(self):
schema = self.driver.get_repository_data_schema()
self.assertIs(RPM_REPO_SCHEMA, schema)

View File

@ -0,0 +1,287 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Mirantis, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import jsonschema
from packetary.schemas import DEB_REPO_SCHEMA
from packetary.schemas import PACKAGE_FILES_SCHEMA
from packetary.schemas import PACKAGES_SCHEMA
from packetary.schemas import RPM_REPO_SCHEMA
from packetary.tests import base
class TestRepositorySchemaBase(base.TestCase):
def check_invalid_name(self):
self._check_invalid_type('name')
def check_invalid_uri(self):
self._check_invalid_type('uri')
def check_invalid_path(self):
self._check_invalid_type('path')
def check_required_properties(self):
repos_data = [{"name": "os"}, {"uri": "file:///repo"}]
for data in repos_data:
self.assertRaisesRegexp(
jsonschema.ValidationError,
"is a required property",
jsonschema.validate, data, self.schema
)
def check_priority(self, min_value=None, max_value=None):
if min_value is not None:
self._check_invalid_priority(min_value - 1)
self._check_valid_priority(min_value)
if max_value is not None:
self._check_invalid_priority(max_value + 1)
self._check_valid_priority(max_value)
self._check_valid_priority(None)
self._check_invalid_priority("abc")
def _check_invalid_type(self, key):
invalid_data = {key: 123}
self.assertRaisesRegexp(
jsonschema.ValidationError, "123 is not of type 'string'",
jsonschema.validate, invalid_data[key],
self.schema['properties'][key]
)
def _check_valid_priority(self, value):
self.assertNotRaises(
jsonschema.ValidationError, jsonschema.validate, value,
self.schema['properties']['priority']
)
def _check_invalid_priority(self, value):
self.assertRaisesRegexp(
jsonschema.ValidationError,
"is not valid under any of the given schemas",
jsonschema.validate, value, self.schema['properties']['priority']
)
class TestDebRepoSchema(TestRepositorySchemaBase):
def setUp(self):
self.schema = DEB_REPO_SCHEMA
def test_valid_repo_data(self):
repo_data = {
"name": "os", "uri": "file:///repo", "suite": "trusty",
"section": ["main", "multiverse"], "path": "/some/path",
"priority": 1001
}
self.assertNotRaises(
jsonschema.ValidationError, jsonschema.validate,
repo_data, self.schema
)
def test_priority(self):
self.check_priority(0)
def test_validation_fail_for_required_properties(self):
self.check_required_properties()
repo_data = {"name": "os", "uri": "file:///repo"}
self.assertRaisesRegexp(
jsonschema.ValidationError, "'suite' is a required property",
jsonschema.validate, repo_data, self.schema
)
def test_validation_fail_if_name_is_invalid(self):
self.check_invalid_name()
def test_validation_fail_if_uri_is_invalid(self):
self.check_invalid_uri()
def test_validation_fail_if_path_is_invalid(self):
self.check_invalid_path()
def test_validation_fail_if_suite_is_invalid(self):
repo_data = {"name": "os", "uri": "file:///repo", "suite": 123}
self.assertRaisesRegexp(
jsonschema.ValidationError, "123 is not of type 'string'",
jsonschema.validate, repo_data, self.schema
)
def test_validation_fail_if_section_not_array(self):
repo_data = {
"name": "os", "uri": "file:///repo", "suite": "trusty",
"section": 123
}
self.assertRaisesRegexp(
jsonschema.ValidationError, "123 is not of type 'array'",
jsonschema.validate, repo_data, self.schema
)
def test_validation_fail_if_section_not_string(self):
repo_data = {
"name": "os", "uri": "file:///repo", "suite": "trusty",
"section": [123]
}
self.assertRaisesRegexp(
jsonschema.ValidationError, "123 is not of type 'string'",
jsonschema.validate, repo_data, self.schema
)
class TestRpmRepoSchema(TestRepositorySchemaBase):
def setUp(self):
self.schema = RPM_REPO_SCHEMA
def test_valid_repo_data(self):
repo_data = {
"name": "os", "uri": "file:///repo", "path": "/some/path",
"priority": 45
}
self.assertNotRaises(
jsonschema.ValidationError, jsonschema.validate, repo_data,
self.schema
)
def test_priority(self):
self.check_priority(1, 99)
def test_validation_fail_for_required_properties(self):
self.check_required_properties()
def test_validation_fail_if_name_is_invalid(self):
self.check_invalid_name()
def test_validation_fail_if_uri_is_invalid(self):
self.check_invalid_uri()
def test_validation_fail_if_path_is_invalid(self):
self.check_invalid_path()
class TestPackagesSchema(base.TestCase):
def setUp(self):
self.schema = PACKAGES_SCHEMA
def test_valid_requirements_data(self):
requirements_data = [
{"name": "test1", "versions": [">= 1.1.2", "<= 3"]},
{"name": "test2", "versions": ["< 3", "> 1", ">= 4"]},
{"name": "test3", "versions": ["= 3"]},
{"name": "test4", "versions": ["= 3"]}
]
self.assertNotRaises(
jsonschema.ValidationError, jsonschema.validate, requirements_data,
self.schema
)
def test_validation_fail_for_required_properties(self):
requirements_data = [
[{"name": "test1"}],
[{"versions": ["< 3", "> 1"]}]
]
for data in requirements_data:
self.assertRaisesRegexp(
jsonschema.ValidationError,
"is a required property",
jsonschema.validate, data, self.schema
)
def test_validation_fail_if_name_is_invalid(self):
requirements_data = [
{"name": 123, "versions": [">= 1.1.2", "<= 3"]},
]
self.assertRaisesRegexp(
jsonschema.ValidationError, "123 is not of type 'string'",
jsonschema.validate, requirements_data, self.schema
)
def test_validation_fail_if_versions_not_array(self):
requirements_data = [
{"name": "test1", "versions": 123}
]
self.assertRaisesRegexp(
jsonschema.ValidationError, "123 is not of type 'array'",
jsonschema.validate, requirements_data,
self.schema
)
def test_validation_fail_if_versions_not_string(self):
requirements_data = [
{"name": "test1", "versions": [123]}
]
self.assertRaisesRegexp(
jsonschema.ValidationError, "123 is not of type 'string'",
jsonschema.validate, requirements_data,
self.schema
)
def test_validation_fail_if_versions_not_match(self):
versions = [
["1.1.2"], # relational operator
[">=3"], # not whitespace after ro
["== 3"] # ==
]
for version in versions:
self.assertRaisesRegexp(
jsonschema.ValidationError, "does not match",
jsonschema.validate, version,
self.schema['items']['properties']['versions']
)
class TestPackageFilesSchema(base.TestCase):
def setUp(self):
self.schema = PACKAGE_FILES_SCHEMA
def test_valid_file_urls(self):
file_urls = [
"file://test1.pkg",
"file:///test2.pkg",
"/test3.pkg",
"http://test4.pkg",
"https://test5.pkg"
]
self.assertNotRaises(
jsonschema.ValidationError, jsonschema.validate, file_urls,
self.schema
)
def test_validation_fail_if_urls_not_array(self):
file_urls = "/test1.pkg"
self.assertRaisesRegexp(
jsonschema.ValidationError, "'/test1.pkg' is not of type 'array'",
jsonschema.validate, file_urls, self.schema
)
def test_validation_fail_if_urls_not_string(self):
file_urls = [123]
self.assertRaisesRegexp(
jsonschema.ValidationError, "123 is not of type 'string'",
jsonschema.validate, file_urls, self.schema
)
def test_validation_fail_if_invalid_file_urls(self):
file_urls = [
["test1.pkg"], # does not match pattern
["./test2.pkg"], # does not match pattern
["file//test3.pkg"], # does not match pattern
["http//test4.pkg"] # does not match pattern
]
for url in file_urls[2:]:
self.assertRaisesRegexp(
jsonschema.ValidationError, "does not match",
jsonschema.validate, url, self.schema
)

View File

@ -12,3 +12,4 @@ stevedore>=1.1.0
six>=1.5.2 six>=1.5.2
python-debian>=0.1.21 python-debian>=0.1.21
lxml>=3.2 lxml>=3.2
jsonschema!=2.5.0,<3.0.0,>=2.0.0 # MIT