diff --git a/.travis.yml b/.travis.yml index 65eb394..ed7e3c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,5 +13,7 @@ matrix: - dist: xenial python: "3.7" env: TOXENV=flake8 +before_install: + - sudo apt-get install -y rpm # for rpm-py-installer (koji requirement) install: pip install tox-travis coveralls script: tox diff --git a/Dockerfile b/Dockerfile index ece75b1..2b1ffc4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,7 @@ RUN dnf -y install \ python3-gunicorn \ python3-flask \ python3-jsonschema \ + python3-koji \ python3-requests \ python3-operator-courier \ && dnf -y clean all \ diff --git a/README.md b/README.md index bcdfb14..281507d 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,10 @@ class ProdConfig: LOG_LEVEL = "INFO" LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" DEFAULT_RELEASE_VERSION = "1.0.0" # default operator manifest version + + # configuration of Koji URLs + KOJIHUB_URL = 'https://koji.fedoraproject.org/kojihub' + KOJIROOT_URL = 'https://kojipkgs.fedoraproject.org/' ``` ## Running service @@ -140,6 +144,82 @@ curl \ -F "file=@manifests.zip" ``` +### Uploading operators manifests from koji + +Downloads operator manifest archive from koji build specified by N-V-R. +Build must be done by [OSBS](https://osbs.readthedocs.io) +service which extracts operator manifests from images and stores them as a zip +archive in koji. + +#### Endpoints + +* [POST] `/v1///koji//` +* [POST] `/v1///koji/` + +Operator image build must be specified by N-V-R value from koji. + +If `` is omitted: +* the latest release version will be incremented and used (for example from `2.5.1` to `3.0.0`) +* for new repository a default initial version will be used (`DEFAULT_RELEASE_VERSION` config option) + +`` must be unique for repository. Quay doesn't support overwriting of releases. + +#### Replies + +**OK** + +HTTP code: 200 + +```json +{ + "organization": "organization name", + "repo": "repository name", + "version": "0.0.1", + "nvr": "n-v-r", + "extracted_files": ["packages.yml", "..."] +} + +``` + +**Failures** + +Error messages have following format: +``` +{ + "status": , + "error": "", + "message": "", +} +``` + + +| HTTP Code / `status` | `error` | Explanation | +|-----------|------------------------|---------------------| +|400| OMPSUploadedFileError | Uploaded file didn't meet expectations (not a zip file, too big after unzip, corrupted zip file) | +|400| OMPSInvalidVersionFormat | Invalid version format in URL | +|400| KojiNotAnOperatorImage | Requested build is not an operator image | +|403| OMPSAuthorizationHeaderRequired| No `Authorization` header found in request| +|404| KojiNVRBuildNotFound | Requested build not found in koji | +|500| KojiManifestsArchiveNotFound | Manifest archive not found in koji build | +|500| KojiError | Koji generic error (connection failures, etc.) | +|500| QuayCourierError | operator-courier module raised exception during building and pushing manifests to quay| +|500| QuayPackageError | Failed to get information about application packages from quay | + + +#### Example +```bash +curl \ + -H "Authorization: ${TOKEN}" \ + -X POST https://example.com/v1/myorg/myrepo/koji/image-1.2-5 +``` +or with explicit release version +```bash +curl \ + -H "Authorization: ${TOKEN}" \ + -X POST https://example.com/v1/myorg/myrepo/koji/image-1.2-5/1.1.5 +``` + + ### Removing released operators manifests @@ -220,10 +300,19 @@ pip install '.[test]' ### Running tests Project is integrated with tox: + +* please install `rpm-devel` (Fedora) or `rpm` (Ubuntu) package to be able +build `koji` dependency `rpm-py-installer` in `tox`: +```bash +sudo dnf install -y rpm-devel +``` +* run: ```bash tox ``` + + To run tests manually, you can use pytest directly: ```bash py.test tests/ diff --git a/omps/api/v1/push.py b/omps/api/v1/push.py index 5c15483..335b8ad 100644 --- a/omps/api/v1/push.py +++ b/omps/api/v1/push.py @@ -2,6 +2,8 @@ # Copyright (C) 2019 Red Hat, Inc # see the LICENSE file for license # + +from functools import partial import logging import os from tempfile import NamedTemporaryFile, TemporaryDirectory @@ -21,6 +23,7 @@ OMPSExpectedFileError, QuayPackageNotFound, ) +from omps.koji_util import KOJI from omps.quay import QuayOrganization, ReleaseVersion logger = logging.getLogger(__name__) @@ -35,7 +38,56 @@ def validate_allowed_extension(filename): extension, ALLOWED_EXTENSIONS)) -def extract_zip_file( +def _extract_zip_file( + filepath, target_dir, + max_uncompressed_size=DEFAULT_ZIPFILE_MAX_UNCOMPRESSED_SIZE +): + """Extract zip file into target directory + + :param filepath: path to zip archive file + :param target_dir: directory where extracted files will be stored + :param max_uncompressed_size: size in Bytes how big data can be accepted + after uncompressing + """ + try: + archive = zipfile.ZipFile(filepath) + except zipfile.BadZipFile as e: + raise OMPSUploadedFileError(str(e)) + + if logger.isEnabledFor(logging.DEBUG): + # log content of zipfile + logger.debug( + 'Content of zip archive:\n%s', + '\n'.join( + "name={zi.filename}, compress_size={zi.compress_size}, " + "file_size={zi.file_size}".format(zi=zipinfo) + for zipinfo in archive.filelist + ) + ) + + uncompressed_size = sum(zi.file_size for zi in archive.filelist) + if uncompressed_size > max_uncompressed_size: + raise OMPSUploadedFileError( + "Uncompressed archive is larger than limit " + "({}B>{}B)".format( + uncompressed_size, max_uncompressed_size + )) + + try: + bad_file = archive.testzip() + except RuntimeError as e: + # trying to open an encrypted zip file without a password + raise OMPSUploadedFileError(str(e)) + + if bad_file is not None: + raise OMPSUploadedFileError( + "CRC check failed for file {} in archive".format(bad_file) + ) + archive.extractall(target_dir) + archive.close() + + +def extract_zip_file_from_request( req, target_dir, max_uncompressed_size=DEFAULT_ZIPFILE_MAX_UNCOMPRESSED_SIZE ): @@ -60,43 +112,25 @@ def extract_zip_file( with NamedTemporaryFile('w', suffix='.zip', dir=target_dir) as tmpf: uploaded_file.save(tmpf.name) - try: - archive = zipfile.ZipFile(tmpf.name) - except zipfile.BadZipFile as e: - raise OMPSUploadedFileError(str(e)) - - if logger.isEnabledFor(logging.DEBUG): - # log content of zipfile - logger.debug( - 'Content of uploaded zip archive "%s":\n%s', - uploaded_file.filename, '\n'.join( - "name={zi.filename}, compress_size={zi.compress_size}, " - "file_size={zi.file_size}".format(zi=zipinfo) - for zipinfo in archive.filelist - ) - ) + _extract_zip_file(tmpf.name, target_dir, + max_uncompressed_size=max_uncompressed_size) - uncompressed_size = sum(zi.file_size for zi in archive.filelist) - if uncompressed_size > max_uncompressed_size: - raise OMPSUploadedFileError( - "Uncompressed archive is larger than limit " - "({}B>{}B)".format( - uncompressed_size, max_uncompressed_size - )) - try: - bad_file = archive.testzip() - except RuntimeError as e: - # trying to open an encrypted zip file without a password - raise OMPSUploadedFileError(str(e)) - - if bad_file is not None: - raise OMPSUploadedFileError( - "CRC check failed for file {} in archive".format(bad_file) - ) +def extract_zip_file_from_koji( + nvr, target_dir, + max_uncompressed_size=DEFAULT_ZIPFILE_MAX_UNCOMPRESSED_SIZE +): + """Store content of operators_manifests zipfile in target_dir - archive.extractall(target_dir) - archive.close() + :param nvr: N-V-R of koji build + :param target_dir: directory where extracted files will be stored + :param max_uncompressed_size: size in Bytes how big data can be accepted + after uncompressing + """ + with NamedTemporaryFile('wb', suffix='.zip', dir=target_dir) as tmpf: + KOJI.download_manifest_archive(nvr, tmpf) + _extract_zip_file(tmpf.name, target_dir, + max_uncompressed_size=max_uncompressed_size) def _get_package_version(quay_org, repo, version=None): @@ -116,17 +150,18 @@ def _get_package_version(quay_org, repo, version=None): return version -@API.route("///zipfile", defaults={"version": None}, - methods=('POST',)) -@API.route("///zipfile/", methods=('POST',)) -def push_zipfile(organization, repo, version=None): +def _zip_flow(*, organization, repo, version, extract_manifest_func, + extras_data=None): """ - Push the particular version of operator manifest to registry from - the uploaded zipfile - - :param organization: quay.io organization - :param repo: target repository - :param version: version of operator manifest + :param str organization: quay.io organization + :param str repo: target repository + :param str|None version: version of operator manifest + :param Callable[str, int] extract_manifest_func: function to retrieve operator + manifests zip file. First argument of function is path to target dir + where zip archive content will be extracted, second argument max size + of extracted files + :param extras_data: extra data added to response + :return: JSON response """ token = extract_auth_token(request) quay_org = QuayOrganization(organization, token) @@ -139,11 +174,12 @@ def push_zipfile(organization, repo, version=None): 'repo': repo, 'version': version, } + if extras_data: + data.update(extras_data) with TemporaryDirectory() as tmpdir: max_size = current_app.config['ZIPFILE_MAX_UNCOMPRESSED_SIZE'] - extract_zip_file(request, tmpdir, - max_uncompressed_size=max_size) + extract_manifest_func(tmpdir, max_uncompressed_size=max_size) extracted_files = os.listdir(tmpdir) logger.info("Extracted files: %s", extracted_files) data['extracted_files'] = extracted_files @@ -155,8 +191,30 @@ def push_zipfile(organization, repo, version=None): return resp -@API.route("///koji/", methods=('POST',)) -def push_koji_nvr(organization, repo, nvr): +@API.route("///zipfile", defaults={"version": None}, + methods=('POST',)) +@API.route("///zipfile/", methods=('POST',)) +def push_zipfile(organization, repo, version=None): + """ + Push the particular version of operator manifest to registry from + the uploaded zipfile + + :param organization: quay.io organization + :param repo: target repository + :param version: version of operator manifest + """ + return _zip_flow( + organization=organization, + repo=repo, + version=version, + extract_manifest_func=partial(extract_zip_file_from_request, request) + ) + + +@API.route("///koji/", defaults={"version": None}, + methods=('POST',)) +@API.route("///koji//", methods=('POST',)) +def push_koji_nvr(organization, repo, nvr, version): """ Get operator manifest from koji by specified NVR and upload operator manifest to registry @@ -164,12 +222,10 @@ def push_koji_nvr(organization, repo, nvr): :param repo: target repository :param nvr: image NVR from koji """ - data = { - 'organization': organization, - 'repo': repo, - 'nvr': nvr, - 'msg': 'Not Implemented. Testing only' - } - resp = jsonify(data) - resp.status_code = 200 - return resp + return _zip_flow( + organization=organization, + repo=repo, + version=version, + extract_manifest_func=partial(extract_zip_file_from_koji, nvr), + extras_data={'nvr': nvr} + ) diff --git a/omps/app.py b/omps/app.py index 449b838..5e094c7 100644 --- a/omps/app.py +++ b/omps/app.py @@ -9,6 +9,7 @@ from .api.v1 import API as API_V1 from .errors import init_errors_handling +from .koji_util import KOJI from .logger import init_logging from .settings import init_config @@ -31,6 +32,7 @@ def _load_config(app): conf = init_config(app) init_logging(conf) logger.debug('Config loaded. Logging initialized') + KOJI.initialize(conf) def _init_errors_handling(app): diff --git a/omps/constants.py b/omps/constants.py index c1da233..a656672 100644 --- a/omps/constants.py +++ b/omps/constants.py @@ -13,3 +13,5 @@ DEFAULT_RELEASE_VERSION = '1.0.0' ALLOWED_EXTENSIONS = {".zip", } + +KOJI_OPERATOR_MANIFESTS_ARCHIVE_KEY = 'operator_manifests_archive' diff --git a/omps/errors.py b/omps/errors.py index ca31bda..5275f88 100644 --- a/omps/errors.py +++ b/omps/errors.py @@ -50,6 +50,26 @@ class OMPSAuthorizationHeaderRequired(OMPSError): code = 403 +class KojiNVRBuildNotFound(OMPSError): + """Requested build not found in koji""" + code = 404 + + +class KojiNotAnOperatorImage(OMPSError): + """Requested build is not an operator image""" + code = 400 + + +class KojiManifestsArchiveNotFound(OMPSError): + """Manifest archive not found in koji""" + code = 500 + + +class KojiError(OMPSError): + """Failed to retrieve data from koji""" + code = 500 + + def json_error(status, error, message): response = jsonify( {'status': status, diff --git a/omps/koji_util.py b/omps/koji_util.py new file mode 100644 index 0000000..e54d057 --- /dev/null +++ b/omps/koji_util.py @@ -0,0 +1,87 @@ +# +# Copyright (C) 2019 Red Hat, Inc +# see the LICENSE file for license +# + +import koji +import requests + +from omps import constants +from omps.errors import ( + KojiError, + KojiManifestsArchiveNotFound, + KojiNVRBuildNotFound, + KojiNotAnOperatorImage, +) + + +class KojiUtil: + """Utils for koji""" + + def __init__(self): + self._session = None + self._kojihub_url = None + self._kojiroot_url = None + + def initialize(self, conf): + self._kojihub_url = conf.kojihub_url + self._kojiroot_url = conf.kojiroot_url + self._session = koji.ClientSession(self._kojihub_url) + + if not self._kojihub_url.endswith('/'): + self._kojihub_url += '/' + + if not self._kojiroot_url.endswith('/'): + self._kojiroot_url += '/' + + @property + def session(self): + return self._session + + def _file_download(self, url, target_fd): + # inspired by: https://stackoverflow.com/a/16696317 + with requests.get(url, stream=True) as r: + try: + r.raise_for_status() + except Exception as e: + raise KojiError(str(e)) + for chunk in r.iter_content(chunk_size=8192): + if chunk: # filter out keep-alive new chunks + target_fd.write(chunk) + target_fd.flush() + + def download_manifest_archive(self, nvr, target_fd): + """Downloads operators + + :param str koji_url: url for koji connection + :param str nvr: koji image nvr + :param FileObject target_fd: output file object (opened in binary mode) + """ + metadata = self.session.getBuild(nvr) + if metadata is None: + raise KojiNVRBuildNotFound("NVR not found: {}".format(nvr)) + + try: + key = constants.KOJI_OPERATOR_MANIFESTS_ARCHIVE_KEY + filename = metadata['extra'][key] + except KeyError: + raise KojiNotAnOperatorImage("Not an operator image: {}".format(nvr)) + + # OSBS stores manifests archive in zip files (will be moved in future) + logs = self.session.getBuildLogs(metadata['build_id']) + path = None + for logfile in logs: + if logfile['name'] == filename: + path = logfile['path'] + break + + if path is None: + raise KojiManifestsArchiveNotFound( + "Expected archive '{}' with manifests not found in build: " + "{}".format(filename, nvr)) + + url = self._kojiroot_url + path + self._file_download(url, target_fd) + + +KOJI = KojiUtil() diff --git a/omps/settings.py b/omps/settings.py index 7d38f86..484214e 100644 --- a/omps/settings.py +++ b/omps/settings.py @@ -29,6 +29,8 @@ class DevConfig(DefaultConfig): class TestConfig(DefaultConfig): TESTING = True + KOJIHUB_URL = 'https://kojihub.example.com/kojihub' + KOJIROOT_URL = 'https://koji.example.com/kojiroot' def init_config(app): @@ -100,6 +102,16 @@ class Config(object): 'default': constants.DEFAULT_RELEASE_VERSION, 'desc': 'Default release version for new operator manifests releases' }, + 'kojihub_url': { + 'type': str, + 'default': "https://koji.fedoraproject.org/kojihub", + 'desc': 'URL to koji hub for API access' + }, + 'kojiroot_url': { + 'type': str, + 'default': "https://kojipkgs.fedoraproject.org/", + 'desc': 'URL to koji root where build artifacts are stored' + }, } def __init__(self, conf_section_obj): diff --git a/setup.py b/setup.py index 54c9f1d..df80d48 100644 --- a/setup.py +++ b/setup.py @@ -36,6 +36,7 @@ python_requires='>=3.6, <4', install_requires=[ 'Flask==1.0.*', + 'koji', 'requests', 'operator-courier', ], diff --git a/tests/api/v1/conftest.py b/tests/api/v1/conftest.py index cb9e2ec..f63e862 100644 --- a/tests/api/v1/conftest.py +++ b/tests/api/v1/conftest.py @@ -8,7 +8,7 @@ import pytest -EntrypointMeta = namedtuple('EntrypointMeta', 'url_path,org,repo,version') +EntrypointMeta = namedtuple('EntrypointMeta', 'url_path,org,repo,version,nvr') @pytest.fixture(params=[ @@ -27,7 +27,28 @@ def endpoint_push_zipfile(request, release_version): yield EntrypointMeta( url_path=url_path, org=organization, - repo=repo, version=version + repo=repo, version=version, nvr=None, + ) + + +@pytest.fixture(params=[ + True, # endpoint with version + False, # endpoint without version +]) +def endpoint_push_koji(request, release_version): + """Returns URL for koji endpoints""" + organization = 'testorg' + repo = 'repo-Y' + nvr = 'build-1.0.1-2' + version = release_version if request.param else None + + url_path = '/v1/{}/{}/koji/{}'.format(organization, repo, nvr) + if version: + url_path = '{}/{}'.format(url_path, version) + + yield EntrypointMeta( + url_path=url_path, org=organization, + repo=repo, version=version, nvr=nvr, ) @@ -46,7 +67,7 @@ def endpoint_packages(request, release_version): yield EntrypointMeta( url_path=url_path, org=organization, - repo=repo, version=release_version + repo=repo, version=release_version, nvr=None, ) diff --git a/tests/api/v1/test_api_push.py b/tests/api/v1/test_api_push.py index 91c77f9..1b10e83 100644 --- a/tests/api/v1/test_api_push.py +++ b/tests/api/v1/test_api_push.py @@ -104,19 +104,34 @@ def test_push_zipfile_encrypted( assert 'is encrypted' in rv_json['message'] -def test_push_koji_nvr(client): +def test_push_koji_nvr( + client, endpoint_push_koji, mocked_quay_io, mocked_op_courier_push, + auth_header, mocked_koji_archive_download): """Test REST API for pushing operators form koji by NVR""" - rv = client.post('/v1/organization-X/repo-Y/koji/nvr-Z') + rv = client.post( + endpoint_push_koji.url_path, + headers=auth_header + ) assert rv.status_code == 200 expected = { - 'organization': 'organization-X', - 'repo': 'repo-Y', - 'nvr': 'nvr-Z', - 'msg': 'Not Implemented. Testing only' + 'organization': endpoint_push_koji.org, + 'repo': endpoint_push_koji.repo, + 'version': endpoint_push_koji.version or constants.DEFAULT_RELEASE_VERSION, + 'nvr': endpoint_push_koji.nvr, + 'extracted_files': ['empty.yml'], } assert rv.get_json() == expected +def test_push_koji_unauthorized(client, endpoint_push_koji): + """Test if api properly refuses unauthorized requests""" + rv = client.post(endpoint_push_koji.url_path) + assert rv.status_code == requests.codes.forbidden, rv.get_json() + rv_json = rv.get_json() + assert rv_json['status'] == requests.codes.forbidden + assert rv_json['error'] == 'OMPSAuthorizationHeaderRequired' + + ZIP_ENDPOINT_NOVER = '/v1/organization-X/repo-Y/zipfile' @@ -124,6 +139,7 @@ def test_push_koji_nvr(client): ZIP_ENDPOINT_NOVER, '/v1/organization-X/repo-Y/zipfile/1.0.1', '/v1/organization-X/repo-Y/koji/nvr-Z', + '/v1/organization-X/repo-Y/koji/nvr-Z/1.0.1', ]) @pytest.mark.parametrize('method', [ 'GET', 'PATCH' 'PUT', 'HEAD', 'DELETE', 'TRACE', @@ -146,6 +162,7 @@ def test_method_not_allowed(client, endpoint, method): '/', '/v1' '/v1/organization-X/repo-Y/koji/', + '/v1/organization-X/repo-Y/koji/nvr-Z/version/extra-something', '/v1/organization-X/repo-Y/zipfile/version-Z/extra-something', ]) def test_404_for_mistyped_entrypoints(client, endpoint): diff --git a/tests/conftest.py b/tests/conftest.py index 3fc1c04..e5d7a00 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,6 +15,8 @@ import requests_mock from omps.app import app +from omps.koji_util import KOJI, KojiUtil +from omps.settings import Config, TestConfig @pytest.fixture() @@ -101,3 +103,29 @@ def mocked_op_courier_push(): yield finally: operatorcourier.api.build_verify_and_push = orig + + +@pytest.fixture +def mocked_koji_archive_download(valid_manifests_archive): + """Mock KojiUtil.koji_download_manifest_archive to return valid archive""" + def fake_download(nvr, target_fd): + with open(valid_manifests_archive, 'rb') as zf: + target_fd.write(zf.read()) + target_fd.flush() + + orig = KOJI.download_manifest_archive + try: + KOJI.download_manifest_archive = fake_download + yield + finally: + KOJI.download_manifest_archive = orig + + +@pytest.fixture +def mocked_koji(): + """Return mocked KojiUtil with session""" + conf = Config(TestConfig) + ku = KojiUtil() + ku.initialize(conf) + ku._session = flexmock() + return ku diff --git a/tests/test_koji_util.py b/tests/test_koji_util.py new file mode 100644 index 0000000..15aff62 --- /dev/null +++ b/tests/test_koji_util.py @@ -0,0 +1,108 @@ +# +# Copyright (C) 2019 Red Hat, Inc +# see the LICENSE file for license +# + +import tempfile + +import pytest +import requests +import requests_mock + +from omps.errors import ( + KojiNVRBuildNotFound, + KojiNotAnOperatorImage, + KojiManifestsArchiveNotFound, + KojiError, +) + + +class TestKojiUtil: + """Tests for KojiUtil class""" + + FILENAME = 'testfile.zip' + FILEPATH = 'path/to/archive.zip' + + def _mock_getBuild(self, mocked_koji, rdata=None): + if rdata is None: + meta = { + "extra": {'operator_manifests_archive': self.FILENAME}, + "build_id": 12345, + } + else: + meta = rdata + mocked_koji.session.should_receive('getBuild').and_return(meta) + + def _mock_getBuildLogs(self, mocked_koji, rdata=None): + if rdata is None: + logs = [ + { + 'name': self.FILENAME, + "path": self.FILEPATH, + }, + ] + else: + logs = rdata + + (mocked_koji.session + .should_receive('getBuildLogs') + .and_return(logs)) + + def test_download_manifest_archive_no_nvr(self, mocked_koji): + """Test if proper exception is raised when NVR is not found in koji""" + mocked_koji.session.should_receive('getBuild').and_return(None).once() + + with pytest.raises(KojiNVRBuildNotFound): + mocked_koji.download_manifest_archive('test', None) + + def test_download_manifest_archive_no_operator_img(self, mocked_koji): + """Test if proper exception is raised when build is not an operator + image""" + self._mock_getBuild(mocked_koji, rdata={"extra": {}}) + + with pytest.raises(KojiNotAnOperatorImage): + mocked_koji.download_manifest_archive('test', None) + + def test_download_manifest_archive_no_file(self, mocked_koji): + """Test if proper exception is raised when build miss the archive + file""" + self._mock_getBuild(mocked_koji) + logs = [ + { + 'name': 'something_else', + "path": "path/to/something_else.log" + }, + ] + self._mock_getBuildLogs(mocked_koji, rdata=logs) + + with pytest.raises(KojiManifestsArchiveNotFound): + mocked_koji.download_manifest_archive('test', None) + + def test_download_manifest_archive_download_error(self, mocked_koji): + """Test if proper exception is raised when download fails""" + self._mock_getBuild(mocked_koji) + self._mock_getBuildLogs(mocked_koji) + + with requests_mock.Mocker() as m: + m.get( + mocked_koji._kojiroot_url + self.FILEPATH, + status_code=requests.codes.not_found) + with pytest.raises(KojiError): + with tempfile.NamedTemporaryFile() as target_f: + mocked_koji.download_manifest_archive('test', target_f) + + def test_download_manifest_archive(self, mocked_koji): + """Positive test, everything should work now""" + self._mock_getBuild(mocked_koji) + self._mock_getBuildLogs(mocked_koji) + + data = b"I'm a zip archive!" + + with requests_mock.Mocker() as m: + m.get( + mocked_koji._kojiroot_url + self.FILEPATH, + content=data) + with tempfile.NamedTemporaryFile() as target_f: + mocked_koji.download_manifest_archive('test', target_f) + target_f.seek(0) + assert target_f.read() == data diff --git a/tests/test_settings.py b/tests/test_settings.py index b30a008..ef2ae02 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -26,6 +26,14 @@ "DEFAULT_RELEASE_VERSION", constants.DEFAULT_RELEASE_VERSION, ), + ( + "KOJIHUB_URL", + "https://koji.fedoraproject.org/kojihub" + ), + ( + "KOJIROOT_URL", + "https://kojipkgs.fedoraproject.org/" + ), )) def test_defaults(key, expected): """Test if defaults are properly propagated to app config""" diff --git a/tox.ini b/tox.ini index 14a35f9..0a992a5 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ deps = .[test] commands = pytest --cov=omps --ignore=tests/integration tests/ [coverage:report] -fail_under = 87 +fail_under = 90 [testenv:integration] basepython = python3