From 0d1a437c1044d866b245c8263a9daa3a3c3ff7f5 Mon Sep 17 00:00:00 2001 From: Gerrod Ubben Date: Fri, 14 May 2021 15:16:26 -0400 Subject: [PATCH] Refactor tests to use workflow generators [noissue] --- flake8.cfg | 3 +- .../tests/functional/api/test_auto_publish.py | 52 +-- .../functional/api/test_consume_content.py | 87 ++-- .../functional/api/test_crud_publications.py | 373 +++++------------- .../functional/api/test_download_content.py | 82 +--- pulp_python/tests/functional/utils.py | 222 ++++++++++- 6 files changed, 373 insertions(+), 446 deletions(-) diff --git a/flake8.cfg b/flake8.cfg index 29738a02e..7ae40bd69 100644 --- a/flake8.cfg +++ b/flake8.cfg @@ -1,6 +1,6 @@ [flake8] exclude = ./docs/*,*/migrations/* -ignore = Q000,Q003,D100,D104,D106,D200,D205,D400,D401,D402 +ignore = Q000,Q003,D100,D104,D106,D200,D205,D400,D401,D402,D413 max-line-length = 100 # Flake8-quotes extension codes @@ -15,3 +15,4 @@ max-line-length = 100 # D200: one-line docstring should fit on one line with quotes # D401: first line should be imperative (nitpicky) # D402: first line should not be the function’s “signature” (false positives) +# D413: missing blank line after last section diff --git a/pulp_python/tests/functional/api/test_auto_publish.py b/pulp_python/tests/functional/api/test_auto_publish.py index 5db677b71..cafd97c99 100644 --- a/pulp_python/tests/functional/api/test_auto_publish.py +++ b/pulp_python/tests/functional/api/test_auto_publish.py @@ -1,52 +1,26 @@ # coding=utf-8 """Tests automatic updating of publications and distributions.""" -import unittest - -from pulp_smash import config from pulp_smash.pulp3.bindings import monitor_task -from pulp_smash.pulp3.utils import gen_repo, gen_distribution, download_content_unit - -from pulp_python.tests.functional.utils import gen_python_client, gen_python_remote -from pulp_python.tests.functional.utils import set_up_module as setUpModule # noqa:F401 +from pulp_smash.pulp3.utils import download_content_unit -from pulpcore.client.pulp_python import ( - RepositoriesPythonApi, - RemotesPythonApi, - PublicationsPypiApi, - ContentPackagesApi, - DistributionsPypiApi, - RepositorySyncURL, +from pulp_python.tests.functional.utils import ( + cfg, + gen_python_remote, + TestCaseUsingBindings, + TestHelpersMixin ) +from pulp_python.tests.functional.utils import set_up_module as setUpModule # noqa:F401 +from pulpcore.client.pulp_python import RepositorySyncURL -class AutoPublishDistributeTestCase(unittest.TestCase): +class AutoPublishDistributeTestCase(TestCaseUsingBindings, TestHelpersMixin): """Test auto-publish and auto-distribution""" - @classmethod - def setUpClass(cls): - """Create class-wide variables.""" - cls.cfg = config.get_config() - cls.client = gen_python_client() - - cls.content_api = ContentPackagesApi(cls.client) - cls.repo_api = RepositoriesPythonApi(cls.client) - cls.remote_api = RemotesPythonApi(cls.client) - cls.publications_api = PublicationsPypiApi(cls.client) - cls.distributions_api = DistributionsPypiApi(cls.client) - def setUp(self): """Create remote, repo, publish settings, and distribution.""" self.remote = self.remote_api.create(gen_python_remote(policy="immediate")) - self.repo = self.repo_api.create(gen_repo(autopublish=True)) - response = self.distributions_api.create(gen_distribution(repository=self.repo.pulp_href)) - distribution_href = monitor_task(response.task).created_resources[0] - self.distribution = self.distributions_api.read(distribution_href) - - def tearDown(self): - """Clean up.""" - self.repo_api.delete(self.repo.pulp_href) - self.remote_api.delete(self.remote.pulp_href) - self.distributions_api.delete(self.distribution.pulp_href) + self.addCleanup(self.remote_api.delete, self.remote.pulp_href) + self.repo, self.distribution = self._create_empty_repo_and_distribution(autopublish=True) def test_01_sync(self): """Assert that syncing the repository triggers auto-publish and auto-distribution.""" @@ -61,7 +35,7 @@ def test_01_sync(self): # Check that all the appropriate resources were created self.assertGreater(len(task.created_resources), 1) self.assertEqual(self.publications_api.list().count, 1) - download_content_unit(self.cfg, self.distribution.to_dict(), "simple/") + download_content_unit(cfg, self.distribution.to_dict(), "simple/") # Sync the repository again. Since there should be no new repository version, there # should be no new publications or distributions either. @@ -86,4 +60,4 @@ def test_02_modify(self): # Check that all the appropriate resources were created self.assertGreater(len(task.created_resources), 1) self.assertEqual(self.publications_api.list().count, 1) - download_content_unit(self.cfg, self.distribution.to_dict(), "simple/") + download_content_unit(cfg, self.distribution.to_dict(), "simple/") diff --git a/pulp_python/tests/functional/api/test_consume_content.py b/pulp_python/tests/functional/api/test_consume_content.py index 001b205af..b9346b0aa 100644 --- a/pulp_python/tests/functional/api/test_consume_content.py +++ b/pulp_python/tests/functional/api/test_consume_content.py @@ -1,10 +1,8 @@ # coding=utf-8 """Tests that perform actions over content unit.""" -import unittest - from pulp_smash import cli from pulp_smash.pulp3.bindings import monitor_task -from pulp_smash.pulp3.utils import gen_repo, gen_distribution, delete_orphans +from pulp_smash.pulp3.utils import delete_orphans, modify_repo from pulp_python.tests.functional.constants import ( PYTHON_FIXTURE_URL, @@ -15,29 +13,19 @@ ) from pulp_python.tests.functional.utils import ( + cfg, gen_artifact, - gen_python_client, gen_python_content_attrs, - cfg, - publish, - gen_python_remote, + TestCaseUsingBindings, + TestHelpersMixin, ) from pulp_python.tests.functional.utils import set_up_module as setUpModule # noqa:F401 from urllib.parse import urljoin, urlsplit -from pulpcore.client.pulp_python import ( - RepositoriesPythonApi, - RemotesPythonApi, - PublicationsPypiApi, - ContentPackagesApi, - DistributionsPypiApi, - RepositorySyncURL, -) - from pulp_smash.utils import http_get -class PipInstallContentTestCase(unittest.TestCase): +class PipInstallContentTestCase(TestCaseUsingBindings, TestHelpersMixin): """ Verify whether content served by Pulp can be consumed through pip install. Workflows tested are: @@ -52,7 +40,7 @@ def setUpClass(cls): """ Check if packages to install through tests are already installed """ - cls.client = gen_python_client() + super().setUpClass() cls.cli_client = cli.Client(cfg) cls.PACKAGES = PYTHON_FIXTURES_PACKAGES cls.PACKAGES_URLS = [ @@ -67,36 +55,28 @@ def setUpClass(cls): cls.check_install(cls.cli_client, pkg), "{} is already installed".format(pkg), ) - cls.repo_api = RepositoriesPythonApi(cls.client) - cls.remote_api = RemotesPythonApi(cls.client) - cls.content_api = ContentPackagesApi(cls.client) - cls.publications_api = PublicationsPypiApi(cls.client) - cls.distro_api = DistributionsPypiApi(cls.client) def test_workflow_01(self): """ Verify workflow 1 """ - repo = self.repo_api.create(gen_repo()) - self.addCleanup(self.repo_api.delete, repo.pulp_href) - - artifacts = [] - for pkg in self.PACKAGES_URLS: - artifacts.append(gen_artifact(pkg)) - - for filename, artifact in zip(PYTHON_FIXTURES_FILENAMES, artifacts): + created_contents = [] + for pkg, filename in zip(self.PACKAGES_URLS, PYTHON_FIXTURES_FILENAMES): content_response = self.content_api.create( - **gen_python_content_attrs(artifact, filename) - ) - created_resources = monitor_task(content_response.task).created_resources - self.repo_api.modify( - repo.pulp_href, {"add_content_units": created_resources} + **gen_python_content_attrs(gen_artifact(pkg), filename) ) + created_contents.extend(monitor_task(content_response.task).created_resources) + created_contents = [self.content_api.read(href).to_dict() for href in created_contents] + + repo = self._create_repository() + # Add content + modify_repo(cfg, repo.to_dict(), add_units=created_contents) repo = self.repo_api.read(repo.pulp_href) - distribution = self.gen_pub_dist(repo) + pub = self._create_publication(repo) + distro = self._create_distribution_from_publication(pub) self.addCleanup(delete_orphans, cfg) - self.check_consume(distribution.to_dict()) + self.check_consume(distro.to_dict()) def test_workflow_02(self): """ @@ -112,20 +92,11 @@ def test_workflow_02(self): * `Pulp #4682 `_ * `Pulp #4677 `_ """ - repo = self.repo_api.create(gen_repo()) - self.addCleanup(self.repo_api.delete, repo.pulp_href) - - body = gen_python_remote(includes=PYTHON_LIST_PROJECT_SPECIFIER) - remote = self.remote_api.create(body) - self.addCleanup(self.remote_api.delete, remote.pulp_href) - - repository_sync_data = RepositorySyncURL(remote=remote.pulp_href) - sync_response = self.repo_api.sync(repo.pulp_href, repository_sync_data) - monitor_task(sync_response.task) - repo = self.repo_api.read(repo.pulp_href) - - distribution = self.gen_pub_dist(repo) - self.check_consume(distribution.to_dict()) + remote = self._create_remote(includes=PYTHON_LIST_PROJECT_SPECIFIER) + repo = self._create_repo_and_sync_with_remote(remote) + pub = self._create_publication(repo) + distro = self._create_distribution_from_publication(pub) + self.check_consume(distro.to_dict()) def check_consume(self, distribution): """Tests that pip packages hosted in a distribution can be consumed""" @@ -138,18 +109,6 @@ def check_consume(self, distribution): self.assertTrue(self.check_install(self.cli_client, pkg), out) self.addCleanup(self.uninstall, self.cli_client, pkg) - def gen_pub_dist(self, repo): - """Takes a repo and generates a publication and then distributes it""" - publication = publish(repo.to_dict()) - self.addCleanup(self.publications_api.delete, publication["pulp_href"]) - - body = gen_distribution() - body["publication"] = publication["pulp_href"] - distro_response = self.distro_api.create(body) - distro = self.distro_api.read(monitor_task(distro_response.task).created_resources[0]) - self.addCleanup(self.distro_api.delete, distro.pulp_href) - return distro - @staticmethod def check_install(cli_client, package): """Returns true if python package is installed, false otherwise""" diff --git a/pulp_python/tests/functional/api/test_crud_publications.py b/pulp_python/tests/functional/api/test_crud_publications.py index 31e3a853d..fb863da74 100644 --- a/pulp_python/tests/functional/api/test_crud_publications.py +++ b/pulp_python/tests/functional/api/test_crud_publications.py @@ -1,19 +1,14 @@ # coding=utf-8 """Tests that publish python plugin repositories.""" import random -import unittest from random import choice -from pulp_smash import config from pulp_smash.pulp3.bindings import monitor_task from pulp_smash.pulp3.utils import ( - gen_repo, get_content, get_versions, - modify_repo, - gen_distribution, + modify_repo ) -from pulp_smash.utils import http_get from urllib.parse import urljoin from pulp_python.tests.functional.constants import ( @@ -25,22 +20,18 @@ PYTHON_EGG_FILENAME, PYTHON_WHEEL_FILENAME, ) -from pulp_python.tests.functional.utils import gen_python_client, gen_python_remote +from pulp_python.tests.functional.utils import ( + cfg, + TestCaseUsingBindings, + TestHelpersMixin, + ensure_simple, +) from pulp_python.tests.functional.utils import set_up_module as setUpModule # noqa:F401 -from pulpcore.client.pulp_python import ( - PublicationsPypiApi, - RepositoriesPythonApi, - RepositorySyncURL, - RemotesPythonApi, - PythonPythonPublication, - DistributionsPypiApi, -) from pulpcore.client.pulp_python.exceptions import ApiException -from lxml import html -class PublishAnyRepoVersionTestCase(unittest.TestCase): +class PublishAnyRepoVersionTestCase(TestCaseUsingBindings, TestHelpersMixin): """Test whether a particular repository version can be published. This test targets the following issues: @@ -62,59 +53,30 @@ def test_all(self): 6. Assert that an exception is raised when providing two different repository versions to be published at same time. """ - cfg = config.get_config() - client = gen_python_client() - repo_api = RepositoriesPythonApi(client) - remote_api = RemotesPythonApi(client) - publications = PublicationsPypiApi(client) - - body = gen_python_remote() - remote = remote_api.create(body) - self.addCleanup(remote_api.delete, remote.pulp_href) - - repo = repo_api.create(gen_repo()) - self.addCleanup(repo_api.delete, repo.pulp_href) - - repository_sync_data = RepositorySyncURL(remote=remote.pulp_href) - sync_response = repo_api.sync(repo.pulp_href, repository_sync_data) - monitor_task(sync_response.task) - + remote = self._create_remote() + repo = self._create_repo_and_sync_with_remote(remote) # Step 1 - repo = repo_api.read(repo.pulp_href) for python_content in get_content(repo.to_dict())[PYTHON_CONTENT_NAME]: modify_repo(cfg, repo.to_dict(), add_units=[python_content]) + version_hrefs = tuple(ver["pulp_href"] for ver in get_versions(repo.to_dict())) non_latest = choice(version_hrefs[:-1]) - # Step 2 - publish_data = PythonPythonPublication(repository=repo.pulp_href) - publish_response = publications.create(publish_data) - created_resources = monitor_task(publish_response.task).created_resources - publication_href = created_resources[0] - self.addCleanup(publications.delete, publication_href) - publication = publications.read(publication_href) - - # Step 3 - self.assertEqual(publication.repository_version, version_hrefs[-1]) - - # Step 4 - publish_data.repository = None - publish_data.repository_version = non_latest - publish_response = publications.create(publish_data) - created_resources = monitor_task(publish_response.task).created_resources - publication_href = created_resources[0] - publication = publications.read(publication_href) - - # Step 5 - self.assertEqual(publication.repository_version, non_latest) + repo = self.repo_api.read(repo.pulp_href) + # Step 2, 3 + pub_latest = self._create_publication(repo) + self.assertEqual(pub_latest.repository_version, version_hrefs[-1]) + # Step 4, 5 + pub_version = self._create_publication(repo, version=non_latest) + self.assertEqual(pub_version.repository_version, non_latest) # Step 6 with self.assertRaises(ApiException): body = {"repository": repo.pulp_href, "repository_version": non_latest} - publications.create(body) + self.publications_api.create(body) -class PublishDifferentPolicyContent(unittest.TestCase): +class PublishDifferentPolicyContent(TestCaseUsingBindings, TestHelpersMixin): """Test whether a 'on_demand', 'immediate', or 'streamed' synced repository can be published. This test targets the following issues: @@ -122,14 +84,6 @@ class PublishDifferentPolicyContent(unittest.TestCase): * `Pulp #7128 `_ """ - @classmethod - def setUpClass(cls): - """Sets up APIs used by tests.""" - client = gen_python_client() - cls.repo_api = RepositoriesPythonApi(client) - cls.remote_api = RemotesPythonApi(client) - cls.pub_api = PublicationsPypiApi(client) - def test_on_demand(self): """Test whether a particular repository version can be published. @@ -138,46 +92,44 @@ def test_on_demand(self): 3. Sync 4. Publish repository """ - repo, _, pub = create_workflow(self.repo_api, self.pub_api, remote_api=self.remote_api, - body={"policy": "on_demand"}, cleanup=self.addCleanup) + remote = self._create_remote(policy="on_demand") + repo = self._create_repo_and_sync_with_remote(remote) + pub = self._create_publication(repo) self.assertEqual(pub.repository_version, repo.latest_version_href) def test_immediate(self): """Test if immediate synced content can be published.""" - repo, _, pub = create_workflow(self.repo_api, self.pub_api, remote_api=self.remote_api, - body={"policy": "immediate"}, cleanup=self.addCleanup) + remote = self._create_remote(policy="immediate") + repo = self._create_repo_and_sync_with_remote(remote) + pub = self._create_publication(repo) self.assertEqual(pub.repository_version, repo.latest_version_href) def test_streamed(self): """Test if streamed synced content can be published.""" - repo, _, pub = create_workflow(self.repo_api, self.pub_api, remote_api=self.remote_api, - body={"policy": "streamed"}, cleanup=self.addCleanup) + remote = self._create_remote(policy="streamed") + repo = self._create_repo_and_sync_with_remote(remote) + pub = self._create_publication(repo) self.assertEqual(pub.repository_version, repo.latest_version_href) def test_mixed(self): """Test if repository with mixed synced content can be published.""" # Sync on demand content - body = {"includes": PYTHON_SM_PROJECT_SPECIFIER} - repo, _, pub = create_workflow(self.repo_api, self.pub_api, remote_api=self.remote_api, - body=body, cleanup=self.addCleanup) + remote = self._create_remote(policy="on_demand", includes=PYTHON_SM_PROJECT_SPECIFIER) + repo = self._create_repo_and_sync_with_remote(remote) + pub = self._create_publication(repo) + # Sync immediate content + remote2 = self._create_remote(policy="immediate") + repo = self._sync_repo(repo, remote=remote2.pulp_href) + pub2 = self._create_publication(repo) - self.assertEqual(pub.repository_version, repo.latest_version_href) - # Add immediate content - body = {"policy": "immediate"} - remote = create_remote(self.remote_api, body=body, cleanup=self.addCleanup) - repository_sync_data = RepositorySyncURL(remote=remote.pulp_href) - sync_response = self.repo_api.sync(repo.pulp_href, repository_sync_data) - monitor_task(sync_response.task) - repo = self.repo_api.read(repo.pulp_href) - pub = create_publication(self.pub_api, repo, cleanup=self.addCleanup) - - self.assertEqual(pub.repository_version, repo.latest_version_href) + self.assertEqual(pub.repository_version, f"{repo.versions_href}1/") + self.assertEqual(pub2.repository_version, repo.latest_version_href) -class ListPublications(unittest.TestCase): +class ListPublications(TestCaseUsingBindings, TestHelpersMixin): """ This tests that publications can be listed. @@ -186,38 +138,32 @@ class ListPublications(unittest.TestCase): # TODO Make test RBAC specific when RBAC is implemented """ - @classmethod - def setUpClass(cls): - """Setup apis for all tests.""" - client = gen_python_client() - cls.repo_api = RepositoriesPythonApi(client) - cls.pub_api = PublicationsPypiApi(client) - cls.dis_api = DistributionsPypiApi(client) - def test_multiple_list(self): """Test listing multiple publications.""" - _, publication = create_workflow(self.repo_api, self.pub_api, cleanup=self.addCleanup) - _, publication2 = create_workflow(self.repo_api, self.pub_api, cleanup=self.addCleanup) + repo = self._create_repository() + publication = self._create_publication(repo) + publication2 = self._create_publication(repo) - publications = self.pub_api.list() + publications = self.publications_api.list() self.assertEqual(publications.results, [publication2, publication]) def test_none_list(self): """Test listing publications when empty.""" - publications = self.pub_api.list() + publications = self.publications_api.list() self.assertEqual(publications.count, 0) def test_publication_distribution(self): """Test that a publication properly lists it's connected distributions.""" - repo, pub, distro = create_workflow(self.repo_api, self.pub_api, - distro_api=self.dis_api, cleanup=self.addCleanup) - distro2 = create_distribution(self.dis_api, pub, cleanup=self.addCleanup) + repo = self._create_repository() + pub = self._create_publication(repo) + distro = self._create_distribution_from_publication(pub) + distro2 = self._create_distribution_from_publication(pub) - publication = self.pub_api.read(pub.pulp_href) + publication = self.publications_api.read(pub.pulp_href) self.assertEqual(publication.distributions, [distro.pulp_href, distro2.pulp_href]) -class DeletePublications(unittest.TestCase): +class DeletePublications(TestCaseUsingBindings, TestHelpersMixin): """ These tests are dedicated to making sure that deleting works correctly. @@ -225,53 +171,44 @@ class DeletePublications(unittest.TestCase): Deleted repositories should remove publications """ - @classmethod - def setUpClass(cls): - """Setup apis for all tests.""" - client = gen_python_client() - cls.repo_api = RepositoriesPythonApi(client) - cls.pub_api = PublicationsPypiApi(client) - cls.dis_api = DistributionsPypiApi(client) - def test_basic_delete(self): """Checks that deleting decreases publication count.""" - publications = self.pub_api.list() + publications = self.publications_api.list() current_length = publications.count - repo, publication = create_workflow(self.repo_api, self.pub_api) - self.addCleanup(self.repo_api.delete, repo.pulp_href) + repo = self._create_repository() + publication = self._create_publication(repo, cleanup=False) - publications = self.pub_api.list() + publications = self.publications_api.list() self.assertEqual(publications.count, 1 + current_length) - self.pub_api.delete(publication.pulp_href) - publications = self.pub_api.list() + self.publications_api.delete(publication.pulp_href) + publications = self.publications_api.list() self.assertEqual(publications.count, current_length) def test_distro_removed_on_delete(self): """Test that distributions no longer point to a publication after deleted.""" - repo, publication, distro = create_workflow( - self.repo_api, self.pub_api, distro_api=self.dis_api - ) - self.addCleanup(self.repo_api.delete, repo.pulp_href) - self.addCleanup(self.dis_api.delete, distro.pulp_href) + repo = self._create_repository() + publication = self._create_publication(repo, cleanup=False) + distro = self._create_distribution_from_publication(publication) self.assertEqual(distro.publication, publication.pulp_href) - self.pub_api.delete(publication.pulp_href) - distro = self.dis_api.read(distro.pulp_href) + self.publications_api.delete(publication.pulp_href) + distro = self.distributions_api.read(distro.pulp_href) self.assertEqual(distro.publication, None) def test_repository_delete(self): """Tests that deleting the repository of a publication deletes the publication.""" - repo, publication = create_workflow(self.repo_api, self.pub_api) + repo = self._create_repository(cleanup=False) + publication = self._create_publication(repo, cleanup=False) monitor_task(self.repo_api.delete(repo.pulp_href).task) with self.assertRaises(ApiException): - self.pub_api.read(publication.pulp_href) + self.publications_api.read(publication.pulp_href) -class PublishedCorrectContent(unittest.TestCase): +class PublishedCorrectContent(TestCaseUsingBindings, TestHelpersMixin): """ These tests ensure that publications only serve content currently in the repository version. @@ -280,178 +217,66 @@ class PublishedCorrectContent(unittest.TestCase): * `Pulp #362 `_ """ - @staticmethod - def ensure_simple(base_path, packages, sha_digests=None): - """ - Tests that the simple api at `base_path` matches the packages supplied. - - `packages`: dictionary of form {package_name: [release_filenames]} - - First check `/simple/index.html` has each package name, no more, no less - Second check `/simple/package_name/index.html` for each package exists - Third check each package's index has all their releases listed, no more, no less - - Returns tuple (`proper`: bool, `error_msg`: str) - - *Technically, if there was a bug, other packages' indexes could be posted, but not present - in the simple index and thus be accessible from the distribution, but if one can't see it - how would one know that it's there?* - """ - def explore_links(page_url, page_name, links_found): - legit_found_links = [] - page = html.fromstring(http_get(page_url)) - page_links = page.xpath("/html/body/a") - for link in page_links: - if link.text in links_found: - if links_found[link.text]: - msgs += f"\nDuplicate {page_name} name {link.text}" # noqa: F823 - links_found[link.text] = True - if link.get("href"): - legit_found_links.append(link.get("href")) - else: - msgs += f"\nFound {page_name} link without href {link.text}" # noqa: F823 - else: - msgs += f"\nFound extra {page_name} link {link.text}" # noqa: F823 - return legit_found_links - - packages_found = {name: False for name in packages.keys()} - releases_found = {name: False for releases in packages.values() for name in releases} - msgs = "" - simple_url = urljoin(PULP_CONTENT_BASE_URL, f"{base_path}/simple/") - found_release_links = explore_links(simple_url, "simple", packages_found) - dl_links = [] - for release_link in found_release_links: - dl_links += explore_links(urljoin(simple_url, release_link), "release", releases_found) - for dl_link in dl_links: - package_link, _, sha = dl_link.partition("#sha256=") - if len(sha) != 64: - msgs += f"\nRelease download link has bad sha256 {dl_link}" - if sha_digests: - package = package_link.split("/")[-1] - if sha_digests[package] != sha: - msgs += f"\nRelease has bad sha256 attached to it {package}" - msgs += "".join(map(lambda x: f"\nSimple link not found for {x}", - [name for name, val in packages_found.items() if not val])) - msgs += "".join(map(lambda x: f"\nReleases link not found for {x}", - [name for name, val in releases_found.items() if not val])) - return len(msgs) == 0, msgs - @classmethod def setUpClass(cls): - """Sets up the apis for the tests.""" - client = gen_python_client() - cls.repo_api = RepositoriesPythonApi(client) - cls.rem_api = RemotesPythonApi(client) - cls.pub_api = PublicationsPypiApi(client) - cls.dis_api = DistributionsPypiApi(client) + """Sets up the class variables""" + super().setUpClass() cls.releases = [PYTHON_EGG_FILENAME, PYTHON_WHEEL_FILENAME] def test_all_content_published(self): """Publishes SM Project and ensures correctness of simple api.""" - body = {"includes": PYTHON_SM_PROJECT_SPECIFIER} - _, _, _, distro = create_workflow(self.repo_api, self.pub_api, self.rem_api, - body, self.dis_api, self.addCleanup) - - proper, msgs = self.ensure_simple(distro.base_path, PYTHON_SM_FIXTURE_RELEASES, - sha_digests=PYTHON_SM_FIXTURE_CHECKSUMS) + remote = self._create_remote(includes=PYTHON_SM_PROJECT_SPECIFIER) + repo = self._create_repo_and_sync_with_remote(remote) + pub = self._create_publication(repo) + distro = self._create_distribution_from_publication(pub) + + url = urljoin(PULP_CONTENT_BASE_URL, f"{distro.base_path}/simple/") + proper, msgs = ensure_simple(url, PYTHON_SM_FIXTURE_RELEASES, + sha_digests=PYTHON_SM_FIXTURE_CHECKSUMS) self.assertTrue(proper, msg=msgs) def test_removed_content_not_published(self): """Ensure content removed from a repository doesn't get published again.""" - repo, _, _, distro = create_workflow(self.repo_api, self.pub_api, remote_api=self.rem_api, - distro_api=self.dis_api, cleanup=self.addCleanup) + remote = self._create_remote() + repo = self._create_repo_and_sync_with_remote(remote) + pub = self._create_publication(repo) + distro = self._create_distribution_from_publication(pub) - proper, msgs = self.ensure_simple(distro.base_path, {"shelf-reader": self.releases}) + url = urljoin(PULP_CONTENT_BASE_URL, f"{distro.base_path}/simple/") + proper, msgs = ensure_simple(url, {"shelf-reader": self.releases}) self.assertTrue(proper, msg=msgs) - cfg = config.get_config() removed_content = random.choice(get_content(repo.to_dict())[PYTHON_CONTENT_NAME]) - modify_repo(cfg, repo.to_dict(), remove_units=[removed_content]) - - publication = create_publication(self.pub_api, repo, cleanup=self.addCleanup) - distro = create_distribution(self.dis_api, publication, cleanup=self.addCleanup) + modify_repo(cfg, repo.to_dict(), add_units=[removed_content]) + repo = self.repo_api.read(repo.pulp_href) + pub = self._create_publication(repo) + distro = self._create_distribution_from_publication(pub) if removed_content["filename"] == PYTHON_WHEEL_FILENAME: remaining_release = [PYTHON_EGG_FILENAME] else: remaining_release = [PYTHON_WHEEL_FILENAME] - proper, msgs = self.ensure_simple(distro.base_path, {"shelf-reader": remaining_release}) + url = urljoin(PULP_CONTENT_BASE_URL, f"{distro.base_path}/simple/") + proper, msgs = ensure_simple(url, {"shelf-reader": remaining_release}) self.assertTrue(proper, msg=msgs) def test_new_content_is_published(self): """Ensures added content is published with a new publication.""" - body = {"package_types": ["sdist"]} - repo, _, _, distro = create_workflow(self.repo_api, self.pub_api, self.rem_api, - body, self.dis_api, self.addCleanup) + remote = self._create_remote(package_types=["sdist"]) + repo = self._create_repo_and_sync_with_remote(remote) + pub = self._create_publication(repo) + distro = self._create_distribution_from_publication(pub) - proper, m = self.ensure_simple(distro.base_path, {"shelf-reader": [PYTHON_EGG_FILENAME]}) + url = urljoin(PULP_CONTENT_BASE_URL, f"{distro.base_path}/simple/") + proper, m = ensure_simple(url, {"shelf-reader": [PYTHON_EGG_FILENAME]}) self.assertTrue(proper, msg=m) - remote = create_remote(self.rem_api, cleanup=self.addCleanup) - repository_sync_data = RepositorySyncURL(remote.pulp_href) - sync_response = self.repo_api.sync(repo.pulp_href, repository_sync_data) - monitor_task(sync_response.task) - - publication = create_publication(self.pub_api, repo, cleanup=self.addCleanup) - distro = create_distribution(self.dis_api, publication, cleanup=self.addCleanup) + remote = self._create_remote() + repo = self._sync_repo(repo, remote=remote.pulp_href) + pub = self._create_publication(repo) + distro = self._create_distribution_from_publication(pub) - proper, msgs = self.ensure_simple(distro.base_path, {"shelf-reader": self.releases}) + url = urljoin(PULP_CONTENT_BASE_URL, f"{distro.base_path}/simple/") + proper, msgs = ensure_simple(url, {"shelf-reader": self.releases}) self.assertTrue(proper, msg=msgs) - - -def create_repository(repo_api, cleanup=None): - """Creates a new python repository.""" - repo = repo_api.create(gen_repo()) - if cleanup: - cleanup(repo_api.delete, repo.pulp_href) - return repo - - -def create_remote(remote_api, body={}, cleanup=None): - """Creates a new python remote.""" - remote = remote_api.create(gen_python_remote(**body)) - if cleanup: - cleanup(remote_api.delete, remote.pulp_href) - return remote - - -def create_publication(pub_api, repository, cleanup=None): - """Creates a new python publication.""" - publish_data = PythonPythonPublication(repository=repository.pulp_href) - publish_response = pub_api.create(publish_data) - publication = pub_api.read(monitor_task(publish_response.task).created_resources[0]) - if cleanup: - cleanup(pub_api.delete, publication.pulp_href) - return publication - - -def create_distribution(dis_api, publication, cleanup=None): - """Creates a new python distribution.""" - dis_body = gen_distribution(publication=publication.pulp_href) - distro_response = dis_api.create(dis_body) - distro = dis_api.read(monitor_task(distro_response.task).created_resources[0]) - if cleanup: - cleanup(dis_api.delete, distro.pulp_href) - return distro - - -def create_workflow(repo_api, pub_api, remote_api=None, body={}, distro_api=None, cleanup=None): - """Creates repository, publication, and potentially remote and distribution if specified.""" - created_objects = [] - repo = create_repository(repo_api, cleanup=cleanup) - created_objects.append(repo) - if remote_api: - remote = create_remote(remote_api, body=body, cleanup=cleanup) - repository_sync_data = RepositorySyncURL(remote=remote.pulp_href) - sync_response = repo_api.sync(repo.pulp_href, repository_sync_data) - monitor_task(sync_response.task) - repo = repo_api.read(repo.pulp_href) - created_objects[0] = repo - created_objects.append(remote) - publication = create_publication(pub_api, repo, cleanup=cleanup) - created_objects.append(publication) - if distro_api: - distro = create_distribution(distro_api, publication, cleanup=cleanup) - created_objects.append(distro) - return created_objects diff --git a/pulp_python/tests/functional/api/test_download_content.py b/pulp_python/tests/functional/api/test_download_content.py index 1ff7663d7..e61841cfd 100644 --- a/pulp_python/tests/functional/api/test_download_content.py +++ b/pulp_python/tests/functional/api/test_download_content.py @@ -7,7 +7,7 @@ from random import choice from urllib.parse import urljoin -from pulp_smash import config, utils +from pulp_smash import utils from pulp_smash.pulp3.bindings import monitor_task from pulp_smash.pulp3.utils import ( download_content_unit, @@ -22,27 +22,22 @@ PYTHON_LG_PROJECT_SPECIFIER, ) from pulp_python.tests.functional.utils import ( - gen_python_client, + cfg, get_python_content_paths, gen_python_remote, publish, + TestCaseUsingBindings, + TestHelpersMixin, ) from pulp_python.tests.functional.utils import set_up_module as setUpModule # noqa:F401 -from pulpcore.client.pulp_python import ( - DistributionsPypiApi, - PublicationsPypiApi, - RepositoriesPythonApi, - RepositorySyncURL, - RemotesPythonApi, - PythonPythonPublication, -) +from pulpcore.client.pulp_python import RepositorySyncURL PYPI_LAST_SERIAL = "X-PYPI-LAST-SERIAL" PYPI_SERIAL_CONSTANT = 1000000000 -class DownloadContentTestCase(unittest.TestCase): +class DownloadContentTestCase(TestCaseUsingBindings, TestHelpersMixin): """Verify whether content served by pulp can be downloaded.""" def test_all(self): @@ -71,40 +66,10 @@ def test_all(self): * `Pulp #2895 `_ * `Pulp Smash #872 `_ """ - client = gen_python_client() - repo_api = RepositoriesPythonApi(client) - remote_api = RemotesPythonApi(client) - publications = PublicationsPypiApi(client) - distributions = DistributionsPypiApi(client) - - repo = repo_api.create(gen_repo()) - self.addCleanup(repo_api.delete, repo.pulp_href) - - body = gen_python_remote() - remote = remote_api.create(body) - self.addCleanup(remote_api.delete, remote.pulp_href) - - # Sync a Repository - repository_sync_data = RepositorySyncURL(remote=remote.pulp_href) - sync_response = repo_api.sync(repo.pulp_href, repository_sync_data) - monitor_task(sync_response.task) - repo = repo_api.read(repo.pulp_href) - - # Create a publication. - publish_data = PythonPythonPublication(repository=repo.pulp_href) - publish_response = publications.create(publish_data) - created_resources = monitor_task(publish_response.task).created_resources - publication_href = created_resources[0] - self.addCleanup(publications.delete, publication_href) - - # Create a distribution. - body = gen_distribution() - body["publication"] = publication_href - distribution_response = distributions.create(body) - created_resources = monitor_task(distribution_response.task).created_resources - distribution = distributions.read(created_resources[0]) - self.addCleanup(distributions.delete, distribution.pulp_href) - + remote = self._create_remote() + repo = self._create_repo_and_sync_with_remote(remote) + pub = self._create_publication(repo) + distro = self._create_distribution_from_publication(pub) # Pick a content unit (of each type), and download it from both Pulp Fixtures… unit_paths = [ choice(paths) for paths in get_python_content_paths(repo.to_dict()).values() @@ -120,15 +85,14 @@ def test_all(self): # …and Pulp. pulp_hashes = [] - cfg = config.get_config() for unit_path in unit_paths: - content = download_content_unit(cfg, distribution.to_dict(), unit_path[1]) + content = download_content_unit(cfg, distro.to_dict(), unit_path[1]) pulp_hashes.append(hashlib.sha256(content).hexdigest()) self.assertEqual(fixtures_hashes, pulp_hashes) -class PublishPyPiJSON(unittest.TestCase): +class PublishPyPiJSON(TestCaseUsingBindings): """Test whether a distributed Python repository has a PyPi json endpoint a.k.a Can be consumed by another Pulp instance @@ -137,16 +101,6 @@ class PublishPyPiJSON(unittest.TestCase): * `Pulp #2886 `_ """ - @classmethod - def setUpClass(cls): - """Sets up the class""" - client = gen_python_client() - cls.cfg = config.get_config() - cls.repo_api = RepositoriesPythonApi(client) - cls.remote_api = RemotesPythonApi(client) - cls.publications_api = PublicationsPypiApi(client) - cls.distro_api = DistributionsPypiApi(client) - @classmethod def setUp(cls): """Sets up the repo before every test""" @@ -166,7 +120,7 @@ def test_pypi_json(self): self.addCleanup(self.remote_api.delete, self.remote.pulp_href) distro = self.gen_pub_dist() rel_url = "pypi/shelf-reader/json" - package_json = download_content_unit(self.cfg, distro.to_dict(), rel_url) + package_json = download_content_unit(cfg, distro.to_dict(), rel_url) package = json.loads(package_json) self.assertEqual(SHELF_PYTHON_JSON["last_serial"], package["last_serial"]) self.assertTrue(SHELF_PYTHON_JSON["info"].items() <= package["info"].items()) @@ -206,7 +160,7 @@ def test_pypi_last_serial(self): distro = self.gen_pub_dist().to_dict() rel_url = "pypi/shelf-reader/json" url_fragments = [ - self.cfg.get_content_host_base_url(), + cfg.get_content_host_base_url(), "pulp/content", distro["base_path"], rel_url, @@ -230,7 +184,7 @@ def test_basic_pulp_to_pulp_sync(self): self.assertEqual(get_content_summary(self.repo.to_dict()), PYTHON_LG_FIXTURE_SUMMARY) distro = self.gen_pub_dist().to_dict() url_fragments = [ - self.cfg.get_content_host_base_url(), + cfg.get_content_host_base_url(), "pulp/content", distro["base_path"], "" @@ -269,7 +223,7 @@ def gen_pub_dist(self): body = gen_distribution() body["publication"] = publication["pulp_href"] - distro_response = self.distro_api.create(body) - distro = self.distro_api.read(monitor_task(distro_response.task).created_resources[0]) - self.addCleanup(self.distro_api.delete, distro.pulp_href) + response = self.distributions_api.create(body) + distro = self.distributions_api.read(monitor_task(response.task).created_resources[0]) + self.addCleanup(self.distributions_api.delete, distro.pulp_href) return distro diff --git a/pulp_python/tests/functional/utils.py b/pulp_python/tests/functional/utils.py index c645d2705..9fd54e8c9 100644 --- a/pulp_python/tests/functional/utils.py +++ b/pulp_python/tests/functional/utils.py @@ -1,13 +1,16 @@ # coding=utf-8 """Utilities for tests for the python plugin.""" from functools import partial -import requests -from unittest import SkipTest +from unittest import SkipTest, TestCase from tempfile import NamedTemporaryFile +from urllib.parse import urljoin +from lxml import html from pulp_smash import config, selectors +from pulp_smash.utils import http_get from pulp_smash.pulp3.bindings import monitor_task from pulp_smash.pulp3.utils import ( + gen_distribution, gen_remote, gen_repo, get_content, @@ -30,6 +33,7 @@ ) from pulpcore.client.pulp_python import ApiClient as PythonApiClient from pulpcore.client.pulp_python import ( + DistributionsPypiApi, RepositoriesPythonApi, ContentPackagesApi, PublicationsPypiApi, @@ -108,6 +112,7 @@ def gen_python_content_attrs(artifact, filename=PYTHON_EGG_FILENAME): repo_api = RepositoriesPythonApi(py_client) remote_api = RemotesPythonApi(py_client) pub_api = PublicationsPypiApi(py_client) +distributions_api = DistributionsPypiApi(py_client) content_api = ContentPackagesApi(py_client) @@ -163,9 +168,218 @@ def publish(repo, version_href=None): def gen_artifact(url=PYTHON_URL): """Creates an artifact.""" - response = requests.get(url) with NamedTemporaryFile() as temp_file: - temp_file.write(response.content) + temp_file.write(http_get(url)) temp_file.flush() artifact = ArtifactsApi(core_client).create(file=temp_file.name) return artifact.to_dict() + + +class TestCaseUsingBindings(TestCase): + """A parent TestCase that instantiates the various bindings used throughout tests.""" + + @classmethod + def setUpClass(cls): + """Create class-wide variables.""" + cls.client = py_client + cls.core_client = core_client + cls.repo_api = repo_api + cls.remote_api = remote_api + cls.publications_api = pub_api + cls.distributions_api = distributions_api + cls.content_api = content_api + + +class TestHelpersMixin: + """A common place for sync helper functions.""" + + def _create_repository(self, cleanup=True, **kwargs): + """Create `PythonRepository`""" + repo = self.repo_api.create(gen_repo(**kwargs)) + if cleanup: + self.addCleanup(self.repo_api.delete, repo.pulp_href) + return repo + + def _create_remote(self, cleanup=True, **kwargs): + """Create `PythonRemote`.""" + remote = self.remote_api.create(gen_python_remote(**kwargs)) + if cleanup: + self.addCleanup(self.remote_api.delete, remote.pulp_href) + return remote + + def _create_repo_and_sync_with_remote(self, remote, **kwargs): + """ + Create a repository and then sync with the provided `remote`. + + Args: + remote: The remote to be sync with + + Returns: + repository: The created repository object to be asserted to. + """ + # Create the repository. + repo = self.repo_api.create(gen_repo(remote=remote.pulp_href)) + self.addCleanup(self.repo_api.delete, repo.pulp_href) + return self._sync_repo(repo, remote=remote.pulp_href, **kwargs) + + def _create_repo_with_attached_remote_and_sync(self, remote, **kwargs): + """ + Create a repository with the remote attached, and then sync without specifying the `remote`. + + Args: + remote: The remote to attach to the repository + + Returns: + repository: The created repository object to be asserted to. + """ + # Create the repository. + repo = self.repo_api.create(gen_repo(remote=remote.pulp_href)) + self.addCleanup(self.repo_api.delete, repo.pulp_href) + return self._sync_repo(repo, **kwargs) + + def _sync_repo(self, repo, **kwargs): + """ + Sync the repo with optional `kwarg` parameters passed on to the sync method. + + Args: + repo: The repository to sync + + Returns: + repository: The updated repository after the sync is complete + """ + repository_sync_data = RepositorySyncURL(**kwargs) + sync_response = self.repo_api.sync(repo.pulp_href, repository_sync_data) + monitor_task(sync_response.task) + repo = self.repo_api.read(repo.pulp_href) + return repo + + def _create_distribution(self, cleanup=True, **kwargs): + """Create a `PythonDistribution`.""" + body = gen_distribution(**kwargs) + distribution_create = self.distributions_api.create(body) + created_resources = monitor_task(distribution_create.task).created_resources + distribution = self.distributions_api.read(created_resources[0]) + if cleanup: + self.addCleanup(self.distributions_api.delete, distribution.pulp_href) + return distribution + + def _create_distribution_from_repo(self, repo, cleanup=True): + """ + Create a `PythonDistribution` serving the `repo`. + + Args: + repo: The repository to serve with the `PythonDistribution` + cleanup: Whether the distribution should be cleaned up + + Returns: + The created `PythonDistribution`. + """ + # Create a distribution. + return self._create_distribution(cleanup, repository=repo.pulp_href) + + def _create_empty_repo_and_distribution(self, cleanup=True, **repo_kwargs): + """ + Creates an empty `PythonRepository` and an `PythonDistribution` serving that repository. + + Args: + cleanup: Whether the repository and distribution should be cleaned up + + Returns: + Tuple of the created `PythonRepository`, `PythonDistribution` + """ + repo = self.repo_api.create(gen_repo(**repo_kwargs)) + if cleanup: + self.addCleanup(self.repo_api.delete, repo.pulp_href) + return repo, self._create_distribution_from_repo(repo, cleanup=cleanup) + + def _create_publication(self, repository, version=None, cleanup=True): + """ + Creates a `PythonPublication` from the `PythonRepository`. + + Args: + repository: The repository to use for the publication + version: Optional - The version to repo_version to use, either an int or href + cleanup: whether to cleanup the publication + + Returns: + The created `PythonPublication` + """ + if version is not None: + try: + rv_href = f"{repository.versions_href}{int(version)}/" + except ValueError: + rv_href = version + publish_data = PythonPythonPublication(repository_version=rv_href) + else: + publish_data = PythonPythonPublication(repository=repository.pulp_href) + publish_response = self.publications_api.create(publish_data) + pub = self.publications_api.read(monitor_task(publish_response.task).created_resources[0]) + if cleanup: + self.addCleanup(self.publications_api.delete, pub.pulp_href) + return pub + + def _create_distribution_from_publication(self, pub, cleanup=True): + """ + Create an `PythonDistribution` serving the `pub`. + + Args: + pub: The publication to serve with the `PythonDistribution` + repo: Optional repo to create the publication from if pub is not set + version: Optional version to create the publication from if pub is not set + cleanup: Whether the distribution should be cleaned up + + Returns: + The created `PythonDistribution`. + """ + return self._create_distribution(cleanup, publication=pub.pulp_href) + + +def ensure_simple(simple_url, packages, sha_digests=None): + """ + Tests that the simple api at `url` matches the packages supplied. + `packages`: dictionary of form {package_name: [release_filenames]} + First check `/simple/index.html` has each package name, no more, no less + Second check `/simple/package_name/index.html` for each package exists + Third check each package's index has all their releases listed, no more, no less + Returns tuple (`proper`: bool, `error_msg`: str) + *Technically, if there was a bug, other packages' indexes could be posted, but not present + in the simple index and thus be accessible from the distribution, but if one can't see it + how would one know that it's there?* + """ + def explore_links(page_url, page_name, links_found, msgs): + legit_found_links = [] + page = html.fromstring(http_get(page_url)) + page_links = page.xpath("/html/body/a") + for link in page_links: + if link.text in links_found: + if links_found[link.text]: + msgs += f"\nDuplicate {page_name} name {link.text}" + links_found[link.text] = True + if link.get("href"): + legit_found_links.append(link.get("href")) + else: + msgs += f"\nFound {page_name} link without href {link.text}" + else: + msgs += f"\nFound extra {page_name} link {link.text}" + return legit_found_links + + packages_found = {name: False for name in packages.keys()} + releases_found = {name: False for releases in packages.values() for name in releases} + msgs = "" + found_release_links = explore_links(simple_url, "simple", packages_found, msgs) + dl_links = [] + for rel_link in found_release_links: + dl_links += explore_links(urljoin(simple_url, rel_link), "release", releases_found, msgs) + for dl_link in dl_links: + package_link, _, sha = dl_link.partition("#sha256=") + if len(sha) != 64: + msgs += f"\nRelease download link has bad sha256 {dl_link}" + if sha_digests: + package = package_link.split("/")[-1] + if sha_digests[package] != sha: + msgs += f"\nRelease has bad sha256 attached to it {package}" + msgs += "".join(map(lambda x: f"\nSimple link not found for {x}", + [name for name, val in packages_found.items() if not val])) + msgs += "".join(map(lambda x: f"\nReleases link not found for {x}", + [name for name, val in releases_found.items() if not val])) + return len(msgs) == 0, msgs