From 6385d465d730e06bb2f26badc2718fa0f2422324 Mon Sep 17 00:00:00 2001 From: jobrauer Date: Sep 21 2020 10:51:36 +0000 Subject: Add test_submit_module_build --- diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 999d255..7c3e4d8 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -33,7 +33,7 @@ def pkg_util(test_env): """Fixture to interact with the packaging utility :return: Packaging utility configured for the tests - :rtype: object of utils.PackagingUtility + :rtype: utils.PackagingUtility """ return utils.PackagingUtility(test_env["packaging_utility"], test_env["mbs_api"]) @@ -99,10 +99,7 @@ def clone_and_start_build(repo, pkg_util): builds = pkg_util.run("--optional", "rebuild_strategy=all") yield repo, builds for build in builds: - try: - pkg_util.cancel(build) - except sh.ErrorReturnCode: - pass # we don't need to bother with clean-up errors + pkg_util.cancel(build, ignore_errors=True) @pytest.fixture(scope="function") diff --git a/tests/integration/example.test.env.yaml b/tests/integration/example.test.env.yaml index 37d3777..1ea2549 100644 --- a/tests/integration/example.test.env.yaml +++ b/tests/integration/example.test.env.yaml @@ -136,3 +136,6 @@ testdata: static_context: module: testmodule branch: test-static-context + rest_submit_module_build: + module: testmodule + branch: test-submit-module diff --git a/tests/integration/test_rest_api.py b/tests/integration/test_rest_api.py deleted file mode 100644 index 59e5d26..0000000 --- a/tests/integration/test_rest_api.py +++ /dev/null @@ -1,45 +0,0 @@ -import getpass - - -def test_rest_module_build(clone_and_start_build, mbs, koji, pkg_util): - """Start a module build and query MBS API for build details (?verbose=True). - Assert as many values as possible. - - * There is no need for exhaustive testing of the REST API here, - as extensive coverage of it is already implemented in tests/test_web/test_views.py. - """ - repo, builds = clone_and_start_build - assert len(builds) >= 1 - - username = getpass.getuser() - build_id = builds[0].id - giturl = pkg_util.giturl() - - # wait until 'build' state - mbs.wait_for_module_build(build_id, lambda bld: bld.get("state") == 2, timeout=200) - - build = mbs.get_module_build(build_id) - assert build.get("id") == build_id - assert build.get("owner") == username - assert build.get("rebuild_strategy") == "all" - # additional '?' in url for for branch parameter (packaging utility prints url without it) - assert build.get("scmurl").replace("?", "") == giturl - assert build.get("state_name") == "build" - assert build.get("name") == repo.module_name - assert build.get("stream") == repo.branch - assert not build.get("scratch") - - assert build.get("tasks") - if repo.modulemd["data"]["components"]: - if repo.modulemd["data"]["components"].get("rpms"): - rpms = [k for k in repo.modulemd["data"]["components"]["rpms"]] - rpms.append("module-build-macros") - assert len(rpms) == len(build.get('component_builds')) - for rpm in rpms: - assert build.get("tasks")["rpms"][rpm] - else: - assert not build.get('component_builds') - - assert type(build.get("id")) is int - assert build.get("name") - assert not build.get("time_completed") diff --git a/tests/integration/test_rest_build_state.py b/tests/integration/test_rest_build_state.py new file mode 100644 index 0000000..59e5d26 --- /dev/null +++ b/tests/integration/test_rest_build_state.py @@ -0,0 +1,45 @@ +import getpass + + +def test_rest_module_build(clone_and_start_build, mbs, koji, pkg_util): + """Start a module build and query MBS API for build details (?verbose=True). + Assert as many values as possible. + + * There is no need for exhaustive testing of the REST API here, + as extensive coverage of it is already implemented in tests/test_web/test_views.py. + """ + repo, builds = clone_and_start_build + assert len(builds) >= 1 + + username = getpass.getuser() + build_id = builds[0].id + giturl = pkg_util.giturl() + + # wait until 'build' state + mbs.wait_for_module_build(build_id, lambda bld: bld.get("state") == 2, timeout=200) + + build = mbs.get_module_build(build_id) + assert build.get("id") == build_id + assert build.get("owner") == username + assert build.get("rebuild_strategy") == "all" + # additional '?' in url for for branch parameter (packaging utility prints url without it) + assert build.get("scmurl").replace("?", "") == giturl + assert build.get("state_name") == "build" + assert build.get("name") == repo.module_name + assert build.get("stream") == repo.branch + assert not build.get("scratch") + + assert build.get("tasks") + if repo.modulemd["data"]["components"]: + if repo.modulemd["data"]["components"].get("rpms"): + rpms = [k for k in repo.modulemd["data"]["components"]["rpms"]] + rpms.append("module-build-macros") + assert len(rpms) == len(build.get('component_builds')) + for rpm in rpms: + assert build.get("tasks")["rpms"][rpm] + else: + assert not build.get('component_builds') + + assert type(build.get("id")) is int + assert build.get("name") + assert not build.get("time_completed") diff --git a/tests/integration/test_rest_submit_build.py b/tests/integration/test_rest_submit_build.py new file mode 100644 index 0000000..43275f8 --- /dev/null +++ b/tests/integration/test_rest_submit_build.py @@ -0,0 +1,48 @@ +from requests import HTTPError + + +def assert_build_in_build_state(mbs, build): + """Assert build state was reached and then cancel the build using REST.""" + try: + mbs.wait_for_module_build(build, lambda bld: bld.get("state") == 2, timeout=10) + finally: + mbs.cancel_module_build(build.id) + + +def test_rest_submit_module_build(pkg_util, scenario, repo, mbs): + """Test module build submission. Tests only whether or not + build gets accepted and transitions successfully to the build state. + + Two variants: + * submit module build with modulemd yaml (test YAMLFileHandler) + * submit module build with scmurl (test SCMHandler) + ..are combined into one method to reuse 1 single test branch. + + Steps: + * Submit module build using module's SCM URL (HTTP POST). + * Assert that build reaches 'build' state. + * Cancel the build (HTTP PATCH) + """ + + # 1) SCMURL submission + repo.bump() + + scmurl = pkg_util.giturl().replace("#", "?#") + branch = scenario["branch"] + data = {"scmurl": scmurl, "branch": branch} + + builds = mbs.submit_module_build(data) + assert len(builds) == 1 + assert_build_in_build_state(mbs, builds[0]) + + # 2) YAML submission (might not be enabled, but if it is, let's test it) + repo.bump() + + data = {"modulemd": str(repo.modulemd)} + try: + builds = mbs.submit_module_build(data) + except HTTPError as e: + if "YAML submission is not enabled" not in e.response.text: + raise + else: + assert_build_in_build_state(mbs, builds[0]) diff --git a/tests/integration/utils.py b/tests/integration/utils.py index a94c552..abbd0b3 100644 --- a/tests/integration/utils.py +++ b/tests/integration/utils.py @@ -20,6 +20,13 @@ our_sh = sh(_out=sys.stdout, _err=sys.stderr, _tee=True) from our_sh import Command, git, pushd # noqa +def get_kerberos_auth(): + """Get the 'default' Kerberos auth header field""" + # (!) User executing this request must be allowed to do so on the target MBS instance. + # MBS server does not support mutual auth, so make it optional (inspired by mbs-cli). + return requests_kerberos.HTTPKerberosAuth(mutual_authentication=requests_kerberos.OPTIONAL) + + class Koji: """Wrapper class to work with Koji @@ -113,14 +120,12 @@ class Repo: :attribute string module_name: name of the module stored in this repo :attribute string branch: name of the branch, the repo is checked-out - :attribute string giturl: GIT URL of the repo/branch :attribute dict _modulemd: Modulemd file as read from the repo """ def __init__(self, module_name, branch): self.module_name = module_name self.branch = branch - self._modulemd = None self._version = None @@ -243,18 +248,23 @@ class PackagingUtility: return stdout - def cancel(self, build): + def cancel(self, build, ignore_errors=False): """Cancel the module build :param Build build: the Build object of the module build to be cancelled. + :param bool ignore_errors: ignore when command fails (ErrorReturnCode exception) :return: Standard output of the "module-build-cancel command :rtype: str """ - stdout = self._packaging_utility("module-build-cancel", build.id).stdout.decode( - "utf-8") - return stdout + cmd = "module-build-cancel" + try: + return self._packaging_utility(cmd, build.id).stdout.decode("utf-8") + except sh.ErrorReturnCode: + if not ignore_errors: + raise def giturl(self): + """Get target URL of the current repository/branch""" return self._packaging_utility("giturl").stdout.decode("utf-8").strip() def clone(self, *args): @@ -519,9 +529,10 @@ class MBS: :param str stream: Stream name :param str order_desc_by: Optional sorting parameter e.g. "version" :return: list of Build objects - :rtype: list + :rtype: list[Build] """ url = f"{self._mbs_api}module-builds/" + payload = {'name': module, "stream": stream} if order_desc_by: payload.update({"order_desc_by": order_desc_by}) @@ -530,40 +541,25 @@ class MBS: return [Build(self._mbs_api, build["id"]) for build in r.json()["items"]] def import_module(self, scmurl): - """Import module from SCM URL (modulemd). + """Import module from SCM URL. :param str scmurl: :return: requests response :rtype: requests response object """ url = f"{self._mbs_api}import-module/" - headers = {"Content-Type": "application/json"} - data = json.dumps({'scmurl': scmurl}) - - # (!) User executing this request must be allowed to do so on the target MBS instance. - # MBS server does not support mutual auth, so make it optional (inspired by mbs-cli). - auth = requests_kerberos.HTTPKerberosAuth(mutual_authentication=requests_kerberos.OPTIONAL) - response = requests.post( - url, - auth=auth, - headers=headers, - verify=False, - data=data - ) - try: - response.raise_for_status() - return response - except requests.exceptions.HTTPError: - # response message contains useful information, which requests module omits - pytest.fail(response.text) + data = json.dumps({'scmurl': scmurl}) + r = requests.post(url, auth=get_kerberos_auth(), verify=False, data=data) + r.raise_for_status() + return r def get_module_builds(self, **kwargs): """Query MBS API on module-builds endpoint - :attribute **kwargs: options for the HTTP GET - :return: list of Build objects - :rtype: list + :return: matched build entries + :rtype: list[Build] + :Keyword Arguments: passed directly to the request as HTTP params. """ url = f"{self._mbs_api}module-builds/" r = requests.get(url, params=kwargs) @@ -574,9 +570,10 @@ class MBS: def get_module_build(self, build_data, **kwargs): """Query MBS API on module-builds endpoint for a specific build - :attribute build_data (int|Build): build ID + :param int|Build build_data: build ID :return: module build object :rtype: Build + :Keyword Arguments: passed directly to the request as HTTP params. """ build_id = self._get_build_id(build_data) url = f"{self._mbs_api}module-builds/{build_id}" @@ -585,6 +582,29 @@ class MBS: r.raise_for_status() return Build(self._mbs_api, r.json()["id"]) + def submit_module_build(self, data): + """Submit a module build with arbitrary payload. + + :param dict data: payload of the POST request + 1) SCMURL submission: data = {scmurl, branch} + 2) YAML submission: data = {modulemd: } + :return: submitted build(s) + :rtype: list[Build] + """ + url = f"{self._mbs_api}module-builds/" + + r = requests.post(url, verify=False, auth=get_kerberos_auth(), data=json.dumps(data)) + r.raise_for_status() + return [Build(self._mbs_api, build["id"]) for build in r.json()] + + def cancel_module_build(self, build_id): + """PATCH the state field of a module build to cancel it""" + url = f"{self._mbs_api}module-builds/{build_id}" + + data = json.dumps({"state": "failed"}) + response = requests.patch(url, auth=get_kerberos_auth(), verify=False, data=data) + response.raise_for_status() + def wait_for_module_build(self, build_data, predicate_func, timeout=60, interval=5): """Wait for module build. Wait until the specified function returns True.