From 60b53d9da17bf61874f54543feb83ad628921e83 Mon Sep 17 00:00:00 2001 From: Facundo Tuesca Date: Fri, 13 Sep 2024 19:22:13 +0200 Subject: [PATCH] Add CLI integration tests for attest subcommand Signed-off-by: Facundo Tuesca --- test/assets/integration/a.txt | 5 + .../attest/slsa_predicate_v0_2.json | 249 ++++++++++++++++++ .../attest/slsa_predicate_v1_0.json | 36 +++ test/integration/cli/conftest.py | 72 +++++ test/integration/cli/test_attest.py | 243 +++++++++++++++++ 5 files changed, 605 insertions(+) create mode 100644 test/assets/integration/a.txt create mode 100644 test/assets/integration/attest/slsa_predicate_v0_2.json create mode 100644 test/assets/integration/attest/slsa_predicate_v1_0.json create mode 100644 test/integration/cli/test_attest.py diff --git a/test/assets/integration/a.txt b/test/assets/integration/a.txt new file mode 100644 index 000000000..8d0585ac7 --- /dev/null +++ b/test/assets/integration/a.txt @@ -0,0 +1,5 @@ +DO NOT MODIFY ME! + +this is "a.txt", a sample input for sigstore-python's unit tests. + +DO NOT MODIFY ME! diff --git a/test/assets/integration/attest/slsa_predicate_v0_2.json b/test/assets/integration/attest/slsa_predicate_v0_2.json new file mode 100644 index 000000000..95c8fad88 --- /dev/null +++ b/test/assets/integration/attest/slsa_predicate_v0_2.json @@ -0,0 +1,249 @@ +{ + "builder": { + "id": "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@refs/tags/v2.0.0" + }, + "buildType": "https://github.com/slsa-framework/slsa-github-generator/generic@v1", + "invocation": { + "configSource": { + "uri": "git+https://github.com/sigstore/sigstore-python@refs/tags/v3.2.0", + "digest": { + "sha1": "fc29ec190575ae345cea23f0953b64ca6f2ab8ba" + }, + "entryPoint": ".github/workflows/release.yml" + }, + "parameters": {}, + "environment": { + "github_actor": "woodruffw", + "github_actor_id": "3059210", + "github_base_ref": "", + "github_event_name": "release", + "github_event_payload": { + "action": "published", + "enterprise": { + "avatar_url": "https://avatars.githubusercontent.com/b/102459?v=4", + "created_at": "2023-12-08T05:54:26Z", + "description": "Open Source Security Foundation (OpenSSF)", + "html_url": "https://github.com/enterprises/openssf", + "id": 102459, + "name": "Open Source Security Foundation", + "node_id": "E_kgDOAAGQOw", + "slug": "openssf", + "updated_at": "2024-01-06T00:47:02Z", + "website_url": "https://openssf.org/" + }, + "organization": { + "avatar_url": "https://avatars.githubusercontent.com/u/71096353?v=4", + "description": "Software Supply Chain Security", + "events_url": "https://api.github.com/orgs/sigstore/events", + "hooks_url": "https://api.github.com/orgs/sigstore/hooks", + "id": 71096353, + "issues_url": "https://api.github.com/orgs/sigstore/issues", + "login": "sigstore", + "members_url": "https://api.github.com/orgs/sigstore/members{/member}", + "node_id": "MDEyOk9yZ2FuaXphdGlvbjcxMDk2MzUz", + "public_members_url": "https://api.github.com/orgs/sigstore/public_members{/member}", + "repos_url": "https://api.github.com/orgs/sigstore/repos", + "url": "https://api.github.com/orgs/sigstore" + }, + "release": { + "assets": [], + "assets_url": "https://api.github.com/repos/sigstore/sigstore-python/releases/170913493/assets", + "author": { + "avatar_url": "https://avatars.githubusercontent.com/u/3059210?v=4", + "events_url": "https://api.github.com/users/woodruffw/events{/privacy}", + "followers_url": "https://api.github.com/users/woodruffw/followers", + "following_url": "https://api.github.com/users/woodruffw/following{/other_user}", + "gists_url": "https://api.github.com/users/woodruffw/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/woodruffw", + "id": 3059210, + "login": "woodruffw", + "node_id": "MDQ6VXNlcjMwNTkyMTA=", + "organizations_url": "https://api.github.com/users/woodruffw/orgs", + "received_events_url": "https://api.github.com/users/woodruffw/received_events", + "repos_url": "https://api.github.com/users/woodruffw/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/woodruffw/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/woodruffw/subscriptions", + "type": "User", + "url": "https://api.github.com/users/woodruffw" + }, + "body": "### Added\n\n* API: `models.Bundle.BundleType` is now a public API\n ([#1089](https://github.com/sigstore/sigstore-python/pull/1089))\n\n* CLI: The `sigstore plumbing` subcommand hierarchy has been added. This\n hierarchy is for *developer-only* interactions, such as fixing malformed\n Sigstore bundles. These subcommands are **not considered stable until\n explicitly documented as such**.\n ([#1089](https://github.com/sigstore/sigstore-python/pull/1089))\n\n### Changed\n\n* CLI: The default console logger now emits to `stderr`, rather than `stdout`\n ([#1089](https://github.com/sigstore/sigstore-python/pull/1089))\n\n", + "created_at": "2024-08-19T17:14:19Z", + "draft": false, + "html_url": "https://github.com/sigstore/sigstore-python/releases/tag/v3.2.0", + "id": 170913493, + "name": "v3.2.0", + "node_id": "RE_kwDOGq85Ts4KL-7V", + "prerelease": false, + "published_at": "2024-08-19T17:15:11Z", + "tag_name": "v3.2.0", + "tarball_url": "https://api.github.com/repos/sigstore/sigstore-python/tarball/v3.2.0", + "target_commitish": "main", + "upload_url": "https://uploads.github.com/repos/sigstore/sigstore-python/releases/170913493/assets{?name,label}", + "url": "https://api.github.com/repos/sigstore/sigstore-python/releases/170913493", + "zipball_url": "https://api.github.com/repos/sigstore/sigstore-python/zipball/v3.2.0" + }, + "repository": { + "allow_forking": true, + "archive_url": "https://api.github.com/repos/sigstore/sigstore-python/{archive_format}{/ref}", + "archived": false, + "assignees_url": "https://api.github.com/repos/sigstore/sigstore-python/assignees{/user}", + "blobs_url": "https://api.github.com/repos/sigstore/sigstore-python/git/blobs{/sha}", + "branches_url": "https://api.github.com/repos/sigstore/sigstore-python/branches{/branch}", + "clone_url": "https://github.com/sigstore/sigstore-python.git", + "collaborators_url": "https://api.github.com/repos/sigstore/sigstore-python/collaborators{/collaborator}", + "comments_url": "https://api.github.com/repos/sigstore/sigstore-python/comments{/number}", + "commits_url": "https://api.github.com/repos/sigstore/sigstore-python/commits{/sha}", + "compare_url": "https://api.github.com/repos/sigstore/sigstore-python/compare/{base}...{head}", + "contents_url": "https://api.github.com/repos/sigstore/sigstore-python/contents/{+path}", + "contributors_url": "https://api.github.com/repos/sigstore/sigstore-python/contributors", + "created_at": "2022-01-13T17:29:37Z", + "custom_properties": {}, + "default_branch": "main", + "deployments_url": "https://api.github.com/repos/sigstore/sigstore-python/deployments", + "description": "A Sigstore client written in Python", + "disabled": false, + "downloads_url": "https://api.github.com/repos/sigstore/sigstore-python/downloads", + "events_url": "https://api.github.com/repos/sigstore/sigstore-python/events", + "fork": false, + "forks": 41, + "forks_count": 41, + "forks_url": "https://api.github.com/repos/sigstore/sigstore-python/forks", + "full_name": "sigstore/sigstore-python", + "git_commits_url": "https://api.github.com/repos/sigstore/sigstore-python/git/commits{/sha}", + "git_refs_url": "https://api.github.com/repos/sigstore/sigstore-python/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/sigstore/sigstore-python/git/tags{/sha}", + "git_url": "git://github.com/sigstore/sigstore-python.git", + "has_discussions": false, + "has_downloads": true, + "has_issues": true, + "has_pages": true, + "has_projects": true, + "has_wiki": false, + "homepage": "https://pypi.org/p/sigstore", + "hooks_url": "https://api.github.com/repos/sigstore/sigstore-python/hooks", + "html_url": "https://github.com/sigstore/sigstore-python", + "id": 447691086, + "is_template": false, + "issue_comment_url": "https://api.github.com/repos/sigstore/sigstore-python/issues/comments{/number}", + "issue_events_url": "https://api.github.com/repos/sigstore/sigstore-python/issues/events{/number}", + "issues_url": "https://api.github.com/repos/sigstore/sigstore-python/issues{/number}", + "keys_url": "https://api.github.com/repos/sigstore/sigstore-python/keys{/key_id}", + "labels_url": "https://api.github.com/repos/sigstore/sigstore-python/labels{/name}", + "language": "Python", + "languages_url": "https://api.github.com/repos/sigstore/sigstore-python/languages", + "license": { + "key": "other", + "name": "Other", + "node_id": "MDc6TGljZW5zZTA=", + "spdx_id": "NOASSERTION", + "url": null + }, + "merges_url": "https://api.github.com/repos/sigstore/sigstore-python/merges", + "milestones_url": "https://api.github.com/repos/sigstore/sigstore-python/milestones{/number}", + "mirror_url": null, + "name": "sigstore-python", + "node_id": "R_kgDOGq85Tg", + "notifications_url": "https://api.github.com/repos/sigstore/sigstore-python/notifications{?since,all,participating}", + "open_issues": 28, + "open_issues_count": 28, + "owner": { + "avatar_url": "https://avatars.githubusercontent.com/u/71096353?v=4", + "events_url": "https://api.github.com/users/sigstore/events{/privacy}", + "followers_url": "https://api.github.com/users/sigstore/followers", + "following_url": "https://api.github.com/users/sigstore/following{/other_user}", + "gists_url": "https://api.github.com/users/sigstore/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/sigstore", + "id": 71096353, + "login": "sigstore", + "node_id": "MDEyOk9yZ2FuaXphdGlvbjcxMDk2MzUz", + "organizations_url": "https://api.github.com/users/sigstore/orgs", + "received_events_url": "https://api.github.com/users/sigstore/received_events", + "repos_url": "https://api.github.com/users/sigstore/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/sigstore/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/sigstore/subscriptions", + "type": "Organization", + "url": "https://api.github.com/users/sigstore" + }, + "private": false, + "pulls_url": "https://api.github.com/repos/sigstore/sigstore-python/pulls{/number}", + "pushed_at": "2024-08-19T17:14:57Z", + "releases_url": "https://api.github.com/repos/sigstore/sigstore-python/releases{/id}", + "size": 1835, + "ssh_url": "git@github.com:sigstore/sigstore-python.git", + "stargazers_count": 219, + "stargazers_url": "https://api.github.com/repos/sigstore/sigstore-python/stargazers", + "statuses_url": "https://api.github.com/repos/sigstore/sigstore-python/statuses/{sha}", + "subscribers_url": "https://api.github.com/repos/sigstore/sigstore-python/subscribers", + "subscription_url": "https://api.github.com/repos/sigstore/sigstore-python/subscription", + "svn_url": "https://github.com/sigstore/sigstore-python", + "tags_url": "https://api.github.com/repos/sigstore/sigstore-python/tags", + "teams_url": "https://api.github.com/repos/sigstore/sigstore-python/teams", + "topics": [ + "codesigning", + "python", + "security", + "supply-chain" + ], + "trees_url": "https://api.github.com/repos/sigstore/sigstore-python/git/trees{/sha}", + "updated_at": "2024-08-19T17:14:23Z", + "url": "https://api.github.com/repos/sigstore/sigstore-python", + "visibility": "public", + "watchers": 219, + "watchers_count": 219, + "web_commit_signoff_required": true + }, + "sender": { + "avatar_url": "https://avatars.githubusercontent.com/u/3059210?v=4", + "events_url": "https://api.github.com/users/woodruffw/events{/privacy}", + "followers_url": "https://api.github.com/users/woodruffw/followers", + "following_url": "https://api.github.com/users/woodruffw/following{/other_user}", + "gists_url": "https://api.github.com/users/woodruffw/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/woodruffw", + "id": 3059210, + "login": "woodruffw", + "node_id": "MDQ6VXNlcjMwNTkyMTA=", + "organizations_url": "https://api.github.com/users/woodruffw/orgs", + "received_events_url": "https://api.github.com/users/woodruffw/received_events", + "repos_url": "https://api.github.com/users/woodruffw/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/woodruffw/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/woodruffw/subscriptions", + "type": "User", + "url": "https://api.github.com/users/woodruffw" + } + }, + "github_head_ref": "", + "github_ref": "refs/tags/v3.2.0", + "github_ref_type": "tag", + "github_repository_id": "447691086", + "github_repository_owner": "sigstore", + "github_repository_owner_id": "71096353", + "github_run_attempt": "1", + "github_run_id": "10457864437", + "github_run_number": "61", + "github_sha1": "fc29ec190575ae345cea23f0953b64ca6f2ab8ba" + } + }, + "metadata": { + "buildInvocationId": "10457864437-1", + "completeness": { + "parameters": true, + "environment": false, + "materials": false + }, + "reproducible": false + }, + "materials": [ + { + "uri": "git+https://github.com/sigstore/sigstore-python@refs/tags/v3.2.0", + "digest": { + "sha1": "fc29ec190575ae345cea23f0953b64ca6f2ab8ba" + } + } + ] +} \ No newline at end of file diff --git a/test/assets/integration/attest/slsa_predicate_v1_0.json b/test/assets/integration/attest/slsa_predicate_v1_0.json new file mode 100644 index 000000000..fc59b8fcf --- /dev/null +++ b/test/assets/integration/attest/slsa_predicate_v1_0.json @@ -0,0 +1,36 @@ +{ + "buildDefinition": { + "buildType": "https://actions.github.io/buildtypes/workflow/v1", + "externalParameters": { + "workflow": { + "ref": "refs/tags/1.21.0", + "repository": "https://github.com/octo-org/octo-repo", + "path": ".github/workflows/ci.yaml" + } + }, + "internalParameters": { + "github": { + "event_name": "push", + "repository_id": "000000000", + "repository_owner_id": "0000000", + "runner_environment": "github-hosted" + } + }, + "resolvedDependencies": [ + { + "uri": "git+https://github.com/octo-org/octo-repo@refs/tags/1.21.0", + "digest": { + "gitCommit": "1ac93ce21ee526b36fd154b9058d97dfaa424c50" + } + } + ] + }, + "runDetails": { + "builder": { + "id": "https://github.com/octo-org/octo-repo/.github/workflows/docker.yaml@refs/heads/development" + }, + "metadata": { + "invocationId": "https://github.com/octo-org/octo-repo/actions/runs/10313983218/attempts/2" + } + } +} \ No newline at end of file diff --git a/test/integration/cli/conftest.py b/test/integration/cli/conftest.py index 1e689d09a..282795350 100644 --- a/test/integration/cli/conftest.py +++ b/test/integration/cli/conftest.py @@ -12,17 +12,89 @@ # See the License for the specific language governing permissions and # limitations under the License. +import os from pathlib import Path from typing import Callable import pytest +from id import ( + AmbientCredentialError, + GitHubOidcPermissionCredentialError, + detect_credential, +) from sigstore._cli import main +from sigstore.oidc import _DEFAULT_AUDIENCE _ASSETS = (Path(__file__).parent.parent.parent / "assets/integration").resolve() assert _ASSETS.is_dir() +def _has_oidc_id(): + # If there are tokens manually defined for us in the environment, use them. + if os.getenv("SIGSTORE_IDENTITY_TOKEN_production") or os.getenv( + "SIGSTORE_IDENTITY_TOKEN_staging" + ): + return True + + try: + token = detect_credential(_DEFAULT_AUDIENCE) + if token is None: + return False + except GitHubOidcPermissionCredentialError: + # On GitHub Actions, forks do not have access to OIDC identities. + # We differentiate this case from other GitHub credential errors, + # since it's a case where we want to skip (i.e. return False). + if os.getenv("GITHUB_EVENT_NAME") == "pull_request": + return False + return True + except AmbientCredentialError: + # If ambient credential detection raises, then we *are* in an ambient + # environment but one that's been configured incorrectly. We + # pass this through, so that the CI fails appropriately rather than + # silently skipping the faulty tests. + return True + + return True + + +def pytest_runtest_setup(item): + # Do we need a network connection? + online = False + for mark in ["online", "staging", "production"]: + if mark in item.keywords: + online = True + + if online and item.config.getoption("--skip-online"): + pytest.skip( + "skipping test that requires network connectivity due to `--skip-online` flag" + ) + elif "ambient_oidc" in item.keywords and not _has_oidc_id(): + pytest.skip("skipping test that requires an ambient OIDC credential") + + if "staging" in item.keywords and item.config.getoption("--skip-staging"): + pytest.skip( + "skipping test that requires staging infrastructure due to `--skip-staging` flag" + ) + + +def pytest_configure(config): + config.addinivalue_line( + "markers", "staging: mark test as requiring Sigstore staging infrastructure" + ) + config.addinivalue_line( + "markers", + "production: mark test as requiring Sigstore production infrastructure", + ) + config.addinivalue_line( + "markers", + "online: mark test as requiring network connectivity (but not a specific Sigstore infrastructure)", + ) + config.addinivalue_line( + "markers", "ambient_oidc: mark test as requiring an ambient OIDC identity" + ) + + @pytest.fixture def asset(): def _asset(name: str) -> Path: diff --git a/test/integration/cli/test_attest.py b/test/integration/cli/test_attest.py new file mode 100644 index 000000000..813823452 --- /dev/null +++ b/test/integration/cli/test_attest.py @@ -0,0 +1,243 @@ +# Copyright 2024 The Sigstore Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from pathlib import Path +from typing import List, Optional + +import pytest + +from sigstore.dsse._predicate import PredicateType +from sigstore.models import Bundle +from sigstore.verify import Verifier +from sigstore.verify.policy import UnsafeNoOp + + +def get_cli_params( + pred_type: str, + pred_path: Path, + artifact_path: Path, + overwrite: bool = False, + bundle_path: Optional[Path] = None, +) -> List[str]: + cli_params = [ + "--staging", + "attest", + "--predicate-type", + pred_type, + "--predicate", + str(pred_path), + ] + if bundle_path is not None: + cli_params.extend(["--bundle", str(bundle_path)]) + if overwrite: + cli_params.append("--overwrite") + cli_params.append(str(artifact_path)) + + return cli_params + + +@pytest.mark.staging +@pytest.mark.ambient_oidc +@pytest.mark.parametrize( + ("predicate_type", "predicate_filename"), + [ + (PredicateType.SLSA_v0_2, "slsa_predicate_v0_2.json"), + (PredicateType.SLSA_v1_0, "slsa_predicate_v1_0.json"), + ], +) +def test_attest_success_default_output_bundle( + capsys, sigstore, asset, predicate_type, predicate_filename +): + predicate_path = asset(f"attest/{predicate_filename}") + artifact = asset("a.txt") + expected_output_bundle = artifact.with_name("a.txt.sigstore.json") + + assert not expected_output_bundle.exists() + sigstore( + *get_cli_params( + pred_type=predicate_type, + pred_path=predicate_path, + artifact_path=artifact, + ) + ) + + assert expected_output_bundle.exists() + verifier = Verifier.staging() + with open(expected_output_bundle, "r") as bundle_file: + bundle = Bundle.from_json(bundle_file.read()) + verifier.verify_dsse(bundle=bundle, policy=UnsafeNoOp()) + + expected_output_bundle.unlink() + + captures = capsys.readouterr() + assert captures.out.endswith( + f"Sigstore bundle written to {str(expected_output_bundle)}\n" + ) + + +@pytest.mark.staging +@pytest.mark.ambient_oidc +def test_attest_success_custom_output_bundle(capsys, sigstore, asset, tmp_path): + predicate_type = PredicateType.SLSA_v0_2 + predicate_filename = "slsa_predicate_v0_2.json" + predicate_path = asset(f"attest/{predicate_filename}") + artifact = asset("a.txt") + + output_bundle = tmp_path / "bundle.json" + assert not output_bundle.exists() + sigstore( + *get_cli_params( + pred_type=predicate_type, + pred_path=predicate_path, + artifact_path=artifact, + bundle_path=output_bundle, + ) + ) + + assert output_bundle.exists() + captures = capsys.readouterr() + assert captures.out.endswith(f"Sigstore bundle written to {str(output_bundle)}\n") + + +@pytest.mark.staging +@pytest.mark.ambient_oidc +def test_attest_overwrite_existing_bundle(capsys, sigstore, asset, tmp_path): + predicate_type = PredicateType.SLSA_v0_2 + predicate_filename = "slsa_predicate_v0_2.json" + predicate_path = asset(f"attest/{predicate_filename}") + artifact = asset("a.txt") + + output_bundle = tmp_path / "bundle.json" + assert not output_bundle.exists() + + cli_params = get_cli_params( + pred_type=predicate_type, + pred_path=predicate_path, + artifact_path=artifact, + bundle_path=output_bundle, + ) + sigstore(*cli_params) + assert output_bundle.exists() + + # On invalid argument errors we call `Argumentparser.error`, which prints + # a message and exits with code 2 + with pytest.raises(SystemExit) as e: + sigstore(*cli_params) + assert e.value.code == 2 + + assert output_bundle.exists() + captures = capsys.readouterr() + assert captures.err.endswith( + f"Refusing to overwrite outputs without --overwrite: {str(output_bundle)}\n" + ) + + cli_params.append("--overwrite") + sigstore(*cli_params) + assert output_bundle.exists() + + assert captures.out.endswith(f"Sigstore bundle written to {str(output_bundle)}\n") + + +def test_attest_invalid_predicate_type(capsys, sigstore, asset, tmp_path): + predicate_type = "invalid_type" + predicate_filename = "slsa_predicate_v0_2.json" + predicate_path = asset(f"attest/{predicate_filename}") + artifact = asset("a.txt") + + output_bundle = tmp_path / "bundle.json" + # On invalid argument errors we call `Argumentparser.error`, which prints + # a message and exits with code 2 + with pytest.raises(SystemExit) as e: + sigstore( + *get_cli_params( + pred_type=predicate_type, + pred_path=predicate_path, + artifact_path=artifact, + bundle_path=output_bundle, + ) + ) + assert e.value.code == 2 + + captures = capsys.readouterr() + assert captures.err.endswith(f"invalid PredicateType value: '{predicate_type}'\n") + + +def test_attest_mismatching_predicate(capsys, sigstore, asset, tmp_path): + predicate_type = PredicateType.SLSA_v0_2 + predicate_filename = "slsa_predicate_v1_0.json" + predicate_path = asset(f"attest/{predicate_filename}") + artifact = asset("a.txt") + + output_bundle = tmp_path / "bundle.json" + # On invalid argument errors we call `Argumentparser.error`, which prints + # a message and exits with code 2 + with pytest.raises(SystemExit) as e: + sigstore( + *get_cli_params( + pred_type=predicate_type, + pred_path=predicate_path, + artifact_path=artifact, + bundle_path=output_bundle, + ) + ) + assert e.value.code == 2 + + captures = capsys.readouterr() + assert f'Unable to parse predicate of type "{predicate_type}":' in captures.err + + +def test_attest_missing_predicate(capsys, sigstore, asset, tmp_path): + predicate_type = PredicateType.SLSA_v0_2 + predicate_filename = "doesnt_exist.json" + predicate_path = asset(f"attest/{predicate_filename}") + artifact = asset("a.txt") + + output_bundle = tmp_path / "bundle.json" + # On invalid argument errors we call `Argumentparser.error`, which prints + # a message and exits with code 2 + with pytest.raises(SystemExit) as e: + sigstore( + *get_cli_params( + pred_type=predicate_type, + pred_path=predicate_path, + artifact_path=artifact, + bundle_path=output_bundle, + ) + ) + assert e.value.code == 2 + + captures = capsys.readouterr() + assert captures.err.endswith(f"Predicate must be a file: {predicate_path}\n") + + +def test_attest_invalid_json_predicate(capsys, sigstore, asset, tmp_path): + predicate_type = PredicateType.SLSA_v0_2 + predicate_path = asset("a.txt") + artifact = asset("a.txt") + + output_bundle = tmp_path / "bundle.json" + # On invalid argument errors we call `Argumentparser.error`, which prints + # a message and exits with code 2 + with pytest.raises(SystemExit) as e: + sigstore( + *get_cli_params( + pred_type=predicate_type, + pred_path=predicate_path, + artifact_path=artifact, + bundle_path=output_bundle, + ) + ) + assert e.value.code == 2 + + captures = capsys.readouterr() + assert f'Unable to parse predicate of type "{predicate_type}":' in captures.err